diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java index b44c9d59ebe55..93e3ea44f7c72 100644 --- a/core/java/android/service/notification/NotificationListenerService.java +++ b/core/java/android/service/notification/NotificationListenerService.java @@ -1514,6 +1514,7 @@ public abstract class NotificationListenerService extends Service { private ArrayList mSmartActions; private ArrayList mSmartReplies; private boolean mCanBubble; + private boolean mVisuallyInterruptive; private static final int PARCEL_VERSION = 2; @@ -1545,6 +1546,7 @@ public abstract class NotificationListenerService extends Service { out.writeTypedList(mSmartActions, flags); out.writeCharSequenceList(mSmartReplies); out.writeBoolean(mCanBubble); + out.writeBoolean(mVisuallyInterruptive); } /** @hide */ @@ -1577,6 +1579,7 @@ public abstract class NotificationListenerService extends Service { mSmartActions = in.createTypedArrayList(Notification.Action.CREATOR); mSmartReplies = in.readCharSequenceList(); mCanBubble = in.readBoolean(); + mVisuallyInterruptive = in.readBoolean(); } @@ -1763,6 +1766,11 @@ public abstract class NotificationListenerService extends Service { return mCanBubble; } + /** @hide */ + public boolean visuallyInterruptive() { + return mVisuallyInterruptive; + } + /** @hide */ public boolean isNoisy() { return mNoisy; @@ -1779,7 +1787,8 @@ public abstract class NotificationListenerService extends Service { ArrayList snoozeCriteria, boolean showBadge, int userSentiment, boolean hidden, long lastAudiblyAlertedMs, boolean noisy, ArrayList smartActions, - ArrayList smartReplies, boolean canBubble) { + ArrayList smartReplies, boolean canBubble, + boolean visuallyInterruptive) { mKey = key; mRank = rank; mIsAmbient = importance < NotificationManager.IMPORTANCE_LOW; @@ -1800,6 +1809,7 @@ public abstract class NotificationListenerService extends Service { mSmartActions = smartActions; mSmartReplies = smartReplies; mCanBubble = canBubble; + mVisuallyInterruptive = visuallyInterruptive; } /** @@ -1824,7 +1834,8 @@ public abstract class NotificationListenerService extends Service { other.mNoisy, other.mSmartActions, other.mSmartReplies, - other.mCanBubble); + other.mCanBubble, + other.mVisuallyInterruptive); } /** diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java index eb826e54d340f..d43e030ed9ebf 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java @@ -184,6 +184,8 @@ public class BubbleData { Log.d(TAG, "notificationEntryUpdated: " + entry); } Bubble bubble = getBubbleWithKey(entry.key); + suppressFlyout = !entry.isVisuallyInterruptive || suppressFlyout; + if (bubble == null) { // Create a new bubble bubble = new Bubble(mContext, entry); @@ -193,8 +195,10 @@ public class BubbleData { } else { // Updates an existing bubble bubble.updateEntry(entry); + bubble.setSuppressFlyout(suppressFlyout); doUpdate(bubble); } + if (bubble.shouldAutoExpand()) { setSelectedBubbleInternal(bubble); if (!mExpanded) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index 027e8e426c4b0..121508b877d21 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -154,6 +154,12 @@ public final class NotificationEntry { */ public boolean canBubble; + /** + * Whether this notification has changed in visual appearance since the previous post. + * New notifications are interruptive by default. + */ + public boolean isVisuallyInterruptive; + /** * Whether this notification is shown to the user as a high priority notification: visible on * the lock screen/status bar and in the top section in the shade. @@ -196,6 +202,7 @@ public final class NotificationEntry { suppressedVisualEffects = ranking.getSuppressedVisualEffects(); suspended = ranking.isSuspended(); canBubble = ranking.canBubble(); + isVisuallyInterruptive = ranking.visuallyInterruptive(); } public void setInterruption() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java index 2ca1b0611cd60..b07ac5ff7ea84 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java @@ -182,7 +182,7 @@ public class NotificationEntryManagerTest extends SysuiTestCase { 0, NotificationManager.IMPORTANCE_DEFAULT, null, null, - null, null, null, true, sentiment, false, -1, false, null, null, false); + null, null, null, true, sentiment, false, -1, false, null, null, false, false); return true; }).when(mRankingMap).getRanking(eq(key), any(NotificationListenerService.Ranking.class)); } @@ -201,7 +201,7 @@ public class NotificationEntryManagerTest extends SysuiTestCase { null, null, null, null, null, true, NotificationListenerService.Ranking.USER_SENTIMENT_NEUTRAL, false, -1, - false, smartActions, null, false); + false, smartActions, null, false, false); return true; }).when(mRankingMap).getRanking(eq(key), any(NotificationListenerService.Ranking.class)); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationDataTest.java index e2d8e5698daf3..cf0c7185f0402 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationDataTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationDataTest.java @@ -623,6 +623,7 @@ public class NotificationDataTest extends SysuiTestCase { public static final String OVERRIDE_SMART_ACTIONS = "sa"; public static final String OVERRIDE_SMART_REPLIES = "sr"; public static final String OVERRIDE_BUBBLE = "cb"; + public static final String OVERRIDE_VISUALLY_INTERRUPTIVE = "vi"; public Map rankingOverrides = new HashMap<>(); @@ -683,7 +684,9 @@ public class NotificationDataTest extends SysuiTestCase { overrides.containsKey(OVERRIDE_SMART_REPLIES) ? overrides.getCharSequenceArrayList(OVERRIDE_SMART_REPLIES) : currentReplies, - overrides.getBoolean(OVERRIDE_BUBBLE, outRanking.canBubble())); + overrides.getBoolean(OVERRIDE_BUBBLE, outRanking.canBubble()), + overrides.getBoolean(OVERRIDE_VISUALLY_INTERRUPTIVE, + outRanking.visuallyInterruptive())); } return true; } diff --git a/services/core/java/com/android/server/notification/NotificationComparator.java b/services/core/java/com/android/server/notification/NotificationComparator.java index 9b9f4de7a18f1..bc051547a53f5 100644 --- a/services/core/java/com/android/server/notification/NotificationComparator.java +++ b/services/core/java/com/android/server/notification/NotificationComparator.java @@ -129,6 +129,12 @@ public class NotificationComparator return -1 * Integer.compare(leftPriority, rightPriority); } + final boolean leftInterruptive = left.isInterruptive(); + final boolean rightInterruptive = right.isInterruptive(); + if (leftInterruptive != rightInterruptive) { + return -1 * Boolean.compare(leftInterruptive, rightInterruptive); + } + // then break ties by time, most recent first return -1 * Long.compare(left.getRankingTimeMs(), right.getRankingTimeMs()); } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index a213d482aacda..cf16cde624c81 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -5542,7 +5542,9 @@ public class NotificationManagerService extends SystemService { notification.flags |= old.getNotification().flags & FLAG_FOREGROUND_SERVICE; r.isUpdate = true; - r.setTextChanged(isVisuallyInterruptive(old, r)); + final boolean isInterruptive = isVisuallyInterruptive(old, r); + r.setTextChanged(isInterruptive); + r.setInterruptive(isInterruptive); } mNotificationsByKey.put(n.getKey(), r); @@ -5641,7 +5643,6 @@ public class NotificationManagerService extends SystemService { Notification oldN = old.sbn.getNotification(); Notification newN = r.sbn.getNotification(); - if (oldN.extras == null || newN.extras == null) { if (DEBUG_INTERRUPTIVENESS) { Slog.v(TAG, "INTERRUPTIVENESS: " @@ -5673,6 +5674,7 @@ public class NotificationManagerService extends SystemService { } return true; } + // Do not compare Spannables (will always return false); compare unstyled Strings final String oldText = String.valueOf(oldN.extras.get(Notification.EXTRA_TEXT)); final String newText = String.valueOf(newN.extras.get(Notification.EXTRA_TEXT)); @@ -5687,6 +5689,7 @@ public class NotificationManagerService extends SystemService { } return true; } + if (oldN.hasCompletedProgress() != newN.hasCompletedProgress()) { if (DEBUG_INTERRUPTIVENESS) { Slog.v(TAG, "INTERRUPTIVENESS: " @@ -5694,6 +5697,16 @@ public class NotificationManagerService extends SystemService { } return true; } + + // Fields below are invisible to bubbles. + if (r.canBubble()) { + if (DEBUG_INTERRUPTIVENESS) { + Slog.v(TAG, "INTERRUPTIVENESS: " + + r.getKey() + " is not interruptive: bubble"); + } + return false; + } + // Actions if (Notification.areActionsVisiblyDifferent(oldN, newN)) { if (DEBUG_INTERRUPTIVENESS) { @@ -5727,7 +5740,6 @@ public class NotificationManagerService extends SystemService { } catch (Exception e) { Slog.w(TAG, "error recovering builder", e); } - return false; } @@ -5922,12 +5934,17 @@ public class NotificationManagerService extends SystemService { Slog.v(TAG, "INTERRUPTIVENESS: " + record.getKey() + " is not interruptive: summary"); } + } else if (record.canBubble()) { + if (DEBUG_INTERRUPTIVENESS) { + Slog.v(TAG, "INTERRUPTIVENESS: " + + record.getKey() + " is not interruptive: bubble"); + } } else { + record.setInterruptive(true); if (DEBUG_INTERRUPTIVENESS) { Slog.v(TAG, "INTERRUPTIVENESS: " + record.getKey() + " is interruptive: alerted"); } - record.setInterruptive(true); } MetricsLogger.action(record.getLogMaker() .setCategory(MetricsEvent.NOTIFICATION_ALERT) @@ -6286,15 +6303,21 @@ public class NotificationManagerService extends SystemService { int indexBefore = findNotificationRecordIndexLocked(record); boolean interceptBefore = record.isIntercepted(); int visibilityBefore = record.getPackageVisibilityOverride(); + boolean interruptiveBefore = record.isInterruptive(); + recon.applyChangesLocked(record); applyZenModeLocked(record); mRankingHelper.sort(mNotificationList); - int indexAfter = findNotificationRecordIndexLocked(record); - boolean interceptAfter = record.isIntercepted(); - int visibilityAfter = record.getPackageVisibilityOverride(); - changed = indexBefore != indexAfter || interceptBefore != interceptAfter - || visibilityBefore != visibilityAfter; - if (interceptBefore && !interceptAfter + boolean indexChanged = indexBefore != findNotificationRecordIndexLocked(record); + boolean interceptChanged = interceptBefore != record.isIntercepted(); + boolean visibilityChanged = visibilityBefore != record.getPackageVisibilityOverride(); + + // Broadcast isInterruptive changes for bubbles. + boolean interruptiveChanged = + record.canBubble() && (interruptiveBefore != record.isInterruptive()); + + changed = indexChanged || interceptChanged || visibilityChanged || interruptiveChanged; + if (interceptBefore && !record.isIntercepted() && record.isNewEnoughForAlerting(System.currentTimeMillis())) { buzzBeepBlinkLocked(record); } @@ -7425,7 +7448,8 @@ public class NotificationManagerService extends SystemService { record.getSound() != null || record.getVibration() != null, record.getSystemGeneratedSmartActions(), record.getSmartReplies(), - record.canBubble() + record.canBubble(), + record.isInterruptive() ); rankings.add(ranking); } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationComparatorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationComparatorTest.java index e15af3dbecc46..0b4760d89686f 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationComparatorTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationComparatorTest.java @@ -68,6 +68,7 @@ public class NotificationComparatorTest extends UiServiceTestCase { private final int uid2 = 1111111; private static final String TEST_CHANNEL_ID = "test_channel_id"; + private NotificationRecord mRecordMinCallNonInterruptive; private NotificationRecord mRecordMinCall; private NotificationRecord mRecordHighCall; private NotificationRecord mRecordDefaultMedia; @@ -105,6 +106,18 @@ public class NotificationComparatorTest extends UiServiceTestCase { smsPkg = Settings.Secure.getString(mContext.getContentResolver(), Settings.Secure.SMS_DEFAULT_APPLICATION); + Notification nonInterruptiveNotif = new Notification.Builder(mContext, TEST_CHANNEL_ID) + .setCategory(Notification.CATEGORY_CALL) + .setFlag(Notification.FLAG_FOREGROUND_SERVICE, true) + .build(); + mRecordMinCallNonInterruptive = new NotificationRecord(mContext, + new StatusBarNotification(callPkg, + callPkg, 1, "mRecordMinCallNonInterruptive", callUid, callUid, + nonInterruptiveNotif, + new UserHandle(userId), "", 2000), getDefaultChannel()); + mRecordMinCallNonInterruptive.setSystemImportance(NotificationManager.IMPORTANCE_MIN); + mRecordMinCallNonInterruptive.setInterruptive(false); + Notification n1 = new Notification.Builder(mContext, TEST_CHANNEL_ID) .setCategory(Notification.CATEGORY_CALL) .setFlag(Notification.FLAG_FOREGROUND_SERVICE, true) @@ -113,6 +126,7 @@ public class NotificationComparatorTest extends UiServiceTestCase { callPkg, 1, "minCall", callUid, callUid, n1, new UserHandle(userId), "", 2000), getDefaultChannel()); mRecordMinCall.setSystemImportance(NotificationManager.IMPORTANCE_MIN); + mRecordMinCall.setInterruptive(true); Notification n2 = new Notification.Builder(mContext, TEST_CHANNEL_ID) .setCategory(Notification.CATEGORY_CALL) @@ -245,6 +259,7 @@ public class NotificationComparatorTest extends UiServiceTestCase { expected.add(mRecordCheater); expected.add(mRecordCheaterColorized); expected.add(mRecordMinCall); + expected.add(mRecordMinCallNonInterruptive); List actual = new ArrayList<>(); actual.addAll(expected); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java index 397d2155beeb3..a9fe1a62b5587 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java @@ -51,6 +51,8 @@ import android.service.notification.NotificationRankingUpdate; import android.service.notification.SnoozeCriterion; import android.test.suitebuilder.annotation.SmallTest; +import androidx.test.runner.AndroidJUnit4; + import com.android.server.UiServiceTestCase; import org.junit.After; @@ -61,8 +63,6 @@ import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.List; -import androidx.test.runner.AndroidJUnit4; - @SmallTest @RunWith(AndroidJUnit4.class) public class NotificationListenerServiceTest extends UiServiceTestCase { @@ -116,6 +116,7 @@ public class NotificationListenerServiceTest extends UiServiceTestCase { assertActionsEqual(getSmartActions(key, i), ranking.getSmartActions()); assertEquals(getSmartReplies(key, i), ranking.getSmartReplies()); assertEquals(canBubble(i), ranking.canBubble()); + assertEquals(visuallyInterruptive(i), ranking.visuallyInterruptive()); } } @@ -182,7 +183,8 @@ public class NotificationListenerServiceTest extends UiServiceTestCase { tweak.isNoisy(), (ArrayList) tweak.getSmartActions(), (ArrayList) tweak.getSmartReplies(), - tweak.canBubble() + tweak.canBubble(), + tweak.visuallyInterruptive() ); assertNotEquals(nru, nru2); } @@ -258,7 +260,8 @@ public class NotificationListenerServiceTest extends UiServiceTestCase { getNoisy(i), getSmartActions(key, i), getSmartReplies(key, i), - canBubble(i) + canBubble(i), + visuallyInterruptive(i) ); rankings[i] = ranking; } @@ -363,6 +366,10 @@ public class NotificationListenerServiceTest extends UiServiceTestCase { return index % 4 == 0; } + private boolean visuallyInterruptive(int index) { + return index % 4 == 0; + } + private void assertActionsEqual( List expecteds, List actuals) { assertEquals(expecteds.size(), actuals.size());