Loading core/api/current.txt +1 −0 Original line number Diff line number Diff line Loading @@ -6495,6 +6495,7 @@ package android.app { field public static final int FLAG_NO_CLEAR = 32; // 0x20 field public static final int FLAG_ONGOING_EVENT = 2; // 0x2 field public static final int FLAG_ONLY_ALERT_ONCE = 8; // 0x8 field @FlaggedApi("android.app.api_rich_ongoing") public static final int FLAG_PROMOTED_ONGOING = 262144; // 0x40000 field @Deprecated public static final int FLAG_SHOW_LIGHTS = 1; // 0x1 field public static final int FOREGROUND_SERVICE_DEFAULT = 0; // 0x0 field public static final int FOREGROUND_SERVICE_DEFERRED = 2; // 0x2 core/java/android/app/INotificationManager.aidl +2 −0 Original line number Diff line number Diff line Loading @@ -258,4 +258,6 @@ interface INotificationManager @EnforcePermission(allOf={"INTERACT_ACROSS_USERS", "ACCESS_NOTIFICATIONS"}) void unregisterCallNotificationEventListener(String packageName, in UserHandle userHandle, in ICallNotificationEventCallback listener); void setCanBePromoted(String pkg, int uid, boolean promote); boolean canBePromoted(String pkg, int uid); } core/java/android/app/Notification.java +58 −0 Original line number Diff line number Diff line Loading @@ -772,6 +772,17 @@ public class Notification implements Parcelable @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_SILENT_FLAG) public static final int FLAG_SILENT = 1 << 17; //0x00020000 /** * Bit to be bitwise-ored into the {@link #flags} field that should be * set by the system if this notification is a promoted ongoing notification, either via a * user setting or allowlist. * * Applications cannot set this flag directly, but the posting app and * {@link android.service.notification.NotificationListenerService} can read it. */ @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) public static final int FLAG_PROMOTED_ONGOING = 0x00040000; private static final List<Class<? extends Style>> PLATFORM_STYLE_CLASSES = Arrays.asList( BigTextStyle.class, BigPictureStyle.class, InboxStyle.class, MediaStyle.class, DecoratedCustomViewStyle.class, DecoratedMediaCustomViewStyle.class, Loading Loading @@ -3109,6 +3120,53 @@ public class Notification implements Parcelable } } /** * @hide */ @FlaggedApi(Flags.FLAG_UI_RICH_ONGOING) public boolean containsCustomViews() { return contentView != null || bigContentView != null || headsUpContentView != null || (publicVersion != null && (publicVersion.contentView != null || publicVersion.bigContentView != null || publicVersion.headsUpContentView != null)); } /** * @hide */ @FlaggedApi(Flags.FLAG_UI_RICH_ONGOING) public boolean hasTitle() { return extras != null && (!TextUtils.isEmpty(extras.getCharSequence(EXTRA_TITLE)) || !TextUtils.isEmpty(extras.getCharSequence(EXTRA_TITLE_BIG))); } /** * @hide */ @FlaggedApi(Flags.FLAG_UI_RICH_ONGOING) public boolean hasPromotableStyle() { //TODO(b/367739672): Add progress style return extras == null || !extras.containsKey(Notification.EXTRA_TEMPLATE) || isStyle(Notification.BigPictureStyle.class) || isStyle(Notification.BigTextStyle.class) || isStyle(Notification.CallStyle.class); } /** * @hide */ @FlaggedApi(Flags.FLAG_UI_RICH_ONGOING) public boolean hasPromotableCharacteristics() { return isColorized() && hasTitle() && !containsCustomViews() && hasPromotableStyle(); } /** * Whether this notification was posted by a headless system app. * Loading core/tests/coretests/src/android/app/NotificationTest.java +278 −3 Original line number Diff line number Diff line Loading @@ -37,6 +37,7 @@ import static android.app.Notification.EXTRA_PICTURE; import static android.app.Notification.EXTRA_PICTURE_ICON; import static android.app.Notification.EXTRA_SUMMARY_TEXT; import static android.app.Notification.EXTRA_TITLE; import static android.app.Notification.FLAG_CAN_COLORIZE; import static android.app.Notification.GROUP_ALERT_CHILDREN; import static android.app.Notification.GROUP_ALERT_SUMMARY; import static android.app.Notification.GROUP_KEY_SILENT; Loading Loading @@ -96,6 +97,7 @@ import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.TextAppearanceSpan; import android.util.Pair; import android.util.Slog; import android.widget.RemoteViews; import androidx.test.InstrumentationRegistry; Loading Loading @@ -126,6 +128,8 @@ public class NotificationTest { private Context mContext; private RemoteViews mRemoteViews; @Rule public TestRule compatChangeRule = new PlatformCompatChangeRule(); @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); Loading @@ -133,23 +137,25 @@ public class NotificationTest { @Before public void setUp() { mContext = InstrumentationRegistry.getContext(); mRemoteViews = new RemoteViews( mContext.getPackageName(), R.layout.notification_template_header); } @Test public void testColorizedByPermission() { Notification n = new Notification.Builder(mContext, "test") .setFlag(Notification.FLAG_CAN_COLORIZE, true) .setFlag(FLAG_CAN_COLORIZE, true) .setColorized(true).setColor(Color.WHITE) .build(); assertTrue(n.isColorized()); n = new Notification.Builder(mContext, "test") .setFlag(Notification.FLAG_CAN_COLORIZE, true) .setFlag(FLAG_CAN_COLORIZE, true) .build(); assertFalse(n.isColorized()); n = new Notification.Builder(mContext, "test") .setFlag(Notification.FLAG_CAN_COLORIZE, false) .setFlag(FLAG_CAN_COLORIZE, false) .setColorized(true).setColor(Color.WHITE) .build(); assertFalse(n.isColorized()); Loading Loading @@ -214,6 +220,275 @@ public class NotificationTest { assertFalse(n.hasCompletedProgress()); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testHasTitle_noStyle() { Notification n = new Notification.Builder(mContext, "test") .setContentTitle("TITLE") .build(); assertThat(n.hasTitle()).isTrue(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testHasTitle_bigText() { Notification n = new Notification.Builder(mContext, "test") .setStyle(new Notification.BigTextStyle().setBigContentTitle("BIG")) .build(); assertThat(n.hasTitle()).isTrue(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testHasTitle_noTitle() { Notification n = new Notification.Builder(mContext, "test") .setContentText("text not title") .build(); assertThat(n.hasTitle()).isFalse(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testContainsCustomViews_none() { Notification np = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setContentText("test") .build(); Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setContentText("test") .setPublicVersion(np) .build(); assertThat(n.containsCustomViews()).isFalse(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testContainsCustomViews_content() { Notification np = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setContentText("test") .build(); Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setContentText("test") .setCustomContentView(mRemoteViews) .setPublicVersion(np) .build(); assertThat(n.containsCustomViews()).isTrue(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testContainsCustomViews_big() { Notification np = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setContentText("test") .build(); Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setContentText("test") .setCustomBigContentView(mRemoteViews) .setPublicVersion(np) .build(); assertThat(n.containsCustomViews()).isTrue(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testContainsCustomViews_headsUp() { Notification np = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setContentText("test") .build(); Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setContentText("test") .setCustomHeadsUpContentView(mRemoteViews) .setPublicVersion(np) .build(); assertThat(n.containsCustomViews()).isTrue(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testContainsCustomViews_content_public() { Notification np = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setContentText("public") .setCustomContentView(mRemoteViews) .build(); Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setContentText("test") .setPublicVersion(np) .build(); assertThat(n.containsCustomViews()).isTrue(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testContainsCustomViews_big_public() { Notification np = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setContentText("test") .setCustomBigContentView(mRemoteViews) .build(); Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setContentText("test") .setPublicVersion(np) .build(); assertThat(n.containsCustomViews()).isTrue(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testContainsCustomViews_headsUp_public() { Notification np = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setContentText("test") .setCustomHeadsUpContentView(mRemoteViews) .build(); Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setContentText("test") .setPublicVersion(np) .build(); assertThat(n.containsCustomViews()).isTrue(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testHasPromotableStyle_noStyle() { Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setContentText("test") .build(); assertThat(n.hasPromotableStyle()).isTrue(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testHasPromotableStyle_bigPicture() { Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setStyle(new Notification.BigPictureStyle()) .build(); assertThat(n.hasPromotableStyle()).isTrue(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testHasPromotableStyle_bigText() { Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setStyle(new Notification.BigTextStyle()) .build(); assertThat(n.hasPromotableStyle()).isTrue(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testHasPromotableStyle_no_messagingStyle() { Notification.MessagingStyle style = new Notification.MessagingStyle("self name") .setGroupConversation(true) .setConversationTitle("test conversation title"); Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setStyle(style) .build(); assertThat(n.hasPromotableStyle()).isFalse(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testHasPromotableStyle_no_mediaStyle() { Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setStyle(new Notification.MediaStyle()) .build(); assertThat(n.hasPromotableStyle()).isFalse(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testHasPromotableStyle_no_inboxStyle() { Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setStyle(new Notification.InboxStyle()) .build(); assertThat(n.hasPromotableStyle()).isFalse(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testHasPromotableStyle_callText() { PendingIntent answerIntent = createPendingIntent("answer"); PendingIntent declineIntent = createPendingIntent("decline"); Notification.CallStyle style = Notification.CallStyle.forIncomingCall( new Person.Builder().setName("A Caller").build(), declineIntent, answerIntent ); Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setStyle(style) .build(); assertThat(n.hasPromotableStyle()).isTrue(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testHasPromotableCharacteristics() { Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setStyle(new Notification.BigTextStyle().setBigContentTitle("BIG")) .setColor(Color.WHITE) .setColorized(true) .setFlag(FLAG_CAN_COLORIZE, true) .build(); assertThat(n.hasPromotableCharacteristics()).isTrue(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testHasPromotableCharacteristics_wrongStyle() { Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setStyle(new Notification.InboxStyle()) .setContentTitle("TITLE") .setColor(Color.WHITE) .setColorized(true) .setFlag(FLAG_CAN_COLORIZE, true) .build(); assertThat(n.hasPromotableCharacteristics()).isFalse(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testHasPromotableCharacteristics_notColorized() { Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setStyle(new Notification.BigTextStyle().setBigContentTitle("BIG")) .setColor(Color.WHITE) .build(); assertThat(n.hasPromotableCharacteristics()).isFalse(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testHasPromotableCharacteristics_noTitle() { Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setStyle(new Notification.BigTextStyle()) .setColor(Color.WHITE) .setColorized(true) .setFlag(FLAG_CAN_COLORIZE, true) .build(); assertThat(n.hasPromotableCharacteristics()).isFalse(); } @Test @EnableFlags(Flags.FLAG_API_RICH_ONGOING) public void testGetShortCriticalText_noneSet() { Loading services/core/java/com/android/server/notification/NotificationManagerService.java +104 −9 Original line number Diff line number Diff line Loading @@ -33,6 +33,7 @@ import static android.app.Notification.EXTRA_LARGE_ICON_BIG; import static android.app.Notification.EXTRA_SUB_TEXT; import static android.app.Notification.EXTRA_TEXT; import static android.app.Notification.EXTRA_TEXT_LINES; import static android.app.Notification.EXTRA_TITLE; import static android.app.Notification.EXTRA_TITLE_BIG; import static android.app.Notification.FLAG_AUTOGROUP_SUMMARY; import static android.app.Notification.FLAG_AUTO_CANCEL; Loading @@ -45,6 +46,7 @@ import static android.app.Notification.FLAG_NO_CLEAR; import static android.app.Notification.FLAG_NO_DISMISS; import static android.app.Notification.FLAG_ONGOING_EVENT; import static android.app.Notification.FLAG_ONLY_ALERT_ONCE; import static android.app.Notification.FLAG_PROMOTED_ONGOING; import static android.app.Notification.FLAG_USER_INITIATED_JOB; import static android.app.NotificationChannel.CONVERSATION_CHANNEL_ID_FORMAT; import static android.app.NotificationChannel.NEWS_ID; Loading Loading @@ -3516,7 +3518,7 @@ public class NotificationManagerService extends SystemService { private String getHistoryTitle(Notification n) { CharSequence title = null; if (n.extras != null) { title = n.extras.getCharSequence(Notification.EXTRA_TITLE); title = n.extras.getCharSequence(EXTRA_TITLE); if (title == null) { title = n.extras.getCharSequence(EXTRA_TITLE_BIG); } Loading Loading @@ -4113,6 +4115,75 @@ public class NotificationManagerService extends SystemService { handleSavePolicyFile(); } @Override @FlaggedApi(android.app.Flags.FLAG_UI_RICH_ONGOING) public boolean canBePromoted(String pkg, int uid) { checkCallerIsSystemOrSystemUiOrShell(); if (!android.app.Flags.uiRichOngoing()) { return false; } return mPreferencesHelper.canBePromoted(pkg, uid); } @Override @FlaggedApi(android.app.Flags.FLAG_UI_RICH_ONGOING) public void setCanBePromoted(String pkg, int uid, boolean promote) { checkCallerIsSystemOrSystemUiOrShell(); if (!android.app.Flags.uiRichOngoing()) { return; } boolean changed = mPreferencesHelper.setCanBePromoted(pkg, uid, promote); if (changed) { // check for pending/posted notifs from this app and update the flag synchronized (mNotificationLock) { // for enqueued we just need to update the flag List<NotificationRecord> enqueued = findAppNotificationByListLocked( mEnqueuedNotifications, pkg, UserHandle.getUserId(uid)); for (NotificationRecord r : enqueued) { if (promote && r.getNotification().hasPromotableCharacteristics() && r.getImportance() > IMPORTANCE_MIN) { r.getNotification().flags |= FLAG_PROMOTED_ONGOING; } else if (!promote) { r.getNotification().flags &= ~FLAG_PROMOTED_ONGOING; } } // if the notification is posted we need to update the flag and tell listeners List<NotificationRecord> posted = findAppNotificationByListLocked( mNotificationList, pkg, UserHandle.getUserId(uid)); for (NotificationRecord r : posted) { if (promote && !hasFlag(r.getNotification().flags, FLAG_PROMOTED_ONGOING) && r.getNotification().hasPromotableCharacteristics() && r.getImportance() > IMPORTANCE_MIN) { r.getNotification().flags |= FLAG_PROMOTED_ONGOING; // we could set a wake lock here but this value should only change // in response to user action, so the device should be awake long enough // to post PostNotificationTracker tracker = mPostNotificationTrackerFactory.newTracker(null); // Set false for isAppForeground because that field is only used // for bubbles and messagingstyle can not be promoted mHandler.post(new EnqueueNotificationRunnable( r.getUser().getIdentifier(), r, /* isAppForeground */ false, /* isAppProvided= */ false, tracker)); } else if (!promote && hasFlag(r.getNotification().flags, FLAG_PROMOTED_ONGOING)){ r.getNotification().flags &= ~FLAG_PROMOTED_ONGOING; PostNotificationTracker tracker = mPostNotificationTrackerFactory.newTracker(null); mHandler.post(new EnqueueNotificationRunnable( r.getUser().getIdentifier(), r, /* isAppForeground */ false, /* isAppProvided= */ false, tracker)); } } } handleSavePolicyFile(); } } @Override public boolean hasSentValidMsg(String pkg, int uid) { checkCallerIsSystem(); Loading Loading @@ -7698,6 +7769,16 @@ public class NotificationManagerService extends SystemService { return false; } if (android.app.Flags.uiRichOngoing()) { // This would normally be done in fixNotification(), but we need the channel info so // it's done a little late if (mPreferencesHelper.canBePromoted(pkg, notificationUid) && notification.hasPromotableCharacteristics() && channel.getImportance() > IMPORTANCE_MIN) { notification.flags |= FLAG_PROMOTED_ONGOING; } } final NotificationRecord r = new NotificationRecord(getContext(), n, channel); r.setIsAppImportanceLocked(mPermissionHelper.isPermissionUserSet(pkg, userId)); r.setPostSilently(postSilently); Loading Loading @@ -7938,6 +8019,9 @@ public class NotificationManagerService extends SystemService { } } // Apps cannot set this flag notification.flags &= ~FLAG_PROMOTED_ONGOING; // Ensure CallStyle has all the correct actions if (notification.isStyle(Notification.CallStyle.class)) { Notification.Builder builder = Loading Loading @@ -8061,12 +8145,7 @@ public class NotificationManagerService extends SystemService { private void checkRemoteViews(String pkg, String tag, int id, Notification notification) { if (android.app.Flags.removeRemoteViews()) { if (notification.contentView != null || notification.bigContentView != null || notification.headsUpContentView != null || (notification.publicVersion != null && (notification.publicVersion.contentView != null || notification.publicVersion.bigContentView != null || notification.publicVersion.headsUpContentView != null))) { if (notification.containsCustomViews()) { Slog.i(TAG, "Removed customViews for " + pkg); mUsageStats.registerImageRemoved(pkg); } Loading Loading @@ -9236,8 +9315,8 @@ public class NotificationManagerService extends SystemService { } } final String oldTitle = String.valueOf(oldN.extras.get(Notification.EXTRA_TITLE)); final String newTitle = String.valueOf(newN.extras.get(Notification.EXTRA_TITLE)); final String oldTitle = String.valueOf(oldN.extras.get(EXTRA_TITLE)); final String newTitle = String.valueOf(newN.extras.get(EXTRA_TITLE)); if (!Objects.equals(oldTitle, newTitle)) { if (DEBUG_INTERRUPTIVENESS) { Slog.v(TAG, "INTERRUPTIVENESS: " Loading Loading @@ -10653,6 +10732,22 @@ public class NotificationManagerService extends SystemService { } @GuardedBy("mNotificationLock") @FlaggedApi(android.app.Flags.FLAG_UI_RICH_ONGOING) private @NonNull List<NotificationRecord> findAppNotificationByListLocked( ArrayList<NotificationRecord> list, String pkg, int userId) { List<NotificationRecord> records = new ArrayList<>(); final int len = list.size(); for (int i = 0; i < len; i++) { NotificationRecord r = list.get(i); if (notificationMatchesUserId(r, userId, false) && r.getSbn().getPackageName().equals(pkg)) { records.add(r); } } return records; } @GuardedBy("mNotificationLock") private @NonNull List<NotificationRecord> findGroupNotificationByListLocked( ArrayList<NotificationRecord> list, String pkg, String groupKey, int userId) { Loading Loading
core/api/current.txt +1 −0 Original line number Diff line number Diff line Loading @@ -6495,6 +6495,7 @@ package android.app { field public static final int FLAG_NO_CLEAR = 32; // 0x20 field public static final int FLAG_ONGOING_EVENT = 2; // 0x2 field public static final int FLAG_ONLY_ALERT_ONCE = 8; // 0x8 field @FlaggedApi("android.app.api_rich_ongoing") public static final int FLAG_PROMOTED_ONGOING = 262144; // 0x40000 field @Deprecated public static final int FLAG_SHOW_LIGHTS = 1; // 0x1 field public static final int FOREGROUND_SERVICE_DEFAULT = 0; // 0x0 field public static final int FOREGROUND_SERVICE_DEFERRED = 2; // 0x2
core/java/android/app/INotificationManager.aidl +2 −0 Original line number Diff line number Diff line Loading @@ -258,4 +258,6 @@ interface INotificationManager @EnforcePermission(allOf={"INTERACT_ACROSS_USERS", "ACCESS_NOTIFICATIONS"}) void unregisterCallNotificationEventListener(String packageName, in UserHandle userHandle, in ICallNotificationEventCallback listener); void setCanBePromoted(String pkg, int uid, boolean promote); boolean canBePromoted(String pkg, int uid); }
core/java/android/app/Notification.java +58 −0 Original line number Diff line number Diff line Loading @@ -772,6 +772,17 @@ public class Notification implements Parcelable @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_SILENT_FLAG) public static final int FLAG_SILENT = 1 << 17; //0x00020000 /** * Bit to be bitwise-ored into the {@link #flags} field that should be * set by the system if this notification is a promoted ongoing notification, either via a * user setting or allowlist. * * Applications cannot set this flag directly, but the posting app and * {@link android.service.notification.NotificationListenerService} can read it. */ @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) public static final int FLAG_PROMOTED_ONGOING = 0x00040000; private static final List<Class<? extends Style>> PLATFORM_STYLE_CLASSES = Arrays.asList( BigTextStyle.class, BigPictureStyle.class, InboxStyle.class, MediaStyle.class, DecoratedCustomViewStyle.class, DecoratedMediaCustomViewStyle.class, Loading Loading @@ -3109,6 +3120,53 @@ public class Notification implements Parcelable } } /** * @hide */ @FlaggedApi(Flags.FLAG_UI_RICH_ONGOING) public boolean containsCustomViews() { return contentView != null || bigContentView != null || headsUpContentView != null || (publicVersion != null && (publicVersion.contentView != null || publicVersion.bigContentView != null || publicVersion.headsUpContentView != null)); } /** * @hide */ @FlaggedApi(Flags.FLAG_UI_RICH_ONGOING) public boolean hasTitle() { return extras != null && (!TextUtils.isEmpty(extras.getCharSequence(EXTRA_TITLE)) || !TextUtils.isEmpty(extras.getCharSequence(EXTRA_TITLE_BIG))); } /** * @hide */ @FlaggedApi(Flags.FLAG_UI_RICH_ONGOING) public boolean hasPromotableStyle() { //TODO(b/367739672): Add progress style return extras == null || !extras.containsKey(Notification.EXTRA_TEMPLATE) || isStyle(Notification.BigPictureStyle.class) || isStyle(Notification.BigTextStyle.class) || isStyle(Notification.CallStyle.class); } /** * @hide */ @FlaggedApi(Flags.FLAG_UI_RICH_ONGOING) public boolean hasPromotableCharacteristics() { return isColorized() && hasTitle() && !containsCustomViews() && hasPromotableStyle(); } /** * Whether this notification was posted by a headless system app. * Loading
core/tests/coretests/src/android/app/NotificationTest.java +278 −3 Original line number Diff line number Diff line Loading @@ -37,6 +37,7 @@ import static android.app.Notification.EXTRA_PICTURE; import static android.app.Notification.EXTRA_PICTURE_ICON; import static android.app.Notification.EXTRA_SUMMARY_TEXT; import static android.app.Notification.EXTRA_TITLE; import static android.app.Notification.FLAG_CAN_COLORIZE; import static android.app.Notification.GROUP_ALERT_CHILDREN; import static android.app.Notification.GROUP_ALERT_SUMMARY; import static android.app.Notification.GROUP_KEY_SILENT; Loading Loading @@ -96,6 +97,7 @@ import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.TextAppearanceSpan; import android.util.Pair; import android.util.Slog; import android.widget.RemoteViews; import androidx.test.InstrumentationRegistry; Loading Loading @@ -126,6 +128,8 @@ public class NotificationTest { private Context mContext; private RemoteViews mRemoteViews; @Rule public TestRule compatChangeRule = new PlatformCompatChangeRule(); @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); Loading @@ -133,23 +137,25 @@ public class NotificationTest { @Before public void setUp() { mContext = InstrumentationRegistry.getContext(); mRemoteViews = new RemoteViews( mContext.getPackageName(), R.layout.notification_template_header); } @Test public void testColorizedByPermission() { Notification n = new Notification.Builder(mContext, "test") .setFlag(Notification.FLAG_CAN_COLORIZE, true) .setFlag(FLAG_CAN_COLORIZE, true) .setColorized(true).setColor(Color.WHITE) .build(); assertTrue(n.isColorized()); n = new Notification.Builder(mContext, "test") .setFlag(Notification.FLAG_CAN_COLORIZE, true) .setFlag(FLAG_CAN_COLORIZE, true) .build(); assertFalse(n.isColorized()); n = new Notification.Builder(mContext, "test") .setFlag(Notification.FLAG_CAN_COLORIZE, false) .setFlag(FLAG_CAN_COLORIZE, false) .setColorized(true).setColor(Color.WHITE) .build(); assertFalse(n.isColorized()); Loading Loading @@ -214,6 +220,275 @@ public class NotificationTest { assertFalse(n.hasCompletedProgress()); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testHasTitle_noStyle() { Notification n = new Notification.Builder(mContext, "test") .setContentTitle("TITLE") .build(); assertThat(n.hasTitle()).isTrue(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testHasTitle_bigText() { Notification n = new Notification.Builder(mContext, "test") .setStyle(new Notification.BigTextStyle().setBigContentTitle("BIG")) .build(); assertThat(n.hasTitle()).isTrue(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testHasTitle_noTitle() { Notification n = new Notification.Builder(mContext, "test") .setContentText("text not title") .build(); assertThat(n.hasTitle()).isFalse(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testContainsCustomViews_none() { Notification np = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setContentText("test") .build(); Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setContentText("test") .setPublicVersion(np) .build(); assertThat(n.containsCustomViews()).isFalse(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testContainsCustomViews_content() { Notification np = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setContentText("test") .build(); Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setContentText("test") .setCustomContentView(mRemoteViews) .setPublicVersion(np) .build(); assertThat(n.containsCustomViews()).isTrue(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testContainsCustomViews_big() { Notification np = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setContentText("test") .build(); Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setContentText("test") .setCustomBigContentView(mRemoteViews) .setPublicVersion(np) .build(); assertThat(n.containsCustomViews()).isTrue(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testContainsCustomViews_headsUp() { Notification np = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setContentText("test") .build(); Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setContentText("test") .setCustomHeadsUpContentView(mRemoteViews) .setPublicVersion(np) .build(); assertThat(n.containsCustomViews()).isTrue(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testContainsCustomViews_content_public() { Notification np = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setContentText("public") .setCustomContentView(mRemoteViews) .build(); Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setContentText("test") .setPublicVersion(np) .build(); assertThat(n.containsCustomViews()).isTrue(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testContainsCustomViews_big_public() { Notification np = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setContentText("test") .setCustomBigContentView(mRemoteViews) .build(); Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setContentText("test") .setPublicVersion(np) .build(); assertThat(n.containsCustomViews()).isTrue(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testContainsCustomViews_headsUp_public() { Notification np = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setContentText("test") .setCustomHeadsUpContentView(mRemoteViews) .build(); Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setContentText("test") .setPublicVersion(np) .build(); assertThat(n.containsCustomViews()).isTrue(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testHasPromotableStyle_noStyle() { Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setContentText("test") .build(); assertThat(n.hasPromotableStyle()).isTrue(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testHasPromotableStyle_bigPicture() { Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setStyle(new Notification.BigPictureStyle()) .build(); assertThat(n.hasPromotableStyle()).isTrue(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testHasPromotableStyle_bigText() { Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setStyle(new Notification.BigTextStyle()) .build(); assertThat(n.hasPromotableStyle()).isTrue(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testHasPromotableStyle_no_messagingStyle() { Notification.MessagingStyle style = new Notification.MessagingStyle("self name") .setGroupConversation(true) .setConversationTitle("test conversation title"); Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setStyle(style) .build(); assertThat(n.hasPromotableStyle()).isFalse(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testHasPromotableStyle_no_mediaStyle() { Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setStyle(new Notification.MediaStyle()) .build(); assertThat(n.hasPromotableStyle()).isFalse(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testHasPromotableStyle_no_inboxStyle() { Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setStyle(new Notification.InboxStyle()) .build(); assertThat(n.hasPromotableStyle()).isFalse(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testHasPromotableStyle_callText() { PendingIntent answerIntent = createPendingIntent("answer"); PendingIntent declineIntent = createPendingIntent("decline"); Notification.CallStyle style = Notification.CallStyle.forIncomingCall( new Person.Builder().setName("A Caller").build(), declineIntent, answerIntent ); Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setStyle(style) .build(); assertThat(n.hasPromotableStyle()).isTrue(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testHasPromotableCharacteristics() { Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setStyle(new Notification.BigTextStyle().setBigContentTitle("BIG")) .setColor(Color.WHITE) .setColorized(true) .setFlag(FLAG_CAN_COLORIZE, true) .build(); assertThat(n.hasPromotableCharacteristics()).isTrue(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testHasPromotableCharacteristics_wrongStyle() { Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setStyle(new Notification.InboxStyle()) .setContentTitle("TITLE") .setColor(Color.WHITE) .setColorized(true) .setFlag(FLAG_CAN_COLORIZE, true) .build(); assertThat(n.hasPromotableCharacteristics()).isFalse(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testHasPromotableCharacteristics_notColorized() { Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setStyle(new Notification.BigTextStyle().setBigContentTitle("BIG")) .setColor(Color.WHITE) .build(); assertThat(n.hasPromotableCharacteristics()).isFalse(); } @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testHasPromotableCharacteristics_noTitle() { Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setStyle(new Notification.BigTextStyle()) .setColor(Color.WHITE) .setColorized(true) .setFlag(FLAG_CAN_COLORIZE, true) .build(); assertThat(n.hasPromotableCharacteristics()).isFalse(); } @Test @EnableFlags(Flags.FLAG_API_RICH_ONGOING) public void testGetShortCriticalText_noneSet() { Loading
services/core/java/com/android/server/notification/NotificationManagerService.java +104 −9 Original line number Diff line number Diff line Loading @@ -33,6 +33,7 @@ import static android.app.Notification.EXTRA_LARGE_ICON_BIG; import static android.app.Notification.EXTRA_SUB_TEXT; import static android.app.Notification.EXTRA_TEXT; import static android.app.Notification.EXTRA_TEXT_LINES; import static android.app.Notification.EXTRA_TITLE; import static android.app.Notification.EXTRA_TITLE_BIG; import static android.app.Notification.FLAG_AUTOGROUP_SUMMARY; import static android.app.Notification.FLAG_AUTO_CANCEL; Loading @@ -45,6 +46,7 @@ import static android.app.Notification.FLAG_NO_CLEAR; import static android.app.Notification.FLAG_NO_DISMISS; import static android.app.Notification.FLAG_ONGOING_EVENT; import static android.app.Notification.FLAG_ONLY_ALERT_ONCE; import static android.app.Notification.FLAG_PROMOTED_ONGOING; import static android.app.Notification.FLAG_USER_INITIATED_JOB; import static android.app.NotificationChannel.CONVERSATION_CHANNEL_ID_FORMAT; import static android.app.NotificationChannel.NEWS_ID; Loading Loading @@ -3516,7 +3518,7 @@ public class NotificationManagerService extends SystemService { private String getHistoryTitle(Notification n) { CharSequence title = null; if (n.extras != null) { title = n.extras.getCharSequence(Notification.EXTRA_TITLE); title = n.extras.getCharSequence(EXTRA_TITLE); if (title == null) { title = n.extras.getCharSequence(EXTRA_TITLE_BIG); } Loading Loading @@ -4113,6 +4115,75 @@ public class NotificationManagerService extends SystemService { handleSavePolicyFile(); } @Override @FlaggedApi(android.app.Flags.FLAG_UI_RICH_ONGOING) public boolean canBePromoted(String pkg, int uid) { checkCallerIsSystemOrSystemUiOrShell(); if (!android.app.Flags.uiRichOngoing()) { return false; } return mPreferencesHelper.canBePromoted(pkg, uid); } @Override @FlaggedApi(android.app.Flags.FLAG_UI_RICH_ONGOING) public void setCanBePromoted(String pkg, int uid, boolean promote) { checkCallerIsSystemOrSystemUiOrShell(); if (!android.app.Flags.uiRichOngoing()) { return; } boolean changed = mPreferencesHelper.setCanBePromoted(pkg, uid, promote); if (changed) { // check for pending/posted notifs from this app and update the flag synchronized (mNotificationLock) { // for enqueued we just need to update the flag List<NotificationRecord> enqueued = findAppNotificationByListLocked( mEnqueuedNotifications, pkg, UserHandle.getUserId(uid)); for (NotificationRecord r : enqueued) { if (promote && r.getNotification().hasPromotableCharacteristics() && r.getImportance() > IMPORTANCE_MIN) { r.getNotification().flags |= FLAG_PROMOTED_ONGOING; } else if (!promote) { r.getNotification().flags &= ~FLAG_PROMOTED_ONGOING; } } // if the notification is posted we need to update the flag and tell listeners List<NotificationRecord> posted = findAppNotificationByListLocked( mNotificationList, pkg, UserHandle.getUserId(uid)); for (NotificationRecord r : posted) { if (promote && !hasFlag(r.getNotification().flags, FLAG_PROMOTED_ONGOING) && r.getNotification().hasPromotableCharacteristics() && r.getImportance() > IMPORTANCE_MIN) { r.getNotification().flags |= FLAG_PROMOTED_ONGOING; // we could set a wake lock here but this value should only change // in response to user action, so the device should be awake long enough // to post PostNotificationTracker tracker = mPostNotificationTrackerFactory.newTracker(null); // Set false for isAppForeground because that field is only used // for bubbles and messagingstyle can not be promoted mHandler.post(new EnqueueNotificationRunnable( r.getUser().getIdentifier(), r, /* isAppForeground */ false, /* isAppProvided= */ false, tracker)); } else if (!promote && hasFlag(r.getNotification().flags, FLAG_PROMOTED_ONGOING)){ r.getNotification().flags &= ~FLAG_PROMOTED_ONGOING; PostNotificationTracker tracker = mPostNotificationTrackerFactory.newTracker(null); mHandler.post(new EnqueueNotificationRunnable( r.getUser().getIdentifier(), r, /* isAppForeground */ false, /* isAppProvided= */ false, tracker)); } } } handleSavePolicyFile(); } } @Override public boolean hasSentValidMsg(String pkg, int uid) { checkCallerIsSystem(); Loading Loading @@ -7698,6 +7769,16 @@ public class NotificationManagerService extends SystemService { return false; } if (android.app.Flags.uiRichOngoing()) { // This would normally be done in fixNotification(), but we need the channel info so // it's done a little late if (mPreferencesHelper.canBePromoted(pkg, notificationUid) && notification.hasPromotableCharacteristics() && channel.getImportance() > IMPORTANCE_MIN) { notification.flags |= FLAG_PROMOTED_ONGOING; } } final NotificationRecord r = new NotificationRecord(getContext(), n, channel); r.setIsAppImportanceLocked(mPermissionHelper.isPermissionUserSet(pkg, userId)); r.setPostSilently(postSilently); Loading Loading @@ -7938,6 +8019,9 @@ public class NotificationManagerService extends SystemService { } } // Apps cannot set this flag notification.flags &= ~FLAG_PROMOTED_ONGOING; // Ensure CallStyle has all the correct actions if (notification.isStyle(Notification.CallStyle.class)) { Notification.Builder builder = Loading Loading @@ -8061,12 +8145,7 @@ public class NotificationManagerService extends SystemService { private void checkRemoteViews(String pkg, String tag, int id, Notification notification) { if (android.app.Flags.removeRemoteViews()) { if (notification.contentView != null || notification.bigContentView != null || notification.headsUpContentView != null || (notification.publicVersion != null && (notification.publicVersion.contentView != null || notification.publicVersion.bigContentView != null || notification.publicVersion.headsUpContentView != null))) { if (notification.containsCustomViews()) { Slog.i(TAG, "Removed customViews for " + pkg); mUsageStats.registerImageRemoved(pkg); } Loading Loading @@ -9236,8 +9315,8 @@ public class NotificationManagerService extends SystemService { } } final String oldTitle = String.valueOf(oldN.extras.get(Notification.EXTRA_TITLE)); final String newTitle = String.valueOf(newN.extras.get(Notification.EXTRA_TITLE)); final String oldTitle = String.valueOf(oldN.extras.get(EXTRA_TITLE)); final String newTitle = String.valueOf(newN.extras.get(EXTRA_TITLE)); if (!Objects.equals(oldTitle, newTitle)) { if (DEBUG_INTERRUPTIVENESS) { Slog.v(TAG, "INTERRUPTIVENESS: " Loading Loading @@ -10653,6 +10732,22 @@ public class NotificationManagerService extends SystemService { } @GuardedBy("mNotificationLock") @FlaggedApi(android.app.Flags.FLAG_UI_RICH_ONGOING) private @NonNull List<NotificationRecord> findAppNotificationByListLocked( ArrayList<NotificationRecord> list, String pkg, int userId) { List<NotificationRecord> records = new ArrayList<>(); final int len = list.size(); for (int i = 0; i < len; i++) { NotificationRecord r = list.get(i); if (notificationMatchesUserId(r, userId, false) && r.getSbn().getPackageName().equals(pkg)) { records.add(r); } } return records; } @GuardedBy("mNotificationLock") private @NonNull List<NotificationRecord> findGroupNotificationByListLocked( ArrayList<NotificationRecord> list, String pkg, String groupKey, int userId) { Loading