| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.systemui.statusbar.notification.collection.coordinator |
| |
| import android.app.Notification |
| import android.app.Notification.GROUP_ALERT_SUMMARY |
| import android.util.ArrayMap |
| import com.android.systemui.dagger.qualifiers.Main |
| import com.android.systemui.statusbar.NotificationRemoteInputManager |
| import com.android.systemui.statusbar.notification.collection.GroupEntry |
| import com.android.systemui.statusbar.notification.collection.ListEntry |
| import com.android.systemui.statusbar.notification.collection.NotifPipeline |
| import com.android.systemui.statusbar.notification.collection.NotificationEntry |
| import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope |
| import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator |
| import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter |
| import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner |
| import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener |
| import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender |
| import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender.OnEndLifetimeExtensionCallback |
| import com.android.systemui.statusbar.notification.collection.render.NodeController |
| import com.android.systemui.statusbar.notification.dagger.IncomingHeader |
| import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder |
| import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider |
| import com.android.systemui.statusbar.notification.stack.BUCKET_HEADS_UP |
| import com.android.systemui.statusbar.policy.HeadsUpManager |
| import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener |
| import com.android.systemui.util.concurrency.DelayableExecutor |
| import com.android.systemui.util.time.SystemClock |
| 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. |
| * |
| * The current HUN, but not HUNs that are queued to heads up, will be: |
| * - Lifetime extended until it's no longer heads upping. |
| * - Promoted out of its group if it's a child of a group. |
| * - In the HeadsUpCoordinatorSection. Ordering is configured in [NotifCoordinators]. |
| * - Removed from HeadsUpManager if it's removed from the NotificationCollection. |
| * |
| * Note: The inflation callback in [PreparationCoordinator] handles showing HUNs. |
| */ |
| @CoordinatorScope |
| class HeadsUpCoordinator @Inject constructor( |
| private val mLogger: HeadsUpCoordinatorLogger, |
| private val mSystemClock: SystemClock, |
| private val mHeadsUpManager: HeadsUpManager, |
| private val mHeadsUpViewBinder: HeadsUpViewBinder, |
| private val mNotificationInterruptStateProvider: NotificationInterruptStateProvider, |
| private val mRemoteInputManager: NotificationRemoteInputManager, |
| @IncomingHeader private val mIncomingHeaderController: NodeController, |
| @Main private val mExecutor: DelayableExecutor, |
| ) : Coordinator { |
| private val mEntriesBindingUntil = ArrayMap<String, Long>() |
| private var mEndLifetimeExtension: OnEndLifetimeExtensionCallback? = null |
| private lateinit var mNotifPipeline: NotifPipeline |
| private var mNow: Long = -1 |
| private val mPostedEntries = LinkedHashMap<String, PostedEntry>() |
| |
| // notifs we've extended the lifetime for with cancellation callbacks |
| private val mNotifsExtendingLifetime = ArrayMap<NotificationEntry, Runnable?>() |
| |
| override fun attach(pipeline: NotifPipeline) { |
| mNotifPipeline = pipeline |
| mHeadsUpManager.addListener(mOnHeadsUpChangedListener) |
| pipeline.addCollectionListener(mNotifCollectionListener) |
| pipeline.addOnBeforeTransformGroupsListener(::onBeforeTransformGroups) |
| pipeline.addOnBeforeFinalizeFilterListener(::onBeforeFinalizeFilter) |
| pipeline.addPromoter(mNotifPromoter) |
| pipeline.addNotificationLifetimeExtender(mLifetimeExtender) |
| mRemoteInputManager.addActionPressListener(mActionPressListener) |
| } |
| |
| private fun onHeadsUpViewBound(entry: NotificationEntry) { |
| mHeadsUpManager.showNotification(entry) |
| mEntriesBindingUntil.remove(entry.key) |
| } |
| |
| /** |
| * 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 alert edge case. |
| */ |
| fun onBeforeTransformGroups(list: List<ListEntry>) { |
| mNow = mSystemClock.currentTimeMillis() |
| if (mPostedEntries.isEmpty()) { |
| return |
| } |
| // Process all non-group adds/updates |
| mHeadsUpManager.modifyHuns { hunMutator -> |
| mPostedEntries.values.toList().forEach { posted -> |
| if (!posted.entry.sbn.isGroup) { |
| handlePostedEntry(posted, hunMutator, "non-group") |
| mPostedEntries.remove(posted.key) |
| } |
| } |
| } |
| } |
| |
| /** |
| * Once we have a nearly final shade list (not including what's pruned for inflation reasons), |
| * 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 alert 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 alert. |
| 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 alert 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 alert to transfer |
| if (!isGoingToShowHunStrict(logicalSummary)) { |
| postedEntries.forEach { |
| handlePostedEntry(it, hunMutator, scenario = "logical-summary-not-alerting") |
| } |
| return@forEach |
| } |
| |
| // The group is alerting! Overall goals: |
| // - Maybe transfer its alert to a child |
| // - Also let any/all newly alerting children still alert |
| var childToReceiveParentAlert: NotificationEntry? |
| var targetType = "undefined" |
| |
| // If the parent is alerting, 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 alert. |
| childToReceiveParentAlert = |
| findAlertOverride(postedEntries, groupLocationsByKey::getLocation) |
| if (childToReceiveParentAlert != null) { |
| targetType = "alertOverride" |
| } |
| |
| // If the summary is Detached and we have not picked a receiver of the alert, then we |
| // need to look for the best child to alert in place of the summary. |
| val isSummaryAttached = groupLocationsByKey.contains(logicalSummary.key) |
| if (!isSummaryAttached && childToReceiveParentAlert == null) { |
| childToReceiveParentAlert = |
| findBestTransferChild(logicalMembers, groupLocationsByKey::getLocation) |
| if (childToReceiveParentAlert != null) { |
| targetType = "bestChild" |
| } |
| } |
| |
| // If there is no child to receive the parent alert, then just handle the posted entries |
| // and return. |
| if (childToReceiveParentAlert == 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] |
| |
| // If the summary was not attached, then remove the alert 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, |
| isAlerting = mHeadsUpManager.isAlerting(logicalSummary.key), |
| isBinding = isEntryBinding(logicalSummary), |
| ) |
| // If we transfer the alert and the summary isn't even attached, that means we |
| // should ensure the summary is no longer alerting, so we remove it here. |
| handlePostedEntry( |
| summaryUpdateForRemoval, |
| hunMutator, |
| scenario = "detached-summary-remove-alert") |
| } else if (summaryUpdate != null) { |
| mLogger.logPostedEntryWillNotEvaluate( |
| summaryUpdate, |
| reason = "attached-summary-transferred") |
| } |
| |
| // Handle all posted entries -- if the child receiving the parent's alert is in the |
| // list, then set its flags to ensure it alerts. |
| var didAlertChildToReceiveParentAlert = false |
| postedEntries.asSequence() |
| .filter { it.key != logicalSummary.key } |
| .forEach { postedEntry -> |
| if (childToReceiveParentAlert.key == postedEntry.key) { |
| // Update the child's posted update so that it |
| postedEntry.shouldHeadsUpEver = true |
| postedEntry.shouldHeadsUpAgain = true |
| handlePostedEntry( |
| postedEntry, |
| hunMutator, |
| scenario = "child-alert-transfer-target-$targetType") |
| didAlertChildToReceiveParentAlert = true |
| } else { |
| handlePostedEntry( |
| postedEntry, |
| hunMutator, |
| scenario = "child-alert-non-target") |
| } |
| } |
| |
| // If the child receiving the alert was not updated on this tick (which can happen in a |
| // standard alert transfer scenario), then construct an update so that we can apply it. |
| if (!didAlertChildToReceiveParentAlert) { |
| val posted = PostedEntry( |
| childToReceiveParentAlert, |
| wasAdded = false, |
| wasUpdated = false, |
| shouldHeadsUpEver = true, |
| shouldHeadsUpAgain = true, |
| isAlerting = mHeadsUpManager.isAlerting(childToReceiveParentAlert.key), |
| isBinding = isEntryBinding(childToReceiveParentAlert), |
| ) |
| handlePostedEntry( |
| posted, |
| hunMutator, |
| scenario = "non-posted-child-alert-transfer-target-$targetType") |
| } |
| } |
| // After this method runs, all posted entries should have been handled (or skipped). |
| mPostedEntries.clear() |
| } |
| |
| /** |
| * Find the posted child with the newest when, and return it if it is isolated and has |
| * GROUP_ALERT_SUMMARY so that it can be alerted. |
| */ |
| private fun findAlertOverride( |
| postedEntries: List<PostedEntry>, |
| locationLookupByKey: (String) -> GroupLocation, |
| ): NotificationEntry? = postedEntries.asSequence() |
| .filter { posted -> !posted.entry.sbn.notification.isGroupSummary } |
| .sortedBy { posted -> -posted.entry.sbn.notification.`when` } |
| .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' |
| */ |
| 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.`when` }, |
| )) |
| .firstOrNull() |
| |
| private fun getGroupLocationsByKey(list: List<ListEntry>): Map<String, GroupLocation> = |
| mutableMapOf<String, GroupLocation>().also { map -> |
| list.forEach { topLevelEntry -> |
| when (topLevelEntry) { |
| is NotificationEntry -> map[topLevelEntry.key] = GroupLocation.Isolated |
| is GroupEntry -> { |
| topLevelEntry.summary?.let { summary -> |
| map[summary.key] = GroupLocation.Summary |
| } |
| topLevelEntry.children.forEach { child -> |
| map[child.key] = GroupLocation.Child |
| } |
| } |
| else -> error("unhandled type $topLevelEntry") |
| } |
| } |
| } |
| |
| private fun handlePostedEntry(posted: PostedEntry, hunMutator: HunMutator, scenario: String) { |
| mLogger.logPostedEntryWillEvaluate(posted, scenario) |
| if (posted.wasAdded) { |
| if (posted.shouldHeadsUpEver) { |
| bindForAsyncHeadsUp(posted) |
| } |
| } else { |
| if (posted.isHeadsUpAlready) { |
| // NOTE: This might be because we're alerting (i.e. tracked by HeadsUpManager) OR |
| // it could be because we're binding, and that will affect the next step. |
| if (posted.shouldHeadsUpEver) { |
| // If alerting, we need to post an update. Otherwise we're still binding, |
| // and we can just let that finish. |
| if (posted.isAlerting) { |
| hunMutator.updateNotification(posted.key, posted.shouldHeadsUpAgain) |
| } |
| } else { |
| if (posted.isAlerting) { |
| // We don't want this to be interrupting anymore, let's remove it |
| hunMutator.removeNotification(posted.key, false /*removeImmediately*/) |
| } else { |
| // Don't let the bind finish |
| cancelHeadsUpBind(posted.entry) |
| } |
| } |
| } else if (posted.shouldHeadsUpEver && posted.shouldHeadsUpAgain) { |
| // This notification was updated to be heads up, show it! |
| bindForAsyncHeadsUp(posted) |
| } |
| } |
| } |
| |
| private fun cancelHeadsUpBind(entry: NotificationEntry) { |
| mEntriesBindingUntil.remove(entry.key) |
| mHeadsUpViewBinder.abortBindCallback(entry) |
| } |
| |
| private fun bindForAsyncHeadsUp(posted: PostedEntry) { |
| // TODO: Add a guarantee to bindHeadsUpView of some kind of callback if the bind is |
| // cancelled so that we don't need to have this sad timeout hack. |
| mEntriesBindingUntil[posted.key] = mNow + BIND_TIMEOUT |
| 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) { |
| // shouldHeadsUp includes check for whether this notification should be filtered |
| val shouldHeadsUpEver = mNotificationInterruptStateProvider.shouldHeadsUp(entry) |
| mPostedEntries[entry.key] = PostedEntry( |
| entry, |
| wasAdded = true, |
| wasUpdated = false, |
| shouldHeadsUpEver = shouldHeadsUpEver, |
| shouldHeadsUpAgain = true, |
| isAlerting = 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 alert once, don't heads |
| * up again. |
| */ |
| override fun onEntryUpdated(entry: NotificationEntry) { |
| val shouldHeadsUpEver = mNotificationInterruptStateProvider.shouldHeadsUp(entry) |
| val shouldHeadsUpAgain = shouldHunAgain(entry) |
| val isAlerting = mHeadsUpManager.isAlerting(entry.key) |
| val isBinding = isEntryBinding(entry) |
| val posted = mPostedEntries.compute(entry.key) { _, value -> |
| value?.also { update -> |
| update.wasUpdated = true |
| update.shouldHeadsUpEver = update.shouldHeadsUpEver || shouldHeadsUpEver |
| update.shouldHeadsUpAgain = update.shouldHeadsUpAgain || shouldHeadsUpAgain |
| update.isAlerting = isAlerting |
| update.isBinding = isBinding |
| } ?: PostedEntry( |
| entry, |
| wasAdded = false, |
| wasUpdated = true, |
| shouldHeadsUpEver = shouldHeadsUpEver, |
| shouldHeadsUpAgain = shouldHeadsUpAgain, |
| isAlerting = isAlerting, |
| isBinding = isBinding, |
| ) |
| } |
| // Handle cancelling alerts 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.isAlerting) { |
| // 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) |
| } |
| } |
| } |
| |
| /** |
| * Stop alerting HUNs that are removed from the notification collection |
| */ |
| override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { |
| mPostedEntries.remove(entry.key) |
| cancelHeadsUpBind(entry) |
| val entryKey = entry.key |
| if (mHeadsUpManager.isAlerting(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) |
| } |
| } |
| |
| /** |
| * Checks whether an update for a notification warrants an alert for the user. |
| */ |
| private fun shouldHunAgain(entry: NotificationEntry): Boolean { |
| return (!entry.hasInterrupted() || |
| (entry.sbn.notification.flags and Notification.FLAG_ONLY_ALERT_ONCE) == 0) |
| } |
| |
| /** When an action is pressed on a notification, end HeadsUp lifetime extension. */ |
| private val mActionPressListener = Consumer<NotificationEntry> { entry -> |
| if (mNotifsExtendingLifetime.contains(entry)) { |
| val removeInMillis = mHeadsUpManager.getEarliestRemovalTime(entry.key) |
| mExecutor.executeDelayed({ endNotifLifetimeExtensionIfExtended(entry) }, removeInMillis) |
| } |
| } |
| |
| private val mLifetimeExtender = object : NotifLifetimeExtender { |
| override fun getName() = TAG |
| |
| override fun setCallback(callback: OnEndLifetimeExtensionCallback) { |
| mEndLifetimeExtension = callback |
| } |
| |
| 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 |
| } |
| return true |
| } |
| |
| override fun cancelLifetimeExtension(entry: NotificationEntry) { |
| mNotifsExtendingLifetime.remove(entry)?.run() |
| } |
| } |
| |
| 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) |
| |
| 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 |
| } |
| |
| private val mOnHeadsUpChangedListener = object : OnHeadsUpChangedListener { |
| override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) { |
| if (!isHeadsUp) { |
| mNotifPromoter.invalidateList() |
| mHeadsUpViewBinder.unbindHeadsUpView(entry) |
| endNotifLifetimeExtensionIfExtended(entry) |
| } |
| } |
| } |
| |
| private fun isSticky(entry: NotificationEntry) = mHeadsUpManager.isSticky(entry.key) |
| |
| private fun isEntryBinding(entry: ListEntry): Boolean { |
| val bindingUntil = mEntriesBindingUntil[entry.key] |
| return bindingUntil != null && bindingUntil >= mNow |
| } |
| |
| /** |
| * Whether the notification is already alerting or binding so that it can imminently alert |
| */ |
| private fun isAttemptingToShowHun(entry: ListEntry) = |
| mHeadsUpManager.isAlerting(entry.key) || isEntryBinding(entry) |
| |
| /** |
| * Whether the notification is already alerting/binding per [isAttemptingToShowHun] OR if it |
| * has been updated so that it should alert this update. This method is permissive because it |
| * returns `true` even if the update would (in isolation of its group) cause the alert to be |
| * retracted. This is important for not retracting transferred group alerts. |
| */ |
| 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 alerting/binding state of [isAttemptingToShowHun]. This method is |
| * strict because any update which would revoke the alert supersedes the current |
| * alerting/binding state. |
| */ |
| private fun isGoingToShowHunStrict(entry: ListEntry) = |
| mPostedEntries[entry.key]?.calculateShouldBeHeadsUpStrict ?: isAttemptingToShowHun(entry) |
| |
| private fun endNotifLifetimeExtensionIfExtended(entry: NotificationEntry) { |
| if (mNotifsExtendingLifetime.contains(entry)) { |
| mNotifsExtendingLifetime.remove(entry)?.run() |
| mEndLifetimeExtension?.onEndLifetimeExtension(mLifetimeExtender, entry) |
| } |
| } |
| |
| companion object { |
| private const val TAG = "HeadsUpCoordinator" |
| private const val BIND_TIMEOUT = 1000L |
| } |
| |
| data class PostedEntry( |
| val entry: NotificationEntry, |
| val wasAdded: Boolean, |
| var wasUpdated: Boolean, |
| var shouldHeadsUpEver: Boolean, |
| var shouldHeadsUpAgain: Boolean, |
| var isAlerting: Boolean, |
| var isBinding: Boolean, |
| ) { |
| val key = entry.key |
| val isHeadsUpAlready: Boolean |
| get() = isAlerting || 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 fun Map<String, GroupLocation>.getLocation(key: String): GroupLocation = |
| getOrDefault(key, GroupLocation.Detached) |
| |
| /** |
| * Invokes the given block with a [HunMutator] that defers all HUN removals. This ensures that the |
| * HeadsUpManager is notified of additions before removals, which prevents a glitch where the |
| * HeadsUpManager temporarily believes that nothing is alerting, causing bad re-entrant behavior. |
| */ |
| private fun <R> HeadsUpManager.modifyHuns(block: (HunMutator) -> R): R { |
| val mutator = HunMutatorImpl(this) |
| return block(mutator).also { mutator.commitModifications() } |
| } |
| |
| /** Mutates the HeadsUp state of notifications. */ |
| private interface HunMutator { |
| fun updateNotification(key: String, alert: Boolean) |
| fun removeNotification(key: String, releaseImmediately: Boolean) |
| } |
| |
| /** |
| * [HunMutator] implementation that defers removing notifications from the HeadsUpManager until |
| * after additions/updates. |
| */ |
| private class HunMutatorImpl(private val headsUpManager: HeadsUpManager) : HunMutator { |
| private val deferred = mutableListOf<Pair<String, Boolean>>() |
| |
| override fun updateNotification(key: String, alert: Boolean) { |
| headsUpManager.updateNotification(key, alert) |
| } |
| |
| override fun removeNotification(key: String, releaseImmediately: Boolean) { |
| val args = Pair(key, releaseImmediately) |
| deferred.add(args) |
| } |
| |
| fun commitModifications() { |
| deferred.forEach { (key, releaseImmediately) -> |
| headsUpManager.removeNotification(key, releaseImmediately) |
| } |
| deferred.clear() |
| } |
| } |