Loading packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/AvalancheControllerTest.kt +90 −1 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ import android.os.Handler import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import android.platform.test.flag.junit.SetFlagsRule import android.testing.TestableLooper.RunWithLooper import androidx.test.filters.SmallTest import com.android.internal.logging.testing.UiEventLoggerFake Loading @@ -32,8 +33,10 @@ import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder import com.android.systemui.statusbar.notification.collection.provider.visualStabilityProvider import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManagerImpl import com.android.systemui.statusbar.notification.headsup.HeadsUpManagerTestUtil.createCallEntry import com.android.systemui.statusbar.notification.headsup.HeadsUpManagerTestUtil.createFullScreenIntentEntry import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi import com.android.systemui.statusbar.notification.shared.AvalancheReplaceHunWhenCritical import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun import com.android.systemui.statusbar.phone.keyguardBypassController import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper Loading Loading @@ -68,6 +71,7 @@ class AvalancheControllerTest(val flags: FlagsParameterization) : SysuiTestCase( } private val kosmos = testKosmos() @get:Rule val setFlagsRule = SetFlagsRule() // For creating mocks @get:Rule var rule: MockitoRule = MockitoJUnit.rule() Loading Loading @@ -138,6 +142,10 @@ class AvalancheControllerTest(val flags: FlagsParameterization) : SysuiTestCase( return testableHeadsUpManager.createHeadsUpEntry(createFullScreenIntentEntry(id, mContext)) } private fun createCallHeadsUpEntry(id: Int): HeadsUpManagerImpl.HeadsUpEntry { return testableHeadsUpManager.createHeadsUpEntry(createCallEntry(id, mContext)) } @Test fun testUpdate_isShowing_runsRunnable() { // Entry is showing Loading Loading @@ -361,6 +369,10 @@ class AvalancheControllerTest(val flags: FlagsParameterization) : SysuiTestCase( mAvalancheController.addToNext(nextEntry, runnableMock!!) // Next entry has lower priority if (AvalancheReplaceHunWhenCritical.isEnabled) { assertThat(showingEntry.getNextHunPriority(nextEntry)) .isEqualTo(NextHunPriority.LowerPriority) } assertThat(nextEntry.compareNonTimeFields(showingEntry)).isEqualTo(1) val durationMs = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000) Loading @@ -377,7 +389,11 @@ class AvalancheControllerTest(val flags: FlagsParameterization) : SysuiTestCase( val nextEntry = createHeadsUpEntry(id = 1) mAvalancheController.addToNext(nextEntry, runnableMock!!) // Same priority // Next entry has same priority if (AvalancheReplaceHunWhenCritical.isEnabled) { assertThat(showingEntry.getNextHunPriority(nextEntry)) .isEqualTo(NextHunPriority.SamePriority) } assertThat(nextEntry.compareNonTimeFields(showingEntry)).isEqualTo(0) val durationMs = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000) Loading @@ -395,6 +411,10 @@ class AvalancheControllerTest(val flags: FlagsParameterization) : SysuiTestCase( mAvalancheController.addToNext(nextEntry, runnableMock!!) // Next entry has higher priority if (AvalancheReplaceHunWhenCritical.isEnabled) { assertThat(showingEntry.getNextHunPriority(nextEntry)) .isEqualTo(NextHunPriority.HigherPriority) } assertThat(nextEntry.compareNonTimeFields(showingEntry)).isEqualTo(-1) val durationMs = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000) Loading Loading @@ -437,6 +457,75 @@ class AvalancheControllerTest(val flags: FlagsParameterization) : SysuiTestCase( assertThat(duration).isEqualTo(RemainingDuration.HideImmediately) } @Test @EnableFlags(AvalancheReplaceHunWhenCritical.FLAG_NAME) fun testGetDuration_currentIsFsi_nextEntryIsCriticalCall_flagOn_hideImmediately() { // FSI HUN Entry is showing val showingEntry = createFsiHeadsUpEntry(id = 0) mAvalancheController.headsUpEntryShowing = showingEntry // There's another entry waiting to show next and it's incoming call val nextEntry = createCallHeadsUpEntry(id = 1) // nextEntry.requestedPinnedStatus = PinnedStatus.PinnedByUser mAvalancheController.addToNext(nextEntry, runnableMock!!) // Then: should hide immediately val duration = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000) assertThat(duration).isEqualTo(RemainingDuration.HideImmediately) } @Test @EnableFlags(AvalancheReplaceHunWhenCritical.FLAG_NAME) fun testGetDuration_currentIsFsi_nextEntryIsFsi_flagOn_hideImmediately() { // FSI HUN Entry is showing val showingEntry = createFsiHeadsUpEntry(id = 0) mAvalancheController.headsUpEntryShowing = showingEntry // There's another entry waiting to show next and it's FSI val nextEntry = createFsiHeadsUpEntry(id = 1) mAvalancheController.addToNext(nextEntry, runnableMock!!) // Then: should hide immediately val duration = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000) assertThat(duration).isEqualTo(RemainingDuration.HideImmediately) } @Test @EnableFlags(AvalancheReplaceHunWhenCritical.FLAG_NAME) fun testGetDuration_currentIsCall_nextEntryIsFsi_flagOn_hideImmediately() { // Call HUN Entry is showing val showingEntry = createCallHeadsUpEntry(id = 0) mAvalancheController.headsUpEntryShowing = showingEntry // There's another entry waiting to show next and it's FSI val nextEntry = createFsiHeadsUpEntry(id = 1) mAvalancheController.addToNext(nextEntry, runnableMock!!) // Then: should hide immediately val duration = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000) assertThat(duration).isEqualTo(RemainingDuration.HideImmediately) } @Test @EnableFlags(AvalancheReplaceHunWhenCritical.FLAG_NAME) fun testGetDuration_currentIsCall_nextEntryIsCall_flagOn_hideImmediately() { // Call HUN Entry is showing val showingEntry = createCallHeadsUpEntry(id = 0) mAvalancheController.headsUpEntryShowing = showingEntry // There's another entry waiting to show next and it's Call val nextEntry = createCallHeadsUpEntry(id = 1) mAvalancheController.addToNext(nextEntry, runnableMock!!) // Then: should hide immediately val duration = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000) assertThat(duration).isEqualTo(RemainingDuration.HideImmediately) } companion object { @JvmStatic @Parameters(name = "{0}") Loading packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerTestUtil.java +21 −0 Original line number Diff line number Diff line Loading @@ -18,8 +18,10 @@ package com.android.systemui.statusbar.notification.headsup; import android.app.ActivityManager; import android.app.Notification; import android.app.PendingIntent; import android.app.Person; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.UserHandle; import android.service.notification.StatusBarNotification; Loading Loading @@ -80,4 +82,23 @@ public class HeadsUpManagerTestUtil { .build(); return HeadsUpManagerTestUtil.createEntry(id, notif); } protected static NotificationEntry createCallEntry(int id, Context context) { final Person person = new Person.Builder() .setName("name") .setKey("abc") .setUri(Uri.parse("fake_uri").toString()) .setBot(false) .build(); final PendingIntent intent = PendingIntent.getActivity( context, 0, new Intent(), PendingIntent.FLAG_IMMUTABLE); final Notification notif = new Notification.Builder(context, "") .setSmallIcon(com.android.systemui.res.R.drawable.ic_person) .setStyle(Notification.CallStyle.forIncomingCall(person, intent, intent)) .build(); return HeadsUpManagerTestUtil.createEntry(id, notif); } } packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/AvalancheController.kt +148 −24 Original line number Diff line number Diff line Loading @@ -15,6 +15,7 @@ */ package com.android.systemui.statusbar.notification.headsup import android.app.Notification import android.os.Handler import androidx.annotation.VisibleForTesting import com.android.internal.logging.UiEvent Loading @@ -25,6 +26,7 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dump.DumpManager import com.android.systemui.statusbar.notification.headsup.HeadsUpManagerImpl.HeadsUpEntry import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi import com.android.systemui.statusbar.notification.shared.AvalancheReplaceHunWhenCritical import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun import java.io.PrintWriter import javax.inject.Inject Loading Loading @@ -154,7 +156,11 @@ constructor( } else if (entry in nextMap) { outcome = "update next" nextMap[entry]?.add(runnable) if (AvalancheReplaceHunWhenCritical.isEnabled) { checkReplaceCurrentHun(entry)?.let { outcome = "$outcome & $it" } } else { checkNextPinnedByUser(entry)?.let { outcome = "$outcome & $it" } } } else if (headsUpEntryShowing == null) { outcome = "show now" showNow(entry, arrayListOf(runnable)) Loading @@ -166,9 +172,14 @@ constructor( outcome = "add next" addToNext(entry, runnable) val nextIsPinnedByUserResult = checkNextPinnedByUser(entry) if (nextIsPinnedByUserResult != null) { outcome = "$outcome & $nextIsPinnedByUserResult" val nextReplacementResult = if (AvalancheReplaceHunWhenCritical.isEnabled) { checkReplaceCurrentHun(entry) } else { checkNextPinnedByUser(entry) } if (nextReplacementResult != null) { outcome = "$outcome & $nextReplacementResult" } else { // Shorten headsUpEntryShowing display time val nextIndex = nextList.indexOf(entry) Loading @@ -179,6 +190,7 @@ constructor( headsUpEntryShowing!!.updateEntry( /* updatePostTime= */ false, /* updateEarliestRemovalTime= */ false, /* ignoreSticky= */ false, /* reason= */ "shorten duration of previously-last HUN", ) } Loading @@ -192,6 +204,7 @@ constructor( fun addToNext(entry: HeadsUpEntry, runnable: Runnable) { nextMap[entry] = arrayListOf(runnable) nextList.add(entry) headsUpManagerLogger.logAddToNext(entry.mEntry) } /** Loading @@ -201,6 +214,7 @@ constructor( * @return a string representing the outcome, or null if nothing changed. */ private fun checkNextPinnedByUser(entry: HeadsUpEntry): String? { AvalancheReplaceHunWhenCritical.assertInLegacyMode() if ( PromotedNotificationUi.isEnabled && entry.requestedPinnedStatus == PinnedStatus.PinnedByUser Loading @@ -209,6 +223,7 @@ constructor( headsUpEntryShowing?.updateEntry( /* updatePostTime= */ false, /* updateEarliestRemovalTime= */ false, /* ignoreSticky= */ false, /* reason= */ string, ) return string Loading @@ -216,6 +231,63 @@ constructor( return null } /** * Checks if the given entry should replace the showing HUN immediately. * * @return a string representing the reason for replacement, or null if should not replace. */ private fun checkReplaceCurrentHun(entry: HeadsUpEntry): String? { if (AvalancheReplaceHunWhenCritical.isUnexpectedlyInLegacyMode()) return null var result: String? = null var ignoreSticky = false if (entry.hasFullScreenIntent()) { result = "next has FSI" ignoreSticky = true } if (entry.isCriticalCall()) { result = "$result next is critical call" ignoreSticky = true } if ( PromotedNotificationUi.isEnabled && entry.requestedPinnedStatus == PinnedStatus.PinnedByUser ) { result = "$result next is PinnedByUser" ignoreSticky = true } if (result != null) { headsUpEntryShowing?.updateEntry( /* updatePostTime= */ false, /* updateEarliestRemovalTime= */ false, /* ignoreSticky= */ ignoreSticky, /* reason= */ result, ) } return result } private fun HeadsUpEntry.hasFullScreenIntent(): Boolean { return this.mEntry?.sbn?.notification?.fullScreenIntent != null } /** * Determines if the notification is for a critical call that must display on top of an active * input notification. The call isOngoing check is for a special case of incoming calls where * the call is not yet answered (see b/164291424). */ private fun HeadsUpEntry.isCriticalCall(): Boolean { val notificationEntry = this.mEntry ?: return false val notification = notificationEntry.sbn.notification val isIncomingCall = notification.isStyle(Notification.CallStyle::class.java) && notification.extras.getInt(Notification.EXTRA_CALL_TYPE) == Notification.CallStyle.CALL_TYPE_INCOMING return isIncomingCall || (notificationEntry.sbn.isOngoing && Notification.CATEGORY_CALL == notification.category) } /** * Run or ignore Runnable for given HeadsUpEntry. If entry was never shown, ignore and delete * all Runnables associated with that entry. Loading Loading @@ -352,6 +424,57 @@ constructor( ) } } if (AvalancheReplaceHunWhenCritical.isEnabled) { return when (val nextPriority = entry.getNextHunPriority(nextEntry)) { // When next is critical, replace immediately is NextHunPriority.ReplaceImmediately -> { RemainingDuration.HideImmediately.also { headsUpManagerLogger.logAvalancheDuration( thisKey, duration = it, nextPriority.message, nextKey, ) } } // When next has higher priority, wait 500ms before replacement is NextHunPriority.HigherPriority -> { RemainingDuration.UpdatedDuration(500).also { headsUpManagerLogger.logAvalancheDuration( thisKey, duration = it, "LOWER priority than next: ", nextKey, ) } } // When next has same priority, wait 1000ms before replacement is NextHunPriority.SamePriority -> { RemainingDuration.UpdatedDuration(1000).also { headsUpManagerLogger.logAvalancheDuration( thisKey, duration = it, "SAME priority as next: ", nextKey, ) } } // When next has lower priority, wait until auto dismiss is NextHunPriority.LowerPriority -> { headsUpManagerLogger.logAvalancheDuration( thisKey, autoDismissMs, "HIGHER priority than next: ", nextKey, ) return autoDismissMs } } } else { if (nextEntry.compareNonTimeFields(entry) == -1) { return RemainingDuration.UpdatedDuration(500).also { headsUpManagerLogger.logAvalancheDuration( Loading Loading @@ -380,6 +503,7 @@ constructor( return autoDismissMs } } } /** Return true if entry is waiting to show. */ fun isWaiting(key: String): Boolean { Loading packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java +67 −8 Original line number Diff line number Diff line Loading @@ -55,6 +55,7 @@ import com.android.systemui.statusbar.notification.data.repository.HeadsUpReposi import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.shared.AvalancheReplaceHunWhenCritical; import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun; import com.android.systemui.statusbar.phone.ExpandHeadsUpOnInlineReply; Loading Loading @@ -1189,7 +1190,8 @@ public class HeadsUpManagerImpl /** * Determines if the notification is for a critical call that must display on top of an active * input notification. * The call isOngoing check is for a special case of incoming calls (see b/164291424). * The call isOngoing check is for a special case of incoming calls where the call is not yet * answered(see b/164291424). */ private static boolean isCriticalCallNotif(NotificationEntry entry) { Notification n = entry.getSbn().getNotification(); Loading Loading @@ -1392,15 +1394,27 @@ public class HeadsUpManagerImpl * @param updatePostTime whether or not to refresh the post time */ public void updateEntry(boolean updatePostTime, @Nullable String reason) { updateEntry(updatePostTime, /* updateEarliestRemovalTime= */ true, reason); updateEntry( updatePostTime, /* updateEarliestRemovalTime= */ true, /* ignoreSticky= */ false, reason ); } /** * Updates an entry's removal time. * * @param updatePostTime whether or not to refresh the post time * @param updateEarliestRemovalTime whether this update should further delay removal * @param ignoreSticky whether or not to ignore sticky and avoid canceling the * removal callback for an entry * @param reason reason for the update */ public void updateEntry(boolean updatePostTime, boolean updateEarliestRemovalTime, public void updateEntry( boolean updatePostTime, boolean updateEarliestRemovalTime, boolean ignoreSticky, @Nullable String reason) { Runnable runnable = () -> { if (mEntry == null) { Loading @@ -1426,7 +1440,7 @@ public class HeadsUpManagerImpl mAvalancheController.update(this, runnable, "updateEntry reason:" + reason + " updatePostTime:" + updatePostTime); if (isSticky()) { if (!ignoreSticky && isSticky()) { cancelAutoRemovalCallbacks("updateEntry (sticky)"); return; } Loading @@ -1436,6 +1450,11 @@ public class HeadsUpManagerImpl mAvalancheController.getDuration(this, mAutoDismissTime); if (remainingDuration instanceof RemainingDuration.HideImmediately) { if (AvalancheReplaceHunWhenCritical.isEnabled()) { // Should not throw exception if either AvalancheReplaceHunWhenCritical or // PromotedNotificationUi flag is enabled return 0; } /* Check if */ PromotedNotificationUi.isUnexpectedlyInLegacyMode(); return 0; } Loading Loading @@ -1550,6 +1569,45 @@ public class HeadsUpManagerImpl return 0; } /** * Determines the priority of the next HUN, comparing to the current HUN */ @NonNull public NextHunPriority getNextHunPriority(HeadsUpEntry nextHeadsUpEntry) { NotificationEntry nextEntry = nextHeadsUpEntry.mEntry; if (mEntry == null && nextEntry == null) { return NextHunPriority.SamePriority.INSTANCE; } else if (nextEntry == null) { return NextHunPriority.LowerPriority.INSTANCE; } else if (mEntry == null) { return NextHunPriority.HigherPriority.INSTANCE; } boolean currentCritical = hasFullScreenIntent(mEntry) || isCriticalCallNotif(mEntry); boolean nextHasFsi = hasFullScreenIntent(nextEntry); boolean nextIsCriticalCall = isCriticalCallNotif(nextEntry); if (nextHasFsi || nextIsCriticalCall) { // If next is critical, replace immediately regardless of current StringBuilder messageBuilder = new StringBuilder("Next is critical for:"); if (nextIsCriticalCall) messageBuilder.append(" critical call"); if (nextHasFsi) messageBuilder.append(" has FSI"); return new NextHunPriority.ReplaceImmediately(messageBuilder.toString()); } if (currentCritical) { return NextHunPriority.LowerPriority.INSTANCE; } if (mRemoteInputActive && !nextHeadsUpEntry.mRemoteInputActive) { return NextHunPriority.LowerPriority.INSTANCE; } else if (!mRemoteInputActive && nextHeadsUpEntry.mRemoteInputActive) { return NextHunPriority.HigherPriority.INSTANCE; } return NextHunPriority.SamePriority.INSTANCE; } public int compareTo(@NonNull HeadsUpEntry headsUpEntry) { if (mEntry == null && headsUpEntry.mEntry == null) { return 0; Loading Loading @@ -1637,7 +1695,7 @@ public class HeadsUpManagerImpl * Clear any pending removal runnables. */ public void cancelAutoRemovalCallbacks(@Nullable String reason) { Runnable runnable = () -> { Runnable cancellationRunnable = () -> { final boolean removed = cancelAutoRemovalCallbackInternal(); if (removed) { Loading @@ -1646,10 +1704,11 @@ public class HeadsUpManagerImpl }; if (mEntry != null && isHeadsUpEntry(mEntry.getKey())) { mLogger.logAutoRemoveCancelRequest(this.mEntry, reason); mAvalancheController.update(this, runnable, reason + " cancelAutoRemovalCallbacks"); mAvalancheController.update(this, cancellationRunnable, reason + " cancelAutoRemovalCallbacks"); } else { // Just removed runnable.run(); cancellationRunnable.run(); } } Loading packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerLogger.kt +4 −0 Original line number Diff line number Diff line Loading @@ -204,6 +204,10 @@ constructor(@NotificationHeadsUpLog private val buffer: LogBuffer) { ) } fun logAddToNext(entry: NotificationEntry?) { buffer.log(TAG, INFO, { str1 = entry?.logKey }, { "Add to next: $str1" }) } fun logRemoveEntryRequest(key: String, reason: String, isWaiting: Boolean) { buffer.log( TAG, Loading Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/AvalancheControllerTest.kt +90 −1 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ import android.os.Handler import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import android.platform.test.flag.junit.SetFlagsRule import android.testing.TestableLooper.RunWithLooper import androidx.test.filters.SmallTest import com.android.internal.logging.testing.UiEventLoggerFake Loading @@ -32,8 +33,10 @@ import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder import com.android.systemui.statusbar.notification.collection.provider.visualStabilityProvider import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManagerImpl import com.android.systemui.statusbar.notification.headsup.HeadsUpManagerTestUtil.createCallEntry import com.android.systemui.statusbar.notification.headsup.HeadsUpManagerTestUtil.createFullScreenIntentEntry import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi import com.android.systemui.statusbar.notification.shared.AvalancheReplaceHunWhenCritical import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun import com.android.systemui.statusbar.phone.keyguardBypassController import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper Loading Loading @@ -68,6 +71,7 @@ class AvalancheControllerTest(val flags: FlagsParameterization) : SysuiTestCase( } private val kosmos = testKosmos() @get:Rule val setFlagsRule = SetFlagsRule() // For creating mocks @get:Rule var rule: MockitoRule = MockitoJUnit.rule() Loading Loading @@ -138,6 +142,10 @@ class AvalancheControllerTest(val flags: FlagsParameterization) : SysuiTestCase( return testableHeadsUpManager.createHeadsUpEntry(createFullScreenIntentEntry(id, mContext)) } private fun createCallHeadsUpEntry(id: Int): HeadsUpManagerImpl.HeadsUpEntry { return testableHeadsUpManager.createHeadsUpEntry(createCallEntry(id, mContext)) } @Test fun testUpdate_isShowing_runsRunnable() { // Entry is showing Loading Loading @@ -361,6 +369,10 @@ class AvalancheControllerTest(val flags: FlagsParameterization) : SysuiTestCase( mAvalancheController.addToNext(nextEntry, runnableMock!!) // Next entry has lower priority if (AvalancheReplaceHunWhenCritical.isEnabled) { assertThat(showingEntry.getNextHunPriority(nextEntry)) .isEqualTo(NextHunPriority.LowerPriority) } assertThat(nextEntry.compareNonTimeFields(showingEntry)).isEqualTo(1) val durationMs = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000) Loading @@ -377,7 +389,11 @@ class AvalancheControllerTest(val flags: FlagsParameterization) : SysuiTestCase( val nextEntry = createHeadsUpEntry(id = 1) mAvalancheController.addToNext(nextEntry, runnableMock!!) // Same priority // Next entry has same priority if (AvalancheReplaceHunWhenCritical.isEnabled) { assertThat(showingEntry.getNextHunPriority(nextEntry)) .isEqualTo(NextHunPriority.SamePriority) } assertThat(nextEntry.compareNonTimeFields(showingEntry)).isEqualTo(0) val durationMs = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000) Loading @@ -395,6 +411,10 @@ class AvalancheControllerTest(val flags: FlagsParameterization) : SysuiTestCase( mAvalancheController.addToNext(nextEntry, runnableMock!!) // Next entry has higher priority if (AvalancheReplaceHunWhenCritical.isEnabled) { assertThat(showingEntry.getNextHunPriority(nextEntry)) .isEqualTo(NextHunPriority.HigherPriority) } assertThat(nextEntry.compareNonTimeFields(showingEntry)).isEqualTo(-1) val durationMs = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000) Loading Loading @@ -437,6 +457,75 @@ class AvalancheControllerTest(val flags: FlagsParameterization) : SysuiTestCase( assertThat(duration).isEqualTo(RemainingDuration.HideImmediately) } @Test @EnableFlags(AvalancheReplaceHunWhenCritical.FLAG_NAME) fun testGetDuration_currentIsFsi_nextEntryIsCriticalCall_flagOn_hideImmediately() { // FSI HUN Entry is showing val showingEntry = createFsiHeadsUpEntry(id = 0) mAvalancheController.headsUpEntryShowing = showingEntry // There's another entry waiting to show next and it's incoming call val nextEntry = createCallHeadsUpEntry(id = 1) // nextEntry.requestedPinnedStatus = PinnedStatus.PinnedByUser mAvalancheController.addToNext(nextEntry, runnableMock!!) // Then: should hide immediately val duration = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000) assertThat(duration).isEqualTo(RemainingDuration.HideImmediately) } @Test @EnableFlags(AvalancheReplaceHunWhenCritical.FLAG_NAME) fun testGetDuration_currentIsFsi_nextEntryIsFsi_flagOn_hideImmediately() { // FSI HUN Entry is showing val showingEntry = createFsiHeadsUpEntry(id = 0) mAvalancheController.headsUpEntryShowing = showingEntry // There's another entry waiting to show next and it's FSI val nextEntry = createFsiHeadsUpEntry(id = 1) mAvalancheController.addToNext(nextEntry, runnableMock!!) // Then: should hide immediately val duration = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000) assertThat(duration).isEqualTo(RemainingDuration.HideImmediately) } @Test @EnableFlags(AvalancheReplaceHunWhenCritical.FLAG_NAME) fun testGetDuration_currentIsCall_nextEntryIsFsi_flagOn_hideImmediately() { // Call HUN Entry is showing val showingEntry = createCallHeadsUpEntry(id = 0) mAvalancheController.headsUpEntryShowing = showingEntry // There's another entry waiting to show next and it's FSI val nextEntry = createFsiHeadsUpEntry(id = 1) mAvalancheController.addToNext(nextEntry, runnableMock!!) // Then: should hide immediately val duration = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000) assertThat(duration).isEqualTo(RemainingDuration.HideImmediately) } @Test @EnableFlags(AvalancheReplaceHunWhenCritical.FLAG_NAME) fun testGetDuration_currentIsCall_nextEntryIsCall_flagOn_hideImmediately() { // Call HUN Entry is showing val showingEntry = createCallHeadsUpEntry(id = 0) mAvalancheController.headsUpEntryShowing = showingEntry // There's another entry waiting to show next and it's Call val nextEntry = createCallHeadsUpEntry(id = 1) mAvalancheController.addToNext(nextEntry, runnableMock!!) // Then: should hide immediately val duration = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000) assertThat(duration).isEqualTo(RemainingDuration.HideImmediately) } companion object { @JvmStatic @Parameters(name = "{0}") Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerTestUtil.java +21 −0 Original line number Diff line number Diff line Loading @@ -18,8 +18,10 @@ package com.android.systemui.statusbar.notification.headsup; import android.app.ActivityManager; import android.app.Notification; import android.app.PendingIntent; import android.app.Person; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.UserHandle; import android.service.notification.StatusBarNotification; Loading Loading @@ -80,4 +82,23 @@ public class HeadsUpManagerTestUtil { .build(); return HeadsUpManagerTestUtil.createEntry(id, notif); } protected static NotificationEntry createCallEntry(int id, Context context) { final Person person = new Person.Builder() .setName("name") .setKey("abc") .setUri(Uri.parse("fake_uri").toString()) .setBot(false) .build(); final PendingIntent intent = PendingIntent.getActivity( context, 0, new Intent(), PendingIntent.FLAG_IMMUTABLE); final Notification notif = new Notification.Builder(context, "") .setSmallIcon(com.android.systemui.res.R.drawable.ic_person) .setStyle(Notification.CallStyle.forIncomingCall(person, intent, intent)) .build(); return HeadsUpManagerTestUtil.createEntry(id, notif); } }
packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/AvalancheController.kt +148 −24 Original line number Diff line number Diff line Loading @@ -15,6 +15,7 @@ */ package com.android.systemui.statusbar.notification.headsup import android.app.Notification import android.os.Handler import androidx.annotation.VisibleForTesting import com.android.internal.logging.UiEvent Loading @@ -25,6 +26,7 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dump.DumpManager import com.android.systemui.statusbar.notification.headsup.HeadsUpManagerImpl.HeadsUpEntry import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi import com.android.systemui.statusbar.notification.shared.AvalancheReplaceHunWhenCritical import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun import java.io.PrintWriter import javax.inject.Inject Loading Loading @@ -154,7 +156,11 @@ constructor( } else if (entry in nextMap) { outcome = "update next" nextMap[entry]?.add(runnable) if (AvalancheReplaceHunWhenCritical.isEnabled) { checkReplaceCurrentHun(entry)?.let { outcome = "$outcome & $it" } } else { checkNextPinnedByUser(entry)?.let { outcome = "$outcome & $it" } } } else if (headsUpEntryShowing == null) { outcome = "show now" showNow(entry, arrayListOf(runnable)) Loading @@ -166,9 +172,14 @@ constructor( outcome = "add next" addToNext(entry, runnable) val nextIsPinnedByUserResult = checkNextPinnedByUser(entry) if (nextIsPinnedByUserResult != null) { outcome = "$outcome & $nextIsPinnedByUserResult" val nextReplacementResult = if (AvalancheReplaceHunWhenCritical.isEnabled) { checkReplaceCurrentHun(entry) } else { checkNextPinnedByUser(entry) } if (nextReplacementResult != null) { outcome = "$outcome & $nextReplacementResult" } else { // Shorten headsUpEntryShowing display time val nextIndex = nextList.indexOf(entry) Loading @@ -179,6 +190,7 @@ constructor( headsUpEntryShowing!!.updateEntry( /* updatePostTime= */ false, /* updateEarliestRemovalTime= */ false, /* ignoreSticky= */ false, /* reason= */ "shorten duration of previously-last HUN", ) } Loading @@ -192,6 +204,7 @@ constructor( fun addToNext(entry: HeadsUpEntry, runnable: Runnable) { nextMap[entry] = arrayListOf(runnable) nextList.add(entry) headsUpManagerLogger.logAddToNext(entry.mEntry) } /** Loading @@ -201,6 +214,7 @@ constructor( * @return a string representing the outcome, or null if nothing changed. */ private fun checkNextPinnedByUser(entry: HeadsUpEntry): String? { AvalancheReplaceHunWhenCritical.assertInLegacyMode() if ( PromotedNotificationUi.isEnabled && entry.requestedPinnedStatus == PinnedStatus.PinnedByUser Loading @@ -209,6 +223,7 @@ constructor( headsUpEntryShowing?.updateEntry( /* updatePostTime= */ false, /* updateEarliestRemovalTime= */ false, /* ignoreSticky= */ false, /* reason= */ string, ) return string Loading @@ -216,6 +231,63 @@ constructor( return null } /** * Checks if the given entry should replace the showing HUN immediately. * * @return a string representing the reason for replacement, or null if should not replace. */ private fun checkReplaceCurrentHun(entry: HeadsUpEntry): String? { if (AvalancheReplaceHunWhenCritical.isUnexpectedlyInLegacyMode()) return null var result: String? = null var ignoreSticky = false if (entry.hasFullScreenIntent()) { result = "next has FSI" ignoreSticky = true } if (entry.isCriticalCall()) { result = "$result next is critical call" ignoreSticky = true } if ( PromotedNotificationUi.isEnabled && entry.requestedPinnedStatus == PinnedStatus.PinnedByUser ) { result = "$result next is PinnedByUser" ignoreSticky = true } if (result != null) { headsUpEntryShowing?.updateEntry( /* updatePostTime= */ false, /* updateEarliestRemovalTime= */ false, /* ignoreSticky= */ ignoreSticky, /* reason= */ result, ) } return result } private fun HeadsUpEntry.hasFullScreenIntent(): Boolean { return this.mEntry?.sbn?.notification?.fullScreenIntent != null } /** * Determines if the notification is for a critical call that must display on top of an active * input notification. The call isOngoing check is for a special case of incoming calls where * the call is not yet answered (see b/164291424). */ private fun HeadsUpEntry.isCriticalCall(): Boolean { val notificationEntry = this.mEntry ?: return false val notification = notificationEntry.sbn.notification val isIncomingCall = notification.isStyle(Notification.CallStyle::class.java) && notification.extras.getInt(Notification.EXTRA_CALL_TYPE) == Notification.CallStyle.CALL_TYPE_INCOMING return isIncomingCall || (notificationEntry.sbn.isOngoing && Notification.CATEGORY_CALL == notification.category) } /** * Run or ignore Runnable for given HeadsUpEntry. If entry was never shown, ignore and delete * all Runnables associated with that entry. Loading Loading @@ -352,6 +424,57 @@ constructor( ) } } if (AvalancheReplaceHunWhenCritical.isEnabled) { return when (val nextPriority = entry.getNextHunPriority(nextEntry)) { // When next is critical, replace immediately is NextHunPriority.ReplaceImmediately -> { RemainingDuration.HideImmediately.also { headsUpManagerLogger.logAvalancheDuration( thisKey, duration = it, nextPriority.message, nextKey, ) } } // When next has higher priority, wait 500ms before replacement is NextHunPriority.HigherPriority -> { RemainingDuration.UpdatedDuration(500).also { headsUpManagerLogger.logAvalancheDuration( thisKey, duration = it, "LOWER priority than next: ", nextKey, ) } } // When next has same priority, wait 1000ms before replacement is NextHunPriority.SamePriority -> { RemainingDuration.UpdatedDuration(1000).also { headsUpManagerLogger.logAvalancheDuration( thisKey, duration = it, "SAME priority as next: ", nextKey, ) } } // When next has lower priority, wait until auto dismiss is NextHunPriority.LowerPriority -> { headsUpManagerLogger.logAvalancheDuration( thisKey, autoDismissMs, "HIGHER priority than next: ", nextKey, ) return autoDismissMs } } } else { if (nextEntry.compareNonTimeFields(entry) == -1) { return RemainingDuration.UpdatedDuration(500).also { headsUpManagerLogger.logAvalancheDuration( Loading Loading @@ -380,6 +503,7 @@ constructor( return autoDismissMs } } } /** Return true if entry is waiting to show. */ fun isWaiting(key: String): Boolean { Loading
packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java +67 −8 Original line number Diff line number Diff line Loading @@ -55,6 +55,7 @@ import com.android.systemui.statusbar.notification.data.repository.HeadsUpReposi import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.shared.AvalancheReplaceHunWhenCritical; import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun; import com.android.systemui.statusbar.phone.ExpandHeadsUpOnInlineReply; Loading Loading @@ -1189,7 +1190,8 @@ public class HeadsUpManagerImpl /** * Determines if the notification is for a critical call that must display on top of an active * input notification. * The call isOngoing check is for a special case of incoming calls (see b/164291424). * The call isOngoing check is for a special case of incoming calls where the call is not yet * answered(see b/164291424). */ private static boolean isCriticalCallNotif(NotificationEntry entry) { Notification n = entry.getSbn().getNotification(); Loading Loading @@ -1392,15 +1394,27 @@ public class HeadsUpManagerImpl * @param updatePostTime whether or not to refresh the post time */ public void updateEntry(boolean updatePostTime, @Nullable String reason) { updateEntry(updatePostTime, /* updateEarliestRemovalTime= */ true, reason); updateEntry( updatePostTime, /* updateEarliestRemovalTime= */ true, /* ignoreSticky= */ false, reason ); } /** * Updates an entry's removal time. * * @param updatePostTime whether or not to refresh the post time * @param updateEarliestRemovalTime whether this update should further delay removal * @param ignoreSticky whether or not to ignore sticky and avoid canceling the * removal callback for an entry * @param reason reason for the update */ public void updateEntry(boolean updatePostTime, boolean updateEarliestRemovalTime, public void updateEntry( boolean updatePostTime, boolean updateEarliestRemovalTime, boolean ignoreSticky, @Nullable String reason) { Runnable runnable = () -> { if (mEntry == null) { Loading @@ -1426,7 +1440,7 @@ public class HeadsUpManagerImpl mAvalancheController.update(this, runnable, "updateEntry reason:" + reason + " updatePostTime:" + updatePostTime); if (isSticky()) { if (!ignoreSticky && isSticky()) { cancelAutoRemovalCallbacks("updateEntry (sticky)"); return; } Loading @@ -1436,6 +1450,11 @@ public class HeadsUpManagerImpl mAvalancheController.getDuration(this, mAutoDismissTime); if (remainingDuration instanceof RemainingDuration.HideImmediately) { if (AvalancheReplaceHunWhenCritical.isEnabled()) { // Should not throw exception if either AvalancheReplaceHunWhenCritical or // PromotedNotificationUi flag is enabled return 0; } /* Check if */ PromotedNotificationUi.isUnexpectedlyInLegacyMode(); return 0; } Loading Loading @@ -1550,6 +1569,45 @@ public class HeadsUpManagerImpl return 0; } /** * Determines the priority of the next HUN, comparing to the current HUN */ @NonNull public NextHunPriority getNextHunPriority(HeadsUpEntry nextHeadsUpEntry) { NotificationEntry nextEntry = nextHeadsUpEntry.mEntry; if (mEntry == null && nextEntry == null) { return NextHunPriority.SamePriority.INSTANCE; } else if (nextEntry == null) { return NextHunPriority.LowerPriority.INSTANCE; } else if (mEntry == null) { return NextHunPriority.HigherPriority.INSTANCE; } boolean currentCritical = hasFullScreenIntent(mEntry) || isCriticalCallNotif(mEntry); boolean nextHasFsi = hasFullScreenIntent(nextEntry); boolean nextIsCriticalCall = isCriticalCallNotif(nextEntry); if (nextHasFsi || nextIsCriticalCall) { // If next is critical, replace immediately regardless of current StringBuilder messageBuilder = new StringBuilder("Next is critical for:"); if (nextIsCriticalCall) messageBuilder.append(" critical call"); if (nextHasFsi) messageBuilder.append(" has FSI"); return new NextHunPriority.ReplaceImmediately(messageBuilder.toString()); } if (currentCritical) { return NextHunPriority.LowerPriority.INSTANCE; } if (mRemoteInputActive && !nextHeadsUpEntry.mRemoteInputActive) { return NextHunPriority.LowerPriority.INSTANCE; } else if (!mRemoteInputActive && nextHeadsUpEntry.mRemoteInputActive) { return NextHunPriority.HigherPriority.INSTANCE; } return NextHunPriority.SamePriority.INSTANCE; } public int compareTo(@NonNull HeadsUpEntry headsUpEntry) { if (mEntry == null && headsUpEntry.mEntry == null) { return 0; Loading Loading @@ -1637,7 +1695,7 @@ public class HeadsUpManagerImpl * Clear any pending removal runnables. */ public void cancelAutoRemovalCallbacks(@Nullable String reason) { Runnable runnable = () -> { Runnable cancellationRunnable = () -> { final boolean removed = cancelAutoRemovalCallbackInternal(); if (removed) { Loading @@ -1646,10 +1704,11 @@ public class HeadsUpManagerImpl }; if (mEntry != null && isHeadsUpEntry(mEntry.getKey())) { mLogger.logAutoRemoveCancelRequest(this.mEntry, reason); mAvalancheController.update(this, runnable, reason + " cancelAutoRemovalCallbacks"); mAvalancheController.update(this, cancellationRunnable, reason + " cancelAutoRemovalCallbacks"); } else { // Just removed runnable.run(); cancellationRunnable.run(); } } Loading
packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerLogger.kt +4 −0 Original line number Diff line number Diff line Loading @@ -204,6 +204,10 @@ constructor(@NotificationHeadsUpLog private val buffer: LogBuffer) { ) } fun logAddToNext(entry: NotificationEntry?) { buffer.log(TAG, INFO, { str1 = entry?.logKey }, { "Add to next: $str1" }) } fun logRemoveEntryRequest(key: String, reason: String, isWaiting: Boolean) { buffer.log( TAG, Loading