summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Yining Liu <liuyining@google.com> 2024-07-26 17:02:26 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2024-07-26 17:02:26 +0000
commitee927d35f2d9c801803c311cab2346d71940fc6c (patch)
treecd348acfb880430ce013b52ff955e3b5e8405292
parent5396e5f0ebb9100f3ca9610672b07ae4a13a299b (diff)
parentd8fcc9dc2b2665a4c6eacb7892852cd524aea4ed (diff)
Merge changes I819a942d,Ibb4899d8 into main
* changes: Format HeadsUpCoordinator Invalidate the pipeline list after HUN disappearing animation ends
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt849
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/GroupHunAnimationFix.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java9
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt39
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/OnHeadsUpChangedListener.java5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java47
7 files changed, 569 insertions, 387 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
index aff57bd076c5..e50d64bcb8f9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
@@ -50,10 +50,10 @@ import java.util.function.Consumer
import javax.inject.Inject
/**
- * Coordinates heads up notification (HUN) interactions with the notification pipeline based on
- * the HUN state reported by the [HeadsUpManager]. In this class we only consider one
- * notification, in particular the [HeadsUpManager.getTopEntry], to be HeadsUpping at a
- * time even though other notifications may be queued to heads up next.
+ * Coordinates heads up notification (HUN) interactions with the notification pipeline based on the
+ * HUN state reported by the [HeadsUpManager]. In this class we only consider one notification, in
+ * particular the [HeadsUpManager.getTopEntry], to be HeadsUpping at a time even though other
+ * notifications may be queued to heads up next.
*
* The current HUN, but not HUNs that are queued to heads up, will be:
* - Lifetime extended until it's no longer heads upping.
@@ -64,7 +64,9 @@ import javax.inject.Inject
* Note: The inflation callback in [PreparationCoordinator] handles showing HUNs.
*/
@CoordinatorScope
-class HeadsUpCoordinator @Inject constructor(
+class HeadsUpCoordinator
+@Inject
+constructor(
private val mLogger: HeadsUpCoordinatorLogger,
private val mSystemClock: SystemClock,
private val mHeadsUpManager: HeadsUpManager,
@@ -104,8 +106,8 @@ class HeadsUpCoordinator @Inject constructor(
}
/**
- * Once the pipeline starts running, we can look through posted entries and quickly process
- * any that don't have groups, and thus will never gave a group heads up edge case.
+ * Once the pipeline starts running, we can look through posted entries and quickly process any
+ * that don't have groups, and thus will never gave a group heads up edge case.
*/
fun onBeforeTransformGroups(list: List<ListEntry>) {
mNow = mSystemClock.currentTimeMillis()
@@ -128,120 +130,137 @@ class HeadsUpCoordinator @Inject constructor(
* we know that stability and [NotifPromoter]s have been applied, so we can use the location of
* notifications in this list to determine what kind of group heads up behavior should happen.
*/
- fun onBeforeFinalizeFilter(list: List<ListEntry>) = mHeadsUpManager.modifyHuns { hunMutator ->
- // Nothing to do if there are no other adds/updates
- if (mPostedEntries.isEmpty()) {
- return@modifyHuns
- }
- // Calculate a bunch of information about the logical group and the locations of group
- // entries in the nearly-finalized shade list. These may be used in the per-group loop.
- val postedEntriesByGroup = mPostedEntries.values.groupBy { it.entry.sbn.groupKey }
- val logicalMembersByGroup = mNotifPipeline.allNotifs.asSequence()
- .filter { postedEntriesByGroup.contains(it.sbn.groupKey) }
- .groupBy { it.sbn.groupKey }
- val groupLocationsByKey: Map<String, GroupLocation> by lazy { getGroupLocationsByKey(list) }
- mLogger.logEvaluatingGroups(postedEntriesByGroup.size)
- // For each group, determine which notification(s) for a group should heads up.
- postedEntriesByGroup.forEach { (groupKey, postedEntries) ->
- // get and classify the logical members
- val logicalMembers = logicalMembersByGroup[groupKey] ?: emptyList()
- val logicalSummary = logicalMembers.find { it.sbn.notification.isGroupSummary }
-
- // Report the start of this group's evaluation
- mLogger.logEvaluatingGroup(groupKey, postedEntries.size, logicalMembers.size)
-
- // If there is no logical summary, then there is no heads up to transfer
- if (logicalSummary == null) {
- postedEntries.forEach {
- handlePostedEntry(it, hunMutator, scenario = "logical-summary-missing")
- }
- return@forEach
+ fun onBeforeFinalizeFilter(list: List<ListEntry>) =
+ mHeadsUpManager.modifyHuns { hunMutator ->
+ // Nothing to do if there are no other adds/updates
+ if (mPostedEntries.isEmpty()) {
+ return@modifyHuns
}
+ // Calculate a bunch of information about the logical group and the locations of group
+ // entries in the nearly-finalized shade list. These may be used in the per-group loop.
+ val postedEntriesByGroup = mPostedEntries.values.groupBy { it.entry.sbn.groupKey }
+ val logicalMembersByGroup =
+ mNotifPipeline.allNotifs
+ .asSequence()
+ .filter { postedEntriesByGroup.contains(it.sbn.groupKey) }
+ .groupBy { it.sbn.groupKey }
+ val groupLocationsByKey: Map<String, GroupLocation> by lazy {
+ getGroupLocationsByKey(list)
+ }
+ mLogger.logEvaluatingGroups(postedEntriesByGroup.size)
+ // For each group, determine which notification(s) for a group should heads up.
+ postedEntriesByGroup.forEach { (groupKey, postedEntries) ->
+ // get and classify the logical members
+ val logicalMembers = logicalMembersByGroup[groupKey] ?: emptyList()
+ val logicalSummary = logicalMembers.find { it.sbn.notification.isGroupSummary }
+
+ // Report the start of this group's evaluation
+ mLogger.logEvaluatingGroup(groupKey, postedEntries.size, logicalMembers.size)
+
+ // If there is no logical summary, then there is no heads up to transfer
+ if (logicalSummary == null) {
+ postedEntries.forEach {
+ handlePostedEntry(it, hunMutator, scenario = "logical-summary-missing")
+ }
+ return@forEach
+ }
- // If summary isn't wanted to be heads up, then there is no heads up to transfer
- if (!isGoingToShowHunStrict(logicalSummary)) {
- postedEntries.forEach {
- handlePostedEntry(it, hunMutator, scenario = "logical-summary-not-heads-up")
+ // If summary isn't wanted to be heads up, then there is no heads up to transfer
+ if (!isGoingToShowHunStrict(logicalSummary)) {
+ postedEntries.forEach {
+ handlePostedEntry(it, hunMutator, scenario = "logical-summary-not-heads-up")
+ }
+ return@forEach
}
- return@forEach
- }
- // The group is heads up! Overall goals:
- // - Maybe transfer its heads up to a child
- // - Also let any/all newly heads up children still heads up
- var childToReceiveParentHeadsUp: NotificationEntry?
- var targetType = "undefined"
-
- // If the parent is heads up, always look at the posted notification with the newest
- // 'when', and if it is isolated with GROUP_ALERT_SUMMARY, then it should receive the
- // parent's heads up.
- childToReceiveParentHeadsUp =
- findHeadsUpOverride(postedEntries, groupLocationsByKey::getLocation)
- if (childToReceiveParentHeadsUp != null) {
- targetType = "headsUpOverride"
- }
+ // The group is heads up! Overall goals:
+ // - Maybe transfer its heads up to a child
+ // - Also let any/all newly heads up children still heads up
+ var childToReceiveParentHeadsUp: NotificationEntry?
+ var targetType = "undefined"
- // If the summary is Detached and we have not picked a receiver of the heads up, then we
- // need to look for the best child to heads up in place of the summary.
- val isSummaryAttached = groupLocationsByKey.contains(logicalSummary.key)
- if (!isSummaryAttached && childToReceiveParentHeadsUp == null) {
+ // If the parent is heads up, always look at the posted notification with the newest
+ // 'when', and if it is isolated with GROUP_ALERT_SUMMARY, then it should receive
+ // the
+ // parent's heads up.
childToReceiveParentHeadsUp =
- findBestTransferChild(logicalMembers, groupLocationsByKey::getLocation)
+ findHeadsUpOverride(postedEntries, groupLocationsByKey::getLocation)
if (childToReceiveParentHeadsUp != null) {
- targetType = "bestChild"
+ targetType = "headsUpOverride"
}
- }
- // If there is no child to receive the parent heads up, then just handle the posted
- // entries and return.
- if (childToReceiveParentHeadsUp == null) {
- postedEntries.forEach {
- handlePostedEntry(it, hunMutator, scenario = "no-transfer-target")
+ // If the summary is Detached and we have not picked a receiver of the heads up,
+ // then we
+ // need to look for the best child to heads up in place of the summary.
+ val isSummaryAttached = groupLocationsByKey.contains(logicalSummary.key)
+ if (!isSummaryAttached && childToReceiveParentHeadsUp == null) {
+ childToReceiveParentHeadsUp =
+ findBestTransferChild(logicalMembers, groupLocationsByKey::getLocation)
+ if (childToReceiveParentHeadsUp != null) {
+ targetType = "bestChild"
+ }
}
- return@forEach
- }
- // At this point we just need to initiate the transfer
- val summaryUpdate = mPostedEntries[logicalSummary.key]
-
- // Because we now know for certain that some child is going to heads up for this summary
- // (as we have found a child to transfer the heads up to), mark the group as having
- // interrupted. This will allow us to know in the future that the "should heads up"
- // state of this group has already been handled, just not via the summary entry itself.
- logicalSummary.setInterruption()
- mLogger.logSummaryMarkedInterrupted(logicalSummary.key, childToReceiveParentHeadsUp.key)
-
- // If the summary was not attached, then remove the heads up from the detached summary.
- // Otherwise we can simply ignore its posted update.
- if (!isSummaryAttached) {
- val summaryUpdateForRemoval = summaryUpdate?.also {
- it.shouldHeadsUpEver = false
- } ?: PostedEntry(
- logicalSummary,
- wasAdded = false,
- wasUpdated = false,
- shouldHeadsUpEver = false,
- shouldHeadsUpAgain = false,
- isHeadsUpEntry = mHeadsUpManager.isHeadsUpEntry(logicalSummary.key),
- isBinding = isEntryBinding(logicalSummary),
+ // If there is no child to receive the parent heads up, then just handle the posted
+ // entries and return.
+ if (childToReceiveParentHeadsUp == null) {
+ postedEntries.forEach {
+ handlePostedEntry(it, hunMutator, scenario = "no-transfer-target")
+ }
+ return@forEach
+ }
+
+ // At this point we just need to initiate the transfer
+ val summaryUpdate = mPostedEntries[logicalSummary.key]
+
+ // Because we now know for certain that some child is going to heads up for this
+ // summary
+ // (as we have found a child to transfer the heads up to), mark the group as having
+ // interrupted. This will allow us to know in the future that the "should heads up"
+ // state of this group has already been handled, just not via the summary entry
+ // itself.
+ logicalSummary.setInterruption()
+ mLogger.logSummaryMarkedInterrupted(
+ logicalSummary.key,
+ childToReceiveParentHeadsUp.key
)
- // If we transfer the heads up notification and the summary isn't even attached,
- // that means we should ensure the summary is no longer a heads up notification,
- // so we remove it here.
- handlePostedEntry(
+
+ // If the summary was not attached, then remove the heads up from the detached
+ // summary.
+ // Otherwise we can simply ignore its posted update.
+ if (!isSummaryAttached) {
+ val summaryUpdateForRemoval =
+ summaryUpdate?.also { it.shouldHeadsUpEver = false }
+ ?: PostedEntry(
+ logicalSummary,
+ wasAdded = false,
+ wasUpdated = false,
+ shouldHeadsUpEver = false,
+ shouldHeadsUpAgain = false,
+ isHeadsUpEntry = mHeadsUpManager.isHeadsUpEntry(logicalSummary.key),
+ isBinding = isEntryBinding(logicalSummary),
+ )
+ // If we transfer the heads up notification and the summary isn't even attached,
+ // that means we should ensure the summary is no longer a heads up notification,
+ // so we remove it here.
+ handlePostedEntry(
summaryUpdateForRemoval,
hunMutator,
- scenario = "detached-summary-remove-heads-up")
- } else if (summaryUpdate != null) {
- mLogger.logPostedEntryWillNotEvaluate(
+ scenario = "detached-summary-remove-heads-up"
+ )
+ } else if (summaryUpdate != null) {
+ mLogger.logPostedEntryWillNotEvaluate(
summaryUpdate,
- reason = "attached-summary-transferred")
- }
+ reason = "attached-summary-transferred"
+ )
+ }
- // Handle all posted entries -- if the child receiving the parent's heads up is in the
- // list, then set its flags to ensure it heads up.
- var didHeadsUpChildToReceiveParentHeadsUp = false
- postedEntries.asSequence()
+ // Handle all posted entries -- if the child receiving the parent's heads up is in
+ // the
+ // list, then set its flags to ensure it heads up.
+ var didHeadsUpChildToReceiveParentHeadsUp = false
+ postedEntries
+ .asSequence()
.filter { it.key != logicalSummary.key }
.forEach { postedEntry ->
if (childToReceiveParentHeadsUp.key == postedEntry.key) {
@@ -249,44 +268,49 @@ class HeadsUpCoordinator @Inject constructor(
postedEntry.shouldHeadsUpEver = true
postedEntry.shouldHeadsUpAgain = true
handlePostedEntry(
- postedEntry,
- hunMutator,
- scenario = "child-heads-up-transfer-target-$targetType")
+ postedEntry,
+ hunMutator,
+ scenario = "child-heads-up-transfer-target-$targetType"
+ )
didHeadsUpChildToReceiveParentHeadsUp = true
} else {
handlePostedEntry(
- postedEntry,
- hunMutator,
- scenario = "child-heads-up-non-target")
+ postedEntry,
+ hunMutator,
+ scenario = "child-heads-up-non-target"
+ )
}
}
- // If the child receiving the heads up notification was not updated on this tick
- // (which can happen in a standard heads up transfer scenario), then construct an update
- // so that we can apply it.
- if (!didHeadsUpChildToReceiveParentHeadsUp) {
- val posted = PostedEntry(
- childToReceiveParentHeadsUp,
- wasAdded = false,
- wasUpdated = false,
- shouldHeadsUpEver = true,
- shouldHeadsUpAgain = true,
- isHeadsUpEntry =
+ // If the child receiving the heads up notification was not updated on this tick
+ // (which can happen in a standard heads up transfer scenario), then construct an
+ // update
+ // so that we can apply it.
+ if (!didHeadsUpChildToReceiveParentHeadsUp) {
+ val posted =
+ PostedEntry(
+ childToReceiveParentHeadsUp,
+ wasAdded = false,
+ wasUpdated = false,
+ shouldHeadsUpEver = true,
+ shouldHeadsUpAgain = true,
+ isHeadsUpEntry =
mHeadsUpManager.isHeadsUpEntry(childToReceiveParentHeadsUp.key),
- isBinding = isEntryBinding(childToReceiveParentHeadsUp),
- )
- handlePostedEntry(
+ isBinding = isEntryBinding(childToReceiveParentHeadsUp),
+ )
+ handlePostedEntry(
posted,
hunMutator,
- scenario = "non-posted-child-heads-up-transfer-target-$targetType")
+ scenario = "non-posted-child-heads-up-transfer-target-$targetType"
+ )
+ }
}
- }
- // After this method runs, all posted entries should have been handled (or skipped).
- mPostedEntries.clear()
+ // After this method runs, all posted entries should have been handled (or skipped).
+ mPostedEntries.clear()
- // Also take this opportunity to clean up any stale entry update times
- cleanUpEntryTimes()
- }
+ // Also take this opportunity to clean up any stale entry update times
+ cleanUpEntryTimes()
+ }
/**
* Find the posted child with the newest when, and return it if it is isolated and has
@@ -295,34 +319,38 @@ class HeadsUpCoordinator @Inject constructor(
private fun findHeadsUpOverride(
postedEntries: List<PostedEntry>,
locationLookupByKey: (String) -> GroupLocation,
- ): NotificationEntry? = postedEntries.asSequence()
- .filter { posted -> !posted.entry.sbn.notification.isGroupSummary }
- .sortedBy { posted ->
- -posted.entry.sbn.notification.getWhen()
- }
- .firstOrNull()
- ?.let { posted ->
- posted.entry.takeIf { entry ->
- locationLookupByKey(entry.key) == GroupLocation.Isolated &&
+ ): NotificationEntry? =
+ postedEntries
+ .asSequence()
+ .filter { posted -> !posted.entry.sbn.notification.isGroupSummary }
+ .sortedBy { posted -> -posted.entry.sbn.notification.getWhen() }
+ .firstOrNull()
+ ?.let { posted ->
+ posted.entry.takeIf { entry ->
+ locationLookupByKey(entry.key) == GroupLocation.Isolated &&
entry.sbn.notification.groupAlertBehavior == GROUP_ALERT_SUMMARY
+ }
}
- }
/**
- * Of children which are attached, look for the child to receive the notification:
- * First prefer children which were updated, then looking for the ones with the newest 'when'
+ * Of children which are attached, look for the child to receive the notification: First prefer
+ * children which were updated, then looking for the ones with the newest 'when'
*/
private fun findBestTransferChild(
logicalMembers: List<NotificationEntry>,
locationLookupByKey: (String) -> GroupLocation,
- ): NotificationEntry? = logicalMembers.asSequence()
- .filter { !it.sbn.notification.isGroupSummary }
- .filter { locationLookupByKey(it.key) != GroupLocation.Detached }
- .sortedWith(compareBy(
- { !mPostedEntries.contains(it.key) },
- { -it.sbn.notification.getWhen() },
- ))
- .firstOrNull()
+ ): NotificationEntry? =
+ logicalMembers
+ .asSequence()
+ .filter { !it.sbn.notification.isGroupSummary }
+ .filter { locationLookupByKey(it.key) != GroupLocation.Detached }
+ .sortedWith(
+ compareBy(
+ { !mPostedEntries.contains(it.key) },
+ { -it.sbn.notification.getWhen() },
+ )
+ )
+ .firstOrNull()
private fun getGroupLocationsByKey(list: List<ListEntry>): Map<String, GroupLocation> =
mutableMapOf<String, GroupLocation>().also { map ->
@@ -387,197 +415,217 @@ class HeadsUpCoordinator @Inject constructor(
mHeadsUpViewBinder.bindHeadsUpView(posted.entry, this::onHeadsUpViewBound)
}
- private val mNotifCollectionListener = object : NotifCollectionListener {
- /**
- * Notification was just added and if it should heads up, bind the view and then show it.
- */
- override fun onEntryAdded(entry: NotificationEntry) {
- // First check whether this notification should launch a full screen intent, and
- // launch it if needed.
- val fsiDecision =
- mVisualInterruptionDecisionProvider.makeUnloggedFullScreenIntentDecision(entry)
- mVisualInterruptionDecisionProvider.logFullScreenIntentDecision(fsiDecision)
- if (fsiDecision.shouldInterrupt) {
- mLaunchFullScreenIntentProvider.launchFullScreenIntent(entry)
- } else if (fsiDecision.wouldInterruptWithoutDnd) {
- // If DND was the only reason this entry was suppressed, note it for potential
- // reconsideration on later ranking updates.
- addForFSIReconsideration(entry, mSystemClock.currentTimeMillis())
- }
-
- // makeAndLogHeadsUpDecision includes check for whether this notification should be
- // filtered
- val shouldHeadsUpEver =
- mVisualInterruptionDecisionProvider.makeAndLogHeadsUpDecision(entry).shouldInterrupt
- mPostedEntries[entry.key] = PostedEntry(
- entry,
- wasAdded = true,
- wasUpdated = false,
- shouldHeadsUpEver = shouldHeadsUpEver,
- shouldHeadsUpAgain = true,
- isHeadsUpEntry = false,
- isBinding = false,
- )
+ private val mNotifCollectionListener =
+ object : NotifCollectionListener {
+ /**
+ * Notification was just added and if it should heads up, bind the view and then show
+ * it.
+ */
+ override fun onEntryAdded(entry: NotificationEntry) {
+ // First check whether this notification should launch a full screen intent, and
+ // launch it if needed.
+ val fsiDecision =
+ mVisualInterruptionDecisionProvider.makeUnloggedFullScreenIntentDecision(entry)
+ mVisualInterruptionDecisionProvider.logFullScreenIntentDecision(fsiDecision)
+ if (fsiDecision.shouldInterrupt) {
+ mLaunchFullScreenIntentProvider.launchFullScreenIntent(entry)
+ } else if (fsiDecision.wouldInterruptWithoutDnd) {
+ // If DND was the only reason this entry was suppressed, note it for potential
+ // reconsideration on later ranking updates.
+ addForFSIReconsideration(entry, mSystemClock.currentTimeMillis())
+ }
- // Record the last updated time for this key
- setUpdateTime(entry, mSystemClock.currentTimeMillis())
- }
+ // makeAndLogHeadsUpDecision includes check for whether this notification should be
+ // filtered
+ val shouldHeadsUpEver =
+ mVisualInterruptionDecisionProvider
+ .makeAndLogHeadsUpDecision(entry)
+ .shouldInterrupt
+ mPostedEntries[entry.key] =
+ PostedEntry(
+ entry,
+ wasAdded = true,
+ wasUpdated = false,
+ shouldHeadsUpEver = shouldHeadsUpEver,
+ shouldHeadsUpAgain = true,
+ isHeadsUpEntry = false,
+ isBinding = false,
+ )
- /**
- * Notification could've updated to be heads up or not heads up. Even if it did update to
- * heads up, if the notification specified that it only wants to heads up once, don't heads
- * up again.
- */
- override fun onEntryUpdated(entry: NotificationEntry) {
- val shouldHeadsUpEver =
- mVisualInterruptionDecisionProvider.makeAndLogHeadsUpDecision(entry).shouldInterrupt
- val shouldHeadsUpAgain = shouldHunAgain(entry)
- val isHeadsUpEntry = mHeadsUpManager.isHeadsUpEntry(entry.key)
- val isBinding = isEntryBinding(entry)
- val posted = mPostedEntries.compute(entry.key) { _, value ->
- value?.also { update ->
- update.wasUpdated = true
- update.shouldHeadsUpEver = shouldHeadsUpEver
- update.shouldHeadsUpAgain = update.shouldHeadsUpAgain || shouldHeadsUpAgain
- update.isHeadsUpEntry = isHeadsUpEntry
- update.isBinding = isBinding
- } ?: PostedEntry(
- entry,
- wasAdded = false,
- wasUpdated = true,
- shouldHeadsUpEver = shouldHeadsUpEver,
- shouldHeadsUpAgain = shouldHeadsUpAgain,
- isHeadsUpEntry = isHeadsUpEntry,
- isBinding = isBinding,
- )
+ // Record the last updated time for this key
+ setUpdateTime(entry, mSystemClock.currentTimeMillis())
}
- // Handle cancelling heads up here, rather than in the OnBeforeFinalizeFilter, so that
- // work can be done before the ShadeListBuilder is run. This prevents re-entrant
- // behavior between this Coordinator, HeadsUpManager, and VisualStabilityManager.
- if (posted?.shouldHeadsUpEver == false) {
- if (posted.isHeadsUpEntry) {
- // We don't want this to be interrupting anymore, let's remove it
- mHeadsUpManager.removeNotification(posted.key, false /*removeImmediately*/)
- } else if (posted.isBinding) {
- // Don't let the bind finish
- cancelHeadsUpBind(posted.entry)
+
+ /**
+ * Notification could've updated to be heads up or not heads up. Even if it did update
+ * to heads up, if the notification specified that it only wants to heads up once, don't
+ * heads up again.
+ */
+ override fun onEntryUpdated(entry: NotificationEntry) {
+ val shouldHeadsUpEver =
+ mVisualInterruptionDecisionProvider
+ .makeAndLogHeadsUpDecision(entry)
+ .shouldInterrupt
+ val shouldHeadsUpAgain = shouldHunAgain(entry)
+ val isHeadsUpEntry = mHeadsUpManager.isHeadsUpEntry(entry.key)
+ val isBinding = isEntryBinding(entry)
+ val posted =
+ mPostedEntries.compute(entry.key) { _, value ->
+ value?.also { update ->
+ update.wasUpdated = true
+ update.shouldHeadsUpEver = shouldHeadsUpEver
+ update.shouldHeadsUpAgain =
+ update.shouldHeadsUpAgain || shouldHeadsUpAgain
+ update.isHeadsUpEntry = isHeadsUpEntry
+ update.isBinding = isBinding
+ }
+ ?: PostedEntry(
+ entry,
+ wasAdded = false,
+ wasUpdated = true,
+ shouldHeadsUpEver = shouldHeadsUpEver,
+ shouldHeadsUpAgain = shouldHeadsUpAgain,
+ isHeadsUpEntry = isHeadsUpEntry,
+ isBinding = isBinding,
+ )
+ }
+ // Handle cancelling heads up here, rather than in the OnBeforeFinalizeFilter, so
+ // that
+ // work can be done before the ShadeListBuilder is run. This prevents re-entrant
+ // behavior between this Coordinator, HeadsUpManager, and VisualStabilityManager.
+ if (posted?.shouldHeadsUpEver == false) {
+ if (posted.isHeadsUpEntry) {
+ // We don't want this to be interrupting anymore, let's remove it
+ mHeadsUpManager.removeNotification(posted.key, false /*removeImmediately*/)
+ } else if (posted.isBinding) {
+ // Don't let the bind finish
+ cancelHeadsUpBind(posted.entry)
+ }
}
+
+ // Update last updated time for this entry
+ setUpdateTime(entry, mSystemClock.currentTimeMillis())
}
- // Update last updated time for this entry
- setUpdateTime(entry, mSystemClock.currentTimeMillis())
- }
+ /** Stop showing as heads up once removed from the notification collection */
+ override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
+ mPostedEntries.remove(entry.key)
+ mEntriesUpdateTimes.remove(entry.key)
+ cancelHeadsUpBind(entry)
+ val entryKey = entry.key
+ if (mHeadsUpManager.isHeadsUpEntry(entryKey)) {
+ // TODO: This should probably know the RemoteInputCoordinator's conditions,
+ // or otherwise reference that coordinator's state, rather than replicate its
+ // logic
+ val removeImmediatelyForRemoteInput =
+ (mRemoteInputManager.isSpinning(entryKey) &&
+ !NotificationRemoteInputManager.FORCE_REMOTE_INPUT_HISTORY)
+ mHeadsUpManager.removeNotification(entry.key, removeImmediatelyForRemoteInput)
+ }
+ }
- /**
- * Stop showing as heads up once removed from the notification collection
- */
- override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
- mPostedEntries.remove(entry.key)
- mEntriesUpdateTimes.remove(entry.key)
- cancelHeadsUpBind(entry)
- val entryKey = entry.key
- if (mHeadsUpManager.isHeadsUpEntry(entryKey)) {
- // TODO: This should probably know the RemoteInputCoordinator's conditions,
- // or otherwise reference that coordinator's state, rather than replicate its logic
- val removeImmediatelyForRemoteInput = (mRemoteInputManager.isSpinning(entryKey) &&
- !NotificationRemoteInputManager.FORCE_REMOTE_INPUT_HISTORY)
- mHeadsUpManager.removeNotification(entry.key, removeImmediatelyForRemoteInput)
+ override fun onEntryCleanUp(entry: NotificationEntry) {
+ mHeadsUpViewBinder.abortBindCallback(entry)
}
- }
- override fun onEntryCleanUp(entry: NotificationEntry) {
- mHeadsUpViewBinder.abortBindCallback(entry)
- }
+ /**
+ * Identify notifications whose heads-up state changes when the notification rankings
+ * are updated, and have those changed notifications heads up if necessary.
+ *
+ * This method will occur after any operations in onEntryAdded or onEntryUpdated, so any
+ * handling of ranking changes needs to take into account that we may have just made a
+ * PostedEntry for some of these notifications.
+ */
+ override fun onRankingApplied() {
+ // Because a ranking update may cause some notifications that are no longer (or were
+ // never) in mPostedEntries to need to heads up, we need to check every notification
+ // known to the pipeline.
+ for (entry in mNotifPipeline.allNotifs) {
+ // Only consider entries that are recent enough, since we want to apply a fairly
+ // strict threshold for when an entry should be updated via only ranking and not
+ // an
+ // app-provided notification update.
+ if (!isNewEnoughForRankingUpdate(entry)) continue
+
+ // The only entries we consider heads up for here are entries that have never
+ // interrupted and that now say they should heads up or FSI; if they've heads
+ // uped in
+ // the past, we don't want to incorrectly heads up a second time if there wasn't
+ // an
+ // explicit notification update.
+ if (entry.hasInterrupted()) continue
+
+ // Before potentially allowing heads-up, check for any candidates for a FSI
+ // launch.
+ // Any entry that is a candidate meets two criteria:
+ // - was suppressed from FSI launch only by a DND suppression
+ // - is within the recency window for reconsideration
+ // If any of these entries are no longer suppressed, launch the FSI now.
+ if (isCandidateForFSIReconsideration(entry)) {
+ val decision =
+ mVisualInterruptionDecisionProvider
+ .makeUnloggedFullScreenIntentDecision(entry)
+ if (decision.shouldInterrupt) {
+ // Log both the launch of the full screen and also that this was via a
+ // ranking update, and finally revoke candidacy for FSI reconsideration
+ mLogger.logEntryUpdatedToFullScreen(entry.key, decision.logReason)
+ mVisualInterruptionDecisionProvider.logFullScreenIntentDecision(
+ decision
+ )
+ mLaunchFullScreenIntentProvider.launchFullScreenIntent(entry)
+ mFSIUpdateCandidates.remove(entry.key)
+
+ // if we launch the FSI then this is no longer a candidate for HUN
+ continue
+ } else if (decision.wouldInterruptWithoutDnd) {
+ // decision has not changed; no need to log
+ } else {
+ // some other condition is now blocking FSI; log that and revoke
+ // candidacy
+ // for FSI reconsideration
+ mLogger.logEntryDisqualifiedFromFullScreen(
+ entry.key,
+ decision.logReason
+ )
+ mVisualInterruptionDecisionProvider.logFullScreenIntentDecision(
+ decision
+ )
+ mFSIUpdateCandidates.remove(entry.key)
+ }
+ }
- /**
- * Identify notifications whose heads-up state changes when the notification rankings are
- * updated, and have those changed notifications heads up if necessary.
- *
- * This method will occur after any operations in onEntryAdded or onEntryUpdated, so any
- * handling of ranking changes needs to take into account that we may have just made a
- * PostedEntry for some of these notifications.
- */
- override fun onRankingApplied() {
- // Because a ranking update may cause some notifications that are no longer (or were
- // never) in mPostedEntries to need to heads up, we need to check every notification
- // known to the pipeline.
- for (entry in mNotifPipeline.allNotifs) {
- // Only consider entries that are recent enough, since we want to apply a fairly
- // strict threshold for when an entry should be updated via only ranking and not an
- // app-provided notification update.
- if (!isNewEnoughForRankingUpdate(entry)) continue
-
- // The only entries we consider heads up for here are entries that have never
- // interrupted and that now say they should heads up or FSI; if they've heads uped in
- // the past, we don't want to incorrectly heads up a second time if there wasn't an
- // explicit notification update.
- if (entry.hasInterrupted()) continue
-
- // Before potentially allowing heads-up, check for any candidates for a FSI launch.
- // Any entry that is a candidate meets two criteria:
- // - was suppressed from FSI launch only by a DND suppression
- // - is within the recency window for reconsideration
- // If any of these entries are no longer suppressed, launch the FSI now.
- if (isCandidateForFSIReconsideration(entry)) {
+ // The cases where we should consider this notification to be updated:
+ // - if this entry is not present in PostedEntries, and is now in a
+ // shouldHeadsUp
+ // state
+ // - if it is present in PostedEntries and the previous state of shouldHeadsUp
+ // differs from the updated one
val decision =
- mVisualInterruptionDecisionProvider.makeUnloggedFullScreenIntentDecision(
- entry
+ mVisualInterruptionDecisionProvider.makeUnloggedHeadsUpDecision(entry)
+ val shouldHeadsUpEver = decision.shouldInterrupt
+ val postedShouldHeadsUpEver =
+ mPostedEntries[entry.key]?.shouldHeadsUpEver ?: false
+ val shouldUpdateEntry = postedShouldHeadsUpEver != shouldHeadsUpEver
+
+ if (shouldUpdateEntry) {
+ mLogger.logEntryUpdatedByRanking(
+ entry.key,
+ shouldHeadsUpEver,
+ decision.logReason
)
- if (decision.shouldInterrupt) {
- // Log both the launch of the full screen and also that this was via a
- // ranking update, and finally revoke candidacy for FSI reconsideration
- mLogger.logEntryUpdatedToFullScreen(entry.key, decision.logReason)
- mVisualInterruptionDecisionProvider.logFullScreenIntentDecision(decision)
- mLaunchFullScreenIntentProvider.launchFullScreenIntent(entry)
- mFSIUpdateCandidates.remove(entry.key)
-
- // if we launch the FSI then this is no longer a candidate for HUN
- continue
- } else if (decision.wouldInterruptWithoutDnd) {
- // decision has not changed; no need to log
- } else {
- // some other condition is now blocking FSI; log that and revoke candidacy
- // for FSI reconsideration
- mLogger.logEntryDisqualifiedFromFullScreen(entry.key, decision.logReason)
- mVisualInterruptionDecisionProvider.logFullScreenIntentDecision(decision)
- mFSIUpdateCandidates.remove(entry.key)
+ onEntryUpdated(entry)
}
}
-
- // The cases where we should consider this notification to be updated:
- // - if this entry is not present in PostedEntries, and is now in a shouldHeadsUp
- // state
- // - if it is present in PostedEntries and the previous state of shouldHeadsUp
- // differs from the updated one
- val decision =
- mVisualInterruptionDecisionProvider.makeUnloggedHeadsUpDecision(entry)
- val shouldHeadsUpEver = decision.shouldInterrupt
- val postedShouldHeadsUpEver = mPostedEntries[entry.key]?.shouldHeadsUpEver ?: false
- val shouldUpdateEntry = postedShouldHeadsUpEver != shouldHeadsUpEver
-
- if (shouldUpdateEntry) {
- mLogger.logEntryUpdatedByRanking(
- entry.key,
- shouldHeadsUpEver,
- decision.logReason
- )
- onEntryUpdated(entry)
- }
}
}
- }
- /**
- * Checks whether an update for a notification warrants an heads up for the user.
- */
+ /** Checks whether an update for a notification warrants an heads up for the user. */
private fun shouldHunAgain(entry: NotificationEntry): Boolean {
return (!entry.hasInterrupted() ||
- (entry.sbn.notification.flags and Notification.FLAG_ONLY_ALERT_ONCE) == 0)
+ (entry.sbn.notification.flags and Notification.FLAG_ONLY_ALERT_ONCE) == 0)
}
- /**
- * Sets the updated time for the given entry to the specified time.
- */
+ /** Sets the updated time for the given entry to the specified time. */
@VisibleForTesting
fun setUpdateTime(entry: NotificationEntry, time: Long) {
mEntriesUpdateTimes[entry.key] = time
@@ -593,10 +641,10 @@ class HeadsUpCoordinator @Inject constructor(
}
/**
- * Checks whether the entry is new enough to be updated via ranking update.
- * We want to avoid updating an entry too long after it was originally posted/updated when we're
- * only reacting to a ranking change, as relevant ranking updates are expected to come in
- * fairly soon after the posting of a notification.
+ * Checks whether the entry is new enough to be updated via ranking update. We want to avoid
+ * updating an entry too long after it was originally posted/updated when we're only reacting to
+ * a ranking change, as relevant ranking updates are expected to come in fairly soon after the
+ * posting of a notification.
*/
private fun isNewEnoughForRankingUpdate(entry: NotificationEntry): Boolean {
// If we don't have an update time for this key, default to "too old"
@@ -648,72 +696,92 @@ class HeadsUpCoordinator @Inject constructor(
* @see HeadsUpManager.setUserActionMayIndirectlyRemove
* @see HeadsUpManager.canRemoveImmediately
*/
- private val mActionPressListener = Consumer<NotificationEntry> { entry ->
- mHeadsUpManager.setUserActionMayIndirectlyRemove(entry)
- mExecutor.execute { endNotifLifetimeExtensionIfExtended(entry) }
- }
-
- private val mLifetimeExtender = object : NotifLifetimeExtender {
- override fun getName() = TAG
-
- override fun setCallback(callback: OnEndLifetimeExtensionCallback) {
- mEndLifetimeExtension = callback
+ private val mActionPressListener =
+ Consumer<NotificationEntry> { entry ->
+ mHeadsUpManager.setUserActionMayIndirectlyRemove(entry)
+ mExecutor.execute { endNotifLifetimeExtensionIfExtended(entry) }
}
- override fun maybeExtendLifetime(entry: NotificationEntry, reason: Int): Boolean {
- if (mHeadsUpManager.canRemoveImmediately(entry.key)) {
- return false
+ private val mLifetimeExtender =
+ object : NotifLifetimeExtender {
+ override fun getName() = TAG
+
+ override fun setCallback(callback: OnEndLifetimeExtensionCallback) {
+ mEndLifetimeExtension = callback
}
- if (isSticky(entry)) {
- val removeAfterMillis = mHeadsUpManager.getEarliestRemovalTime(entry.key)
- mNotifsExtendingLifetime[entry] = mExecutor.executeDelayed({
- mHeadsUpManager.removeNotification(entry.key, /* releaseImmediately */ true)
- }, removeAfterMillis)
- } else {
- mExecutor.execute {
- mHeadsUpManager.removeNotification(entry.key, /* releaseImmediately */ false)
+
+ override fun maybeExtendLifetime(entry: NotificationEntry, reason: Int): Boolean {
+ if (mHeadsUpManager.canRemoveImmediately(entry.key)) {
+ return false
+ }
+ if (isSticky(entry)) {
+ val removeAfterMillis = mHeadsUpManager.getEarliestRemovalTime(entry.key)
+ mNotifsExtendingLifetime[entry] =
+ mExecutor.executeDelayed(
+ {
+ mHeadsUpManager.removeNotification(
+ entry.key, /* releaseImmediately */
+ true
+ )
+ },
+ removeAfterMillis
+ )
+ } else {
+ mExecutor.execute {
+ mHeadsUpManager.removeNotification(
+ entry.key, /* releaseImmediately */
+ false
+ )
+ }
+ mNotifsExtendingLifetime[entry] = null
}
- mNotifsExtendingLifetime[entry] = null
+ return true
}
- return true
- }
- override fun cancelLifetimeExtension(entry: NotificationEntry) {
- mNotifsExtendingLifetime.remove(entry)?.run()
+ override fun cancelLifetimeExtension(entry: NotificationEntry) {
+ mNotifsExtendingLifetime.remove(entry)?.run()
+ }
}
- }
- private val mNotifPromoter = object : NotifPromoter(TAG) {
- override fun shouldPromoteToTopLevel(entry: NotificationEntry): Boolean =
- isGoingToShowHunNoRetract(entry)
- }
+ private val mNotifPromoter =
+ object : NotifPromoter(TAG) {
+ override fun shouldPromoteToTopLevel(entry: NotificationEntry): Boolean =
+ isGoingToShowHunNoRetract(entry)
+ }
- val sectioner = object : NotifSectioner("HeadsUp", BUCKET_HEADS_UP) {
- override fun isInSection(entry: ListEntry): Boolean =
- // TODO: This check won't notice if a child of the group is going to HUN...
- isGoingToShowHunNoRetract(entry)
+ val sectioner =
+ object : NotifSectioner("HeadsUp", BUCKET_HEADS_UP) {
+ override fun isInSection(entry: ListEntry): Boolean =
+ // TODO: This check won't notice if a child of the group is going to HUN...
+ isGoingToShowHunNoRetract(entry)
- override fun getComparator(): NotifComparator {
- return object : NotifComparator("HeadsUp") {
- override fun compare(o1: ListEntry, o2: ListEntry): Int =
- mHeadsUpManager.compare(o1.representativeEntry, o2.representativeEntry)
+ override fun getComparator(): NotifComparator {
+ return object : NotifComparator("HeadsUp") {
+ override fun compare(o1: ListEntry, o2: ListEntry): Int =
+ mHeadsUpManager.compare(o1.representativeEntry, o2.representativeEntry)
+ }
}
+
+ override fun getHeaderNodeController(): NodeController? =
+ // TODO: remove SHOW_ALL_SECTIONS, this redundant method, and
+ // mIncomingHeaderController
+ if (RankingCoordinator.SHOW_ALL_SECTIONS) mIncomingHeaderController else null
}
- override fun getHeaderNodeController(): NodeController? =
- // TODO: remove SHOW_ALL_SECTIONS, this redundant method, and mIncomingHeaderController
- if (RankingCoordinator.SHOW_ALL_SECTIONS) mIncomingHeaderController else null
- }
+ private val mOnHeadsUpChangedListener =
+ object : OnHeadsUpChangedListener {
+ override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) {
+ if (!isHeadsUp) {
+ mNotifPromoter.invalidateList("headsUpEnded: ${entry.logKey}")
+ mHeadsUpViewBinder.unbindHeadsUpView(entry)
+ endNotifLifetimeExtensionIfExtended(entry)
+ }
+ }
- private val mOnHeadsUpChangedListener = object : OnHeadsUpChangedListener {
- override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) {
- if (!isHeadsUp) {
- mNotifPromoter.invalidateList("headsUpEnded: ${entry.logKey}")
- mHeadsUpViewBinder.unbindHeadsUpView(entry)
- endNotifLifetimeExtensionIfExtended(entry)
+ override fun onHeadsUpAnimatingAwayEnded(entry: NotificationEntry) {
+ mNotifPromoter.invalidateList("headsUpAnimatingAwayEnded: ${entry.logKey}")
}
}
- }
private fun isSticky(entry: NotificationEntry) = mHeadsUpManager.isSticky(entry.key)
@@ -726,8 +794,9 @@ class HeadsUpCoordinator @Inject constructor(
* Whether the notification is already heads up or binding so that it can imminently heads up
*/
private fun isAttemptingToShowHun(entry: ListEntry) =
- mHeadsUpManager.isHeadsUpEntry(entry.key) || isEntryBinding(entry)
- || isHeadsUpAnimatingAway(entry)
+ mHeadsUpManager.isHeadsUpEntry(entry.key) ||
+ isEntryBinding(entry) ||
+ isHeadsUpAnimatingAway(entry)
private fun isHeadsUpAnimatingAway(entry: ListEntry): Boolean {
if (!GroupHunAnimationFix.isEnabled) return false
@@ -735,19 +804,19 @@ class HeadsUpCoordinator @Inject constructor(
}
/**
- * Whether the notification is already heads up/binding per [isAttemptingToShowHun] OR if it
- * has been updated so that it should heads up this update. This method is permissive because
- * it returns `true` even if the update would (in isolation of its group) cause the heads up to
- * be retracted. This is important for not retracting transferred group heads ups.
+ * Whether the notification is already heads up/binding per [isAttemptingToShowHun] OR if it has
+ * been updated so that it should heads up this update. This method is permissive because it
+ * returns `true` even if the update would (in isolation of its group) cause the heads up to be
+ * retracted. This is important for not retracting transferred group heads ups.
*/
private fun isGoingToShowHunNoRetract(entry: ListEntry) =
mPostedEntries[entry.key]?.calculateShouldBeHeadsUpNoRetract ?: isAttemptingToShowHun(entry)
/**
* If the notification has been updated, then whether it should HUN in isolation, otherwise
- * defers to the already heads up/binding state of [isAttemptingToShowHun]. This method is
- * strict because any update which would revoke the heads up supersedes the current
- * heads up/binding state.
+ * defers to the already heads up/binding state of [isAttemptingToShowHun]. This method is
+ * strict because any update which would revoke the heads up supersedes the current heads
+ * up/binding state.
*/
private fun isGoingToShowHunStrict(entry: ListEntry) =
mPostedEntries[entry.key]?.calculateShouldBeHeadsUpStrict ?: isAttemptingToShowHun(entry)
@@ -779,14 +848,21 @@ class HeadsUpCoordinator @Inject constructor(
val key = entry.key
val isHeadsUpAlready: Boolean
get() = isHeadsUpEntry || isBinding
+
val calculateShouldBeHeadsUpStrict: Boolean
get() = shouldHeadsUpEver && (wasAdded || shouldHeadsUpAgain || isHeadsUpAlready)
+
val calculateShouldBeHeadsUpNoRetract: Boolean
get() = isHeadsUpAlready || (shouldHeadsUpEver && (wasAdded || shouldHeadsUpAgain))
}
}
-private enum class GroupLocation { Detached, Isolated, Summary, Child }
+private enum class GroupLocation {
+ Detached,
+ Isolated,
+ Summary,
+ Child
+}
private fun Map<String, GroupLocation>.getLocation(key: String): GroupLocation =
getOrDefault(key, GroupLocation.Detached)
@@ -804,6 +880,7 @@ private fun <R> HeadsUpManager.modifyHuns(block: (HunMutator) -> R): R {
/** Mutates the HeadsUp state of notifications. */
private interface HunMutator {
fun updateNotification(key: String, shouldHeadsUpAgain: Boolean)
+
fun removeNotification(key: String, releaseImmediately: Boolean)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/GroupHunAnimationFix.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/GroupHunAnimationFix.kt
index 5867612d0b51..3b30c8623491 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/GroupHunAnimationFix.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/GroupHunAnimationFix.kt
@@ -29,7 +29,7 @@ object GroupHunAnimationFix {
val token: FlagToken
get() = FlagToken(FLAG_NAME, isEnabled)
- /** Are sections sorted by time? */
+ /** Return whether the fix is enabled */
@JvmStatic
inline val isEnabled
get() = Flags.notificationGroupHunRemovalAnimationFix()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index a072ea6ec3eb..fb1c5254cc5c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -124,6 +124,7 @@ import com.android.systemui.statusbar.notification.row.ExpandableView;
import com.android.systemui.statusbar.notification.row.NotificationGuts;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
import com.android.systemui.statusbar.notification.row.NotificationSnooze;
+import com.android.systemui.statusbar.notification.shared.GroupHunAnimationFix;
import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor;
import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder;
import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
@@ -1987,6 +1988,10 @@ public class NotificationStackScrollLayoutController implements Dumpable {
NotificationEntry entry = row.getEntry();
mHeadsUpAppearanceController.updateHeader(entry);
mHeadsUpAppearanceController.updateHeadsUpAndPulsingRoundness(entry);
+ if (GroupHunAnimationFix.isEnabled() && !animatingAway) {
+ // invalidate list to make sure the row is sorted to the correct section
+ mHeadsUpManager.onEntryAnimatingAwayEnded(entry);
+ }
});
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
index 031539691fe4..4bd0f22ca681 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
@@ -464,6 +464,15 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager {
}
/**
+ * Called to notify the listeners that the HUN animating away animation has ended.
+ */
+ public void onEntryAnimatingAwayEnded(@NonNull NotificationEntry entry) {
+ for (OnHeadsUpChangedListener listener : mListeners) {
+ listener.onHeadsUpAnimatingAwayEnded(entry);
+ }
+ }
+
+ /**
* Manager-specific logic, that should occur, when the entry is updated, and its posted time has
* changed.
*
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt
index 28a2a1f49bf6..fcf77d5526d4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt
@@ -42,6 +42,7 @@ interface HeadsUpManager : Dumpable {
* should be ranked higher and 0 if they are equal.
*/
fun compare(a: NotificationEntry?, b: NotificationEntry?): Int
+
/**
* Extends the lifetime of the currently showing pulsing notification so that the pulse lasts
* longer.
@@ -184,6 +185,8 @@ interface HeadsUpManager : Dumpable {
fun unpinAll(userUnPinned: Boolean)
fun updateNotification(key: String, shouldHeadsUpAgain: Boolean)
+
+ fun onEntryAnimatingAwayEnded(entry: NotificationEntry)
}
/** Sets the animation state of the HeadsUpManager. */
@@ -204,41 +207,77 @@ interface OnHeadsUpPhoneListenerChange {
/* No op impl of HeadsUpManager. */
class HeadsUpManagerEmptyImpl @Inject constructor() : HeadsUpManager {
override val allEntries = Stream.empty<NotificationEntry>()
+
override fun addHeadsUpPhoneListener(listener: OnHeadsUpPhoneListenerChange) {}
+
override fun addListener(listener: OnHeadsUpChangedListener) {}
+
override fun addSwipedOutNotification(key: String) {}
+
override fun canRemoveImmediately(key: String) = false
+
override fun compare(a: NotificationEntry?, b: NotificationEntry?) = 0
+
override fun dump(pw: PrintWriter, args: Array<out String>) {}
+
override fun extendHeadsUp() {}
+
override fun getEarliestRemovalTime(key: String?) = 0L
+
override fun getTouchableRegion(): Region? = null
+
override fun getTopEntry() = null
+
override fun hasPinnedHeadsUp() = false
+
override fun isHeadsUpEntry(key: String) = false
+
override fun isHeadsUpAnimatingAwayValue() = false
+
override fun isSnoozed(packageName: String) = false
+
override fun isSticky(key: String?) = false
+
override fun isTrackingHeadsUp() = false
+
override fun onExpandingFinished() {}
+
override fun releaseAllImmediately() {}
+
override fun removeListener(listener: OnHeadsUpChangedListener) {}
+
override fun removeNotification(key: String, releaseImmediately: Boolean) = false
+
override fun removeNotification(key: String, releaseImmediately: Boolean, animate: Boolean) =
false
+
override fun setAnimationStateHandler(handler: AnimationStateHandler) {}
+
override fun setExpanded(entry: NotificationEntry, expanded: Boolean) {}
+
override fun setGutsShown(entry: NotificationEntry, gutsShown: Boolean) {}
+
override fun setHeadsUpAnimatingAway(headsUpAnimatingAway: Boolean) {}
+
override fun setRemoteInputActive(entry: NotificationEntry, remoteInputActive: Boolean) {}
+
override fun setTrackingHeadsUp(tracking: Boolean) {}
+
override fun setUser(user: Int) {}
+
override fun setUserActionMayIndirectlyRemove(entry: NotificationEntry) {}
+
override fun shouldSwallowClick(key: String): Boolean = false
+
override fun showNotification(entry: NotificationEntry) {}
+
override fun snooze() {}
+
override fun unpinAll(userUnPinned: Boolean) {}
+
override fun updateNotification(key: String, alert: Boolean) {}
+
+ override fun onEntryAnimatingAwayEnded(entry: NotificationEntry) {}
}
@Module
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/OnHeadsUpChangedListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/OnHeadsUpChangedListener.java
index 86998ab2fdd9..de3bf0462d5b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/OnHeadsUpChangedListener.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/OnHeadsUpChangedListener.java
@@ -48,4 +48,9 @@ public interface OnHeadsUpChangedListener {
* @param isHeadsUp whether the notification is now a headsUp notification
*/
default void onHeadsUpStateChanged(@NonNull NotificationEntry entry, boolean isHeadsUp) {}
+
+ /**
+ * Called on HUN disappearing animation ends
+ */
+ default void onHeadsUpAnimatingAwayEnded(@NonNull NotificationEntry entry) {}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index c36a046532bd..3df4a677b9e5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -20,6 +20,7 @@ import static com.android.server.notification.Flags.FLAG_SCREENSHARE_NOTIFICATIO
import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer;
import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
import static com.android.systemui.statusbar.StatusBarState.SHADE;
+import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL;
import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_ALL;
import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
@@ -96,9 +97,12 @@ import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefac
import com.android.systemui.statusbar.notification.init.NotificationsController;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
+import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
+import com.android.systemui.statusbar.notification.shared.GroupHunAnimationFix;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController.NotificationPanelEvent;
import com.android.systemui.statusbar.notification.stack.NotificationSwipeHelper.NotificationCallback;
import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder;
+import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
import com.android.systemui.statusbar.phone.HeadsUpTouchHelper;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -192,6 +196,8 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase {
private NotificationStackScrollLayoutController mController;
+ private NotificationTestHelper mNotificationTestHelper;
+
@Before
public void setUp() {
allowTestableLooperAsMainThread();
@@ -199,6 +205,11 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase {
when(mNotificationSwipeHelperBuilder.build()).thenReturn(mNotificationSwipeHelper);
when(mKeyguardTransitionRepo.getTransitions()).thenReturn(emptyFlow());
+ mNotificationTestHelper = new NotificationTestHelper(
+ mContext,
+ mDependency,
+ TestableLooper.get(this));
+ mNotificationTestHelper.setDefaultInflationFlags(FLAG_CONTENT_VIEW_ALL);
}
@Test
@@ -222,6 +233,42 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase {
}
@Test
+ @EnableFlags(GroupHunAnimationFix.FLAG_NAME)
+ public void changeHeadsUpAnimatingAwayToTrue_onEntryAnimatingAwayEndedNotCalled()
+ throws Exception {
+ // Before: bind an ExpandableNotificationRow,
+ initController(/* viewIsAttached= */ true);
+ mController.setHeadsUpAppearanceController(mock(HeadsUpAppearanceController.class));
+ NotificationListContainer listContainer = mController.getNotificationListContainer();
+ ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+ listContainer.bindRow(row);
+
+ // When: call setHeadsUpAnimatingAway to change set mHeadsupDisappearRunning to true
+ row.setHeadsUpAnimatingAway(true);
+
+ // Then: mHeadsUpManager.onEntryAnimatingAwayEnded is not called
+ verify(mHeadsUpManager, never()).onEntryAnimatingAwayEnded(row.getEntry());
+ }
+
+ @Test
+ @EnableFlags(GroupHunAnimationFix.FLAG_NAME)
+ public void changeHeadsUpAnimatingAwayToFalse_onEntryAnimatingAwayEndedCalled()
+ throws Exception {
+ // Before: bind an ExpandableNotificationRow, set its mHeadsupDisappearRunning to true
+ initController(/* viewIsAttached= */ true);
+ mController.setHeadsUpAppearanceController(mock(HeadsUpAppearanceController.class));
+ NotificationListContainer listContainer = mController.getNotificationListContainer();
+ ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+ listContainer.bindRow(row);
+ row.setHeadsUpAnimatingAway(true);
+
+ // When: call setHeadsUpAnimatingAway to change set mHeadsupDisappearRunning to false
+ row.setHeadsUpAnimatingAway(false);
+
+ // Then: mHeadsUpManager.onEntryAnimatingAwayEnded is called
+ verify(mHeadsUpManager).onEntryAnimatingAwayEnded(row.getEntry());
+ }
+ @Test
public void testOnDensityOrFontScaleChanged_reInflatesFooterViews() {
initController(/* viewIsAttached= */ true);