diff options
| author | 2024-07-26 17:02:26 +0000 | |
|---|---|---|
| committer | 2024-07-26 17:02:26 +0000 | |
| commit | ee927d35f2d9c801803c311cab2346d71940fc6c (patch) | |
| tree | cd348acfb880430ce013b52ff955e3b5e8405292 | |
| parent | 5396e5f0ebb9100f3ca9610672b07ae4a13a299b (diff) | |
| parent | d8fcc9dc2b2665a4c6eacb7892852cd524aea4ed (diff) | |
Merge changes I819a942d,Ibb4899d8 into main
* changes:
Format HeadsUpCoordinator
Invalidate the pipeline list after HUN disappearing animation ends
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); |