Loading core/java/android/app/Notification.java +16 −0 Original line number Diff line number Diff line Loading @@ -617,6 +617,13 @@ public class Notification implements Parcelable */ public static final int FLAG_CAN_COLORIZE = 0x00000800; /** * Bit to be bitswised-ored into the {@link #flags} field that should be * set if this notification can be shown as a bubble. * @hide */ public static final int FLAG_BUBBLE = 0x00001000; public int flags; /** @hide */ Loading Loading @@ -6243,6 +6250,15 @@ public class Notification implements Parcelable return false; } /** * @return true if this is a notification that can show as a bubble. * * @hide */ public boolean isBubbleNotification() { return (flags & Notification.FLAG_BUBBLE) != 0; } private boolean hasLargeIcon() { return mLargeIcon != null || largeIcon != null; } Loading services/core/java/com/android/server/notification/NotificationManagerService.java +17 −0 Original line number Diff line number Diff line Loading @@ -4740,6 +4740,20 @@ public class NotificationManagerService extends SystemService { } } /** * Updates the flags for this notification to reflect whether it is a bubble or not. */ private void flagNotificationForBubbles(NotificationRecord r, String pkg, int userId) { Notification notification = r.getNotification(); boolean canBubble = mPreferencesHelper.areBubblesAllowed(pkg, userId) && r.getChannel().canBubble(); if (notification.getBubbleMetadata() != null && canBubble) { notification.flags |= Notification.FLAG_BUBBLE; } else { notification.flags &= ~Notification.FLAG_BUBBLE; } } private void doChannelWarningToast(CharSequence toastText) { Binder.withCleanCallingIdentity(() -> { final int defaultWarningEnabled = Build.IS_DEBUGGABLE ? 1 : 0; Loading Loading @@ -5095,6 +5109,9 @@ public class NotificationManagerService extends SystemService { final int id = n.getId(); final String tag = n.getTag(); // We need to fix the notification up a little for bubbles flagNotificationForBubbles(r, pkg, callingUid); // Handle grouped notifications and bail out early if we // can to avoid extracting signals. handleGroupedNotificationLocked(r, old, callingUid, callingPid); Loading services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +140 −1 Original line number Diff line number Diff line Loading @@ -79,6 +79,7 @@ import android.app.Notification.MessagingStyle.Message; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.admin.DevicePolicyManagerInternal; import android.app.usage.UsageStatsManagerInternal; import android.companion.ICompanionDeviceManager; Loading @@ -92,6 +93,7 @@ import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.content.res.Resources; import android.graphics.Color; import android.graphics.drawable.Icon; import android.media.AudioManager; import android.net.Uri; import android.os.Binder; Loading Loading @@ -149,7 +151,6 @@ import org.mockito.stubbing.Answer; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.util.ArrayList; import java.util.Arrays; Loading Loading @@ -507,6 +508,13 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { false); } private Notification.BubbleMetadata.Builder getBasicBubbleMetadataBuilder() { PendingIntent pi = PendingIntent.getActivity(mContext, 0, new Intent(), 0); return new Notification.BubbleMetadata.Builder() .setIntent(pi) .setIcon(Icon.createWithResource(mContext, android.R.drawable.sym_def_app_icon)); } @Test public void testCreateNotificationChannels_SingleChannel() throws Exception { final NotificationChannel channel = Loading Loading @@ -4290,6 +4298,137 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { .onGranted(eq(xmlConfig), eq(0), eq(true)); } @Test public void testFlagBubbleNotifs_flagIfAllowed() throws RemoteException { // Bubbles are allowed! mService.setPreferencesHelper(mPreferencesHelper); when(mPreferencesHelper.areBubblesAllowed(anyString(), anyInt())).thenReturn(true); when(mPreferencesHelper.getNotificationChannel( anyString(), anyInt(), anyString(), anyBoolean())).thenReturn( mTestNotificationChannel); when(mPreferencesHelper.getImportance(anyString(), anyInt())).thenReturn( mTestNotificationChannel.getImportance()); // Notif with bubble metadata Notification.BubbleMetadata data = getBasicBubbleMetadataBuilder().build(); Notification.Builder nb = new Notification.Builder(mContext, mTestNotificationChannel.getId()) .setContentTitle("foo") .setBubbleMetadata(data) .setSmallIcon(android.R.drawable.sym_def_app_icon); StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1, null, mUid, 0, nb.build(), new UserHandle(mUid), null, 0); NotificationRecord nr = new NotificationRecord(mContext, sbn, mTestNotificationChannel); mBinderService.enqueueNotificationWithTag(PKG, PKG, null, nr.sbn.getId(), nr.sbn.getNotification(), nr.sbn.getUserId()); waitForIdle(); // yes allowed, yes bubble assertTrue(mService.getNotificationRecord( sbn.getKey()).getNotification().isBubbleNotification()); } @Test public void testFlagBubbleNotifs_noFlagIfNotAllowed() throws RemoteException { // Bubbles are NOT allowed! mService.setPreferencesHelper(mPreferencesHelper); when(mPreferencesHelper.areBubblesAllowed(anyString(), anyInt())).thenReturn(false); when(mPreferencesHelper.getNotificationChannel( anyString(), anyInt(), anyString(), anyBoolean())).thenReturn( mTestNotificationChannel); when(mPreferencesHelper.getImportance(anyString(), anyInt())).thenReturn( mTestNotificationChannel.getImportance()); // Notif with bubble metadata Notification.BubbleMetadata data = getBasicBubbleMetadataBuilder().build(); Notification.Builder nb = new Notification.Builder(mContext, mTestNotificationChannel.getId()) .setContentTitle("foo") .setBubbleMetadata(data) .setSmallIcon(android.R.drawable.sym_def_app_icon); StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1, null, mUid, 0, nb.build(), new UserHandle(mUid), null, 0); NotificationRecord nr = new NotificationRecord(mContext, sbn, mTestNotificationChannel); // Post the notification mBinderService.enqueueNotificationWithTag(PKG, PKG, null, nr.sbn.getId(), nr.sbn.getNotification(), nr.sbn.getUserId()); waitForIdle(); // not allowed, no bubble assertFalse(mService.getNotificationRecord( sbn.getKey()).getNotification().isBubbleNotification()); } @Test public void testFlagBubbleNotifs_noFlagIfNotBubble() throws RemoteException { // Bubbles are allowed! mService.setPreferencesHelper(mPreferencesHelper); when(mPreferencesHelper.areBubblesAllowed(anyString(), anyInt())).thenReturn(true); when(mPreferencesHelper.getNotificationChannel( anyString(), anyInt(), anyString(), anyBoolean())).thenReturn( mTestNotificationChannel); when(mPreferencesHelper.getImportance(anyString(), anyInt())).thenReturn( mTestNotificationChannel.getImportance()); // Notif WITHOUT bubble metadata Notification.Builder nb = new Notification.Builder(mContext, mTestNotificationChannel.getId()) .setContentTitle("foo") .setSmallIcon(android.R.drawable.sym_def_app_icon); StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1, null, mUid, 0, nb.build(), new UserHandle(mUid), null, 0); NotificationRecord nr = new NotificationRecord(mContext, sbn, mTestNotificationChannel); // Post the notification mBinderService.enqueueNotificationWithTag(PKG, PKG, null, nr.sbn.getId(), nr.sbn.getNotification(), nr.sbn.getUserId()); waitForIdle(); // no bubble metadata, no bubble assertFalse(mService.getNotificationRecord( sbn.getKey()).getNotification().isBubbleNotification()); } @Test public void testFlagBubbleNotifs_noFlagIfChannelNotBubble() throws RemoteException { // Bubbles are allowed! mService.setPreferencesHelper(mPreferencesHelper); when(mPreferencesHelper.areBubblesAllowed(anyString(), anyInt())).thenReturn(true); when(mPreferencesHelper.getNotificationChannel( anyString(), anyInt(), anyString(), anyBoolean())).thenReturn( mTestNotificationChannel); when(mPreferencesHelper.getImportance(anyString(), anyInt())).thenReturn( mTestNotificationChannel.getImportance()); // But not on this channel! mTestNotificationChannel.setAllowBubbles(false); // Notif with bubble metadata Notification.BubbleMetadata data = getBasicBubbleMetadataBuilder().build(); Notification.Builder nb = new Notification.Builder(mContext, mTestNotificationChannel.getId()) .setContentTitle("foo") .setBubbleMetadata(data) .setSmallIcon(android.R.drawable.sym_def_app_icon); StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1, null, mUid, 0, nb.build(), new UserHandle(mUid), null, 0); NotificationRecord nr = new NotificationRecord(mContext, sbn, mTestNotificationChannel); // Post the notification mBinderService.enqueueNotificationWithTag(PKG, PKG, null, nr.sbn.getId(), nr.sbn.getNotification(), nr.sbn.getUserId()); waitForIdle(); // channel not allowed, no bubble assertFalse(mService.getNotificationRecord( sbn.getKey()).getNotification().isBubbleNotification()); } public void testGetAllowedAssistantCapabilities() throws Exception { Loading Loading
core/java/android/app/Notification.java +16 −0 Original line number Diff line number Diff line Loading @@ -617,6 +617,13 @@ public class Notification implements Parcelable */ public static final int FLAG_CAN_COLORIZE = 0x00000800; /** * Bit to be bitswised-ored into the {@link #flags} field that should be * set if this notification can be shown as a bubble. * @hide */ public static final int FLAG_BUBBLE = 0x00001000; public int flags; /** @hide */ Loading Loading @@ -6243,6 +6250,15 @@ public class Notification implements Parcelable return false; } /** * @return true if this is a notification that can show as a bubble. * * @hide */ public boolean isBubbleNotification() { return (flags & Notification.FLAG_BUBBLE) != 0; } private boolean hasLargeIcon() { return mLargeIcon != null || largeIcon != null; } Loading
services/core/java/com/android/server/notification/NotificationManagerService.java +17 −0 Original line number Diff line number Diff line Loading @@ -4740,6 +4740,20 @@ public class NotificationManagerService extends SystemService { } } /** * Updates the flags for this notification to reflect whether it is a bubble or not. */ private void flagNotificationForBubbles(NotificationRecord r, String pkg, int userId) { Notification notification = r.getNotification(); boolean canBubble = mPreferencesHelper.areBubblesAllowed(pkg, userId) && r.getChannel().canBubble(); if (notification.getBubbleMetadata() != null && canBubble) { notification.flags |= Notification.FLAG_BUBBLE; } else { notification.flags &= ~Notification.FLAG_BUBBLE; } } private void doChannelWarningToast(CharSequence toastText) { Binder.withCleanCallingIdentity(() -> { final int defaultWarningEnabled = Build.IS_DEBUGGABLE ? 1 : 0; Loading Loading @@ -5095,6 +5109,9 @@ public class NotificationManagerService extends SystemService { final int id = n.getId(); final String tag = n.getTag(); // We need to fix the notification up a little for bubbles flagNotificationForBubbles(r, pkg, callingUid); // Handle grouped notifications and bail out early if we // can to avoid extracting signals. handleGroupedNotificationLocked(r, old, callingUid, callingPid); Loading
services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +140 −1 Original line number Diff line number Diff line Loading @@ -79,6 +79,7 @@ import android.app.Notification.MessagingStyle.Message; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.admin.DevicePolicyManagerInternal; import android.app.usage.UsageStatsManagerInternal; import android.companion.ICompanionDeviceManager; Loading @@ -92,6 +93,7 @@ import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.content.res.Resources; import android.graphics.Color; import android.graphics.drawable.Icon; import android.media.AudioManager; import android.net.Uri; import android.os.Binder; Loading Loading @@ -149,7 +151,6 @@ import org.mockito.stubbing.Answer; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.util.ArrayList; import java.util.Arrays; Loading Loading @@ -507,6 +508,13 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { false); } private Notification.BubbleMetadata.Builder getBasicBubbleMetadataBuilder() { PendingIntent pi = PendingIntent.getActivity(mContext, 0, new Intent(), 0); return new Notification.BubbleMetadata.Builder() .setIntent(pi) .setIcon(Icon.createWithResource(mContext, android.R.drawable.sym_def_app_icon)); } @Test public void testCreateNotificationChannels_SingleChannel() throws Exception { final NotificationChannel channel = Loading Loading @@ -4290,6 +4298,137 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { .onGranted(eq(xmlConfig), eq(0), eq(true)); } @Test public void testFlagBubbleNotifs_flagIfAllowed() throws RemoteException { // Bubbles are allowed! mService.setPreferencesHelper(mPreferencesHelper); when(mPreferencesHelper.areBubblesAllowed(anyString(), anyInt())).thenReturn(true); when(mPreferencesHelper.getNotificationChannel( anyString(), anyInt(), anyString(), anyBoolean())).thenReturn( mTestNotificationChannel); when(mPreferencesHelper.getImportance(anyString(), anyInt())).thenReturn( mTestNotificationChannel.getImportance()); // Notif with bubble metadata Notification.BubbleMetadata data = getBasicBubbleMetadataBuilder().build(); Notification.Builder nb = new Notification.Builder(mContext, mTestNotificationChannel.getId()) .setContentTitle("foo") .setBubbleMetadata(data) .setSmallIcon(android.R.drawable.sym_def_app_icon); StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1, null, mUid, 0, nb.build(), new UserHandle(mUid), null, 0); NotificationRecord nr = new NotificationRecord(mContext, sbn, mTestNotificationChannel); mBinderService.enqueueNotificationWithTag(PKG, PKG, null, nr.sbn.getId(), nr.sbn.getNotification(), nr.sbn.getUserId()); waitForIdle(); // yes allowed, yes bubble assertTrue(mService.getNotificationRecord( sbn.getKey()).getNotification().isBubbleNotification()); } @Test public void testFlagBubbleNotifs_noFlagIfNotAllowed() throws RemoteException { // Bubbles are NOT allowed! mService.setPreferencesHelper(mPreferencesHelper); when(mPreferencesHelper.areBubblesAllowed(anyString(), anyInt())).thenReturn(false); when(mPreferencesHelper.getNotificationChannel( anyString(), anyInt(), anyString(), anyBoolean())).thenReturn( mTestNotificationChannel); when(mPreferencesHelper.getImportance(anyString(), anyInt())).thenReturn( mTestNotificationChannel.getImportance()); // Notif with bubble metadata Notification.BubbleMetadata data = getBasicBubbleMetadataBuilder().build(); Notification.Builder nb = new Notification.Builder(mContext, mTestNotificationChannel.getId()) .setContentTitle("foo") .setBubbleMetadata(data) .setSmallIcon(android.R.drawable.sym_def_app_icon); StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1, null, mUid, 0, nb.build(), new UserHandle(mUid), null, 0); NotificationRecord nr = new NotificationRecord(mContext, sbn, mTestNotificationChannel); // Post the notification mBinderService.enqueueNotificationWithTag(PKG, PKG, null, nr.sbn.getId(), nr.sbn.getNotification(), nr.sbn.getUserId()); waitForIdle(); // not allowed, no bubble assertFalse(mService.getNotificationRecord( sbn.getKey()).getNotification().isBubbleNotification()); } @Test public void testFlagBubbleNotifs_noFlagIfNotBubble() throws RemoteException { // Bubbles are allowed! mService.setPreferencesHelper(mPreferencesHelper); when(mPreferencesHelper.areBubblesAllowed(anyString(), anyInt())).thenReturn(true); when(mPreferencesHelper.getNotificationChannel( anyString(), anyInt(), anyString(), anyBoolean())).thenReturn( mTestNotificationChannel); when(mPreferencesHelper.getImportance(anyString(), anyInt())).thenReturn( mTestNotificationChannel.getImportance()); // Notif WITHOUT bubble metadata Notification.Builder nb = new Notification.Builder(mContext, mTestNotificationChannel.getId()) .setContentTitle("foo") .setSmallIcon(android.R.drawable.sym_def_app_icon); StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1, null, mUid, 0, nb.build(), new UserHandle(mUid), null, 0); NotificationRecord nr = new NotificationRecord(mContext, sbn, mTestNotificationChannel); // Post the notification mBinderService.enqueueNotificationWithTag(PKG, PKG, null, nr.sbn.getId(), nr.sbn.getNotification(), nr.sbn.getUserId()); waitForIdle(); // no bubble metadata, no bubble assertFalse(mService.getNotificationRecord( sbn.getKey()).getNotification().isBubbleNotification()); } @Test public void testFlagBubbleNotifs_noFlagIfChannelNotBubble() throws RemoteException { // Bubbles are allowed! mService.setPreferencesHelper(mPreferencesHelper); when(mPreferencesHelper.areBubblesAllowed(anyString(), anyInt())).thenReturn(true); when(mPreferencesHelper.getNotificationChannel( anyString(), anyInt(), anyString(), anyBoolean())).thenReturn( mTestNotificationChannel); when(mPreferencesHelper.getImportance(anyString(), anyInt())).thenReturn( mTestNotificationChannel.getImportance()); // But not on this channel! mTestNotificationChannel.setAllowBubbles(false); // Notif with bubble metadata Notification.BubbleMetadata data = getBasicBubbleMetadataBuilder().build(); Notification.Builder nb = new Notification.Builder(mContext, mTestNotificationChannel.getId()) .setContentTitle("foo") .setBubbleMetadata(data) .setSmallIcon(android.R.drawable.sym_def_app_icon); StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1, null, mUid, 0, nb.build(), new UserHandle(mUid), null, 0); NotificationRecord nr = new NotificationRecord(mContext, sbn, mTestNotificationChannel); // Post the notification mBinderService.enqueueNotificationWithTag(PKG, PKG, null, nr.sbn.getId(), nr.sbn.getNotification(), nr.sbn.getUserId()); waitForIdle(); // channel not allowed, no bubble assertFalse(mService.getNotificationRecord( sbn.getKey()).getNotification().isBubbleNotification()); } public void testGetAllowedAssistantCapabilities() throws Exception { Loading