• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.systemui.statusbar.notification.collection.coordinator
17 
18 import android.app.Notification
19 import android.app.Notification.GROUP_ALERT_SUMMARY
20 import android.util.ArrayMap
21 import android.util.ArraySet
22 import com.android.internal.annotations.VisibleForTesting
23 import com.android.systemui.dagger.qualifiers.Main
24 import com.android.systemui.statusbar.NotificationRemoteInputManager
25 import com.android.systemui.statusbar.notification.NotifPipelineFlags
26 import com.android.systemui.statusbar.notification.collection.GroupEntry
27 import com.android.systemui.statusbar.notification.collection.ListEntry
28 import com.android.systemui.statusbar.notification.collection.NotifPipeline
29 import com.android.systemui.statusbar.notification.collection.NotificationEntry
30 import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
31 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator
32 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter
33 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
34 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
35 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender
36 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender.OnEndLifetimeExtensionCallback
37 import com.android.systemui.statusbar.notification.collection.provider.LaunchFullScreenIntentProvider
38 import com.android.systemui.statusbar.notification.collection.render.NodeController
39 import com.android.systemui.statusbar.notification.dagger.IncomingHeader
40 import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder
41 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider
42 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider.FullScreenIntentDecision
43 import com.android.systemui.statusbar.notification.logKey
44 import com.android.systemui.statusbar.notification.stack.BUCKET_HEADS_UP
45 import com.android.systemui.statusbar.policy.HeadsUpManager
46 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener
47 import com.android.systemui.util.concurrency.DelayableExecutor
48 import com.android.systemui.util.time.SystemClock
49 import java.util.function.Consumer
50 import javax.inject.Inject
51 
52 /**
53  * Coordinates heads up notification (HUN) interactions with the notification pipeline based on
54  * the HUN state reported by the [HeadsUpManager]. In this class we only consider one
55  * notification, in particular the [HeadsUpManager.getTopEntry], to be HeadsUpping at a
56  * time even though other notifications may be queued to heads up next.
57  *
58  * The current HUN, but not HUNs that are queued to heads up, will be:
59  * - Lifetime extended until it's no longer heads upping.
60  * - Promoted out of its group if it's a child of a group.
61  * - In the HeadsUpCoordinatorSection. Ordering is configured in [NotifCoordinators].
62  * - Removed from HeadsUpManager if it's removed from the NotificationCollection.
63  *
64  * Note: The inflation callback in [PreparationCoordinator] handles showing HUNs.
65  */
66 @CoordinatorScope
67 class HeadsUpCoordinator @Inject constructor(
68     private val mLogger: HeadsUpCoordinatorLogger,
69     private val mSystemClock: SystemClock,
70     private val mHeadsUpManager: HeadsUpManager,
71     private val mHeadsUpViewBinder: HeadsUpViewBinder,
72     private val mNotificationInterruptStateProvider: NotificationInterruptStateProvider,
73     private val mRemoteInputManager: NotificationRemoteInputManager,
74     private val mLaunchFullScreenIntentProvider: LaunchFullScreenIntentProvider,
75     private val mFlags: NotifPipelineFlags,
76     @IncomingHeader private val mIncomingHeaderController: NodeController,
77     @Main private val mExecutor: DelayableExecutor,
78 ) : Coordinator {
79     private val mEntriesBindingUntil = ArrayMap<String, Long>()
80     private val mEntriesUpdateTimes = ArrayMap<String, Long>()
81     private val mFSIUpdateCandidates = ArrayMap<String, Long>()
82     private var mEndLifetimeExtension: OnEndLifetimeExtensionCallback? = null
83     private lateinit var mNotifPipeline: NotifPipeline
84     private var mNow: Long = -1
85     private val mPostedEntries = LinkedHashMap<String, PostedEntry>()
86 
87     // notifs we've extended the lifetime for with cancellation callbacks
88     private val mNotifsExtendingLifetime = ArrayMap<NotificationEntry, Runnable?>()
89 
90     override fun attach(pipeline: NotifPipeline) {
91         mNotifPipeline = pipeline
92         mHeadsUpManager.addListener(mOnHeadsUpChangedListener)
93         pipeline.addCollectionListener(mNotifCollectionListener)
94         pipeline.addOnBeforeTransformGroupsListener(::onBeforeTransformGroups)
95         pipeline.addOnBeforeFinalizeFilterListener(::onBeforeFinalizeFilter)
96         pipeline.addPromoter(mNotifPromoter)
97         pipeline.addNotificationLifetimeExtender(mLifetimeExtender)
98         mRemoteInputManager.addActionPressListener(mActionPressListener)
99     }
100 
101     private fun onHeadsUpViewBound(entry: NotificationEntry) {
102         mHeadsUpManager.showNotification(entry)
103         mEntriesBindingUntil.remove(entry.key)
104     }
105 
106     /**
107      * Once the pipeline starts running, we can look through posted entries and quickly process
108      * any that don't have groups, and thus will never gave a group alert edge case.
109      */
110     fun onBeforeTransformGroups(list: List<ListEntry>) {
111         mNow = mSystemClock.currentTimeMillis()
112         if (mPostedEntries.isEmpty()) {
113             return
114         }
115         // Process all non-group adds/updates
116         mHeadsUpManager.modifyHuns { hunMutator ->
117             mPostedEntries.values.toList().forEach { posted ->
118                 if (!posted.entry.sbn.isGroup) {
119                     handlePostedEntry(posted, hunMutator, "non-group")
120                     mPostedEntries.remove(posted.key)
121                 }
122             }
123         }
124     }
125 
126     /**
127      * Once we have a nearly final shade list (not including what's pruned for inflation reasons),
128      * we know that stability and [NotifPromoter]s have been applied, so we can use the location of
129      * notifications in this list to determine what kind of group alert behavior should happen.
130      */
131     fun onBeforeFinalizeFilter(list: List<ListEntry>) = mHeadsUpManager.modifyHuns { hunMutator ->
132         // Nothing to do if there are no other adds/updates
133         if (mPostedEntries.isEmpty()) {
134             return@modifyHuns
135         }
136         // Calculate a bunch of information about the logical group and the locations of group
137         // entries in the nearly-finalized shade list.  These may be used in the per-group loop.
138         val postedEntriesByGroup = mPostedEntries.values.groupBy { it.entry.sbn.groupKey }
139         val logicalMembersByGroup = mNotifPipeline.allNotifs.asSequence()
140             .filter { postedEntriesByGroup.contains(it.sbn.groupKey) }
141             .groupBy { it.sbn.groupKey }
142         val groupLocationsByKey: Map<String, GroupLocation> by lazy { getGroupLocationsByKey(list) }
143         mLogger.logEvaluatingGroups(postedEntriesByGroup.size)
144         // For each group, determine which notification(s) for a group should alert.
145         postedEntriesByGroup.forEach { (groupKey, postedEntries) ->
146             // get and classify the logical members
147             val logicalMembers = logicalMembersByGroup[groupKey] ?: emptyList()
148             val logicalSummary = logicalMembers.find { it.sbn.notification.isGroupSummary }
149 
150             // Report the start of this group's evaluation
151             mLogger.logEvaluatingGroup(groupKey, postedEntries.size, logicalMembers.size)
152 
153             // If there is no logical summary, then there is no alert to transfer
154             if (logicalSummary == null) {
155                 postedEntries.forEach {
156                     handlePostedEntry(it, hunMutator, scenario = "logical-summary-missing")
157                 }
158                 return@forEach
159             }
160 
161             // If summary isn't wanted to be heads up, then there is no alert to transfer
162             if (!isGoingToShowHunStrict(logicalSummary)) {
163                 postedEntries.forEach {
164                     handlePostedEntry(it, hunMutator, scenario = "logical-summary-not-alerting")
165                 }
166                 return@forEach
167             }
168 
169             // The group is alerting! Overall goals:
170             //  - Maybe transfer its alert to a child
171             //  - Also let any/all newly alerting children still alert
172             var childToReceiveParentAlert: NotificationEntry?
173             var targetType = "undefined"
174 
175             // If the parent is alerting, always look at the posted notification with the newest
176             // 'when', and if it is isolated with GROUP_ALERT_SUMMARY, then it should receive the
177             // parent's alert.
178             childToReceiveParentAlert =
179                 findAlertOverride(postedEntries, groupLocationsByKey::getLocation)
180             if (childToReceiveParentAlert != null) {
181                 targetType = "alertOverride"
182             }
183 
184             // If the summary is Detached and we have not picked a receiver of the alert, then we
185             // need to look for the best child to alert in place of the summary.
186             val isSummaryAttached = groupLocationsByKey.contains(logicalSummary.key)
187             if (!isSummaryAttached && childToReceiveParentAlert == null) {
188                 childToReceiveParentAlert =
189                     findBestTransferChild(logicalMembers, groupLocationsByKey::getLocation)
190                 if (childToReceiveParentAlert != null) {
191                     targetType = "bestChild"
192                 }
193             }
194 
195             // If there is no child to receive the parent alert, then just handle the posted entries
196             // and return.
197             if (childToReceiveParentAlert == null) {
198                 postedEntries.forEach {
199                     handlePostedEntry(it, hunMutator, scenario = "no-transfer-target")
200                 }
201                 return@forEach
202             }
203 
204             // At this point we just need to initiate the transfer
205             val summaryUpdate = mPostedEntries[logicalSummary.key]
206 
207             // Because we now know for certain that some child is going to alert for this summary
208             // (as we have found a child to transfer the alert to), mark the group as having
209             // interrupted. This will allow us to know in the future that the "should heads up"
210             // state of this group has already been handled, just not via the summary entry itself.
211             logicalSummary.setInterruption()
212             mLogger.logSummaryMarkedInterrupted(logicalSummary.key, childToReceiveParentAlert.key)
213 
214             // If the summary was not attached, then remove the alert from the detached summary.
215             // Otherwise we can simply ignore its posted update.
216             if (!isSummaryAttached) {
217                 val summaryUpdateForRemoval = summaryUpdate?.also {
218                     it.shouldHeadsUpEver = false
219                 } ?: PostedEntry(
220                         logicalSummary,
221                         wasAdded = false,
222                         wasUpdated = false,
223                         shouldHeadsUpEver = false,
224                         shouldHeadsUpAgain = false,
225                         isAlerting = mHeadsUpManager.isAlerting(logicalSummary.key),
226                         isBinding = isEntryBinding(logicalSummary),
227                 )
228                 // If we transfer the alert and the summary isn't even attached, that means we
229                 // should ensure the summary is no longer alerting, so we remove it here.
230                 handlePostedEntry(
231                         summaryUpdateForRemoval,
232                         hunMutator,
233                         scenario = "detached-summary-remove-alert")
234             } else if (summaryUpdate != null) {
235                 mLogger.logPostedEntryWillNotEvaluate(
236                         summaryUpdate,
237                         reason = "attached-summary-transferred")
238             }
239 
240             // Handle all posted entries -- if the child receiving the parent's alert is in the
241             // list, then set its flags to ensure it alerts.
242             var didAlertChildToReceiveParentAlert = false
243             postedEntries.asSequence()
244                     .filter { it.key != logicalSummary.key }
245                     .forEach { postedEntry ->
246                         if (childToReceiveParentAlert.key == postedEntry.key) {
247                             // Update the child's posted update so that it
248                             postedEntry.shouldHeadsUpEver = true
249                             postedEntry.shouldHeadsUpAgain = true
250                             handlePostedEntry(
251                                     postedEntry,
252                                     hunMutator,
253                                     scenario = "child-alert-transfer-target-$targetType")
254                             didAlertChildToReceiveParentAlert = true
255                         } else {
256                             handlePostedEntry(
257                                     postedEntry,
258                                     hunMutator,
259                                     scenario = "child-alert-non-target")
260                         }
261                     }
262 
263             // If the child receiving the alert was not updated on this tick (which can happen in a
264             // standard alert transfer scenario), then construct an update so that we can apply it.
265             if (!didAlertChildToReceiveParentAlert) {
266                 val posted = PostedEntry(
267                         childToReceiveParentAlert,
268                         wasAdded = false,
269                         wasUpdated = false,
270                         shouldHeadsUpEver = true,
271                         shouldHeadsUpAgain = true,
272                         isAlerting = mHeadsUpManager.isAlerting(childToReceiveParentAlert.key),
273                         isBinding = isEntryBinding(childToReceiveParentAlert),
274                 )
275                 handlePostedEntry(
276                         posted,
277                         hunMutator,
278                         scenario = "non-posted-child-alert-transfer-target-$targetType")
279             }
280         }
281         // After this method runs, all posted entries should have been handled (or skipped).
282         mPostedEntries.clear()
283 
284         // Also take this opportunity to clean up any stale entry update times
285         cleanUpEntryTimes()
286     }
287 
288     /**
289      * Find the posted child with the newest when, and return it if it is isolated and has
290      * GROUP_ALERT_SUMMARY so that it can be alerted.
291      */
292     private fun findAlertOverride(
293         postedEntries: List<PostedEntry>,
294         locationLookupByKey: (String) -> GroupLocation,
295     ): NotificationEntry? = postedEntries.asSequence()
296         .filter { posted -> !posted.entry.sbn.notification.isGroupSummary }
297         .sortedBy { posted -> -posted.entry.sbn.notification.`when` }
298         .firstOrNull()
299         ?.let { posted ->
300             posted.entry.takeIf { entry ->
301                 locationLookupByKey(entry.key) == GroupLocation.Isolated &&
302                         entry.sbn.notification.groupAlertBehavior == GROUP_ALERT_SUMMARY
303             }
304         }
305 
306     /**
307      * Of children which are attached, look for the child to receive the notification:
308      * First prefer children which were updated, then looking for the ones with the newest 'when'
309      */
310     private fun findBestTransferChild(
311         logicalMembers: List<NotificationEntry>,
312         locationLookupByKey: (String) -> GroupLocation,
313     ): NotificationEntry? = logicalMembers.asSequence()
314         .filter { !it.sbn.notification.isGroupSummary }
315         .filter { locationLookupByKey(it.key) != GroupLocation.Detached }
316         .sortedWith(compareBy(
317             { !mPostedEntries.contains(it.key) },
318             { -it.sbn.notification.`when` },
319         ))
320         .firstOrNull()
321 
322     private fun getGroupLocationsByKey(list: List<ListEntry>): Map<String, GroupLocation> =
323         mutableMapOf<String, GroupLocation>().also { map ->
324             list.forEach { topLevelEntry ->
325                 when (topLevelEntry) {
326                     is NotificationEntry -> map[topLevelEntry.key] = GroupLocation.Isolated
327                     is GroupEntry -> {
328                         topLevelEntry.summary?.let { summary ->
329                             map[summary.key] = GroupLocation.Summary
330                         }
331                         topLevelEntry.children.forEach { child ->
332                             map[child.key] = GroupLocation.Child
333                         }
334                     }
335                     else -> error("unhandled type $topLevelEntry")
336                 }
337             }
338         }
339 
340     private fun handlePostedEntry(posted: PostedEntry, hunMutator: HunMutator, scenario: String) {
341         mLogger.logPostedEntryWillEvaluate(posted, scenario)
342         if (posted.wasAdded) {
343             if (posted.shouldHeadsUpEver) {
344                 bindForAsyncHeadsUp(posted)
345             }
346         } else {
347             if (posted.isHeadsUpAlready) {
348                 // NOTE: This might be because we're alerting (i.e. tracked by HeadsUpManager) OR
349                 // it could be because we're binding, and that will affect the next step.
350                 if (posted.shouldHeadsUpEver) {
351                     // If alerting, we need to post an update.  Otherwise we're still binding,
352                     // and we can just let that finish.
353                     if (posted.isAlerting) {
354                         hunMutator.updateNotification(posted.key, posted.shouldHeadsUpAgain)
355                     }
356                 } else {
357                     if (posted.isAlerting) {
358                         // We don't want this to be interrupting anymore, let's remove it
359                         hunMutator.removeNotification(posted.key, false /*removeImmediately*/)
360                     } else {
361                         // Don't let the bind finish
362                         cancelHeadsUpBind(posted.entry)
363                     }
364                 }
365             } else if (posted.shouldHeadsUpEver && posted.shouldHeadsUpAgain) {
366                 // This notification was updated to be heads up, show it!
367                 bindForAsyncHeadsUp(posted)
368             }
369         }
370     }
371 
372     private fun cancelHeadsUpBind(entry: NotificationEntry) {
373         mEntriesBindingUntil.remove(entry.key)
374         mHeadsUpViewBinder.abortBindCallback(entry)
375     }
376 
377     private fun bindForAsyncHeadsUp(posted: PostedEntry) {
378         // TODO: Add a guarantee to bindHeadsUpView of some kind of callback if the bind is
379         //  cancelled so that we don't need to have this sad timeout hack.
380         mEntriesBindingUntil[posted.key] = mNow + BIND_TIMEOUT
381         mHeadsUpViewBinder.bindHeadsUpView(posted.entry, this::onHeadsUpViewBound)
382     }
383 
384     private val mNotifCollectionListener = object : NotifCollectionListener {
385         /**
386          * Notification was just added and if it should heads up, bind the view and then show it.
387          */
388         override fun onEntryAdded(entry: NotificationEntry) {
389             // First check whether this notification should launch a full screen intent, and
390             // launch it if needed.
391             val fsiDecision = mNotificationInterruptStateProvider.getFullScreenIntentDecision(entry)
392             if (fsiDecision != null && fsiDecision.shouldLaunch) {
393                 mNotificationInterruptStateProvider.logFullScreenIntentDecision(entry, fsiDecision)
394                 mLaunchFullScreenIntentProvider.launchFullScreenIntent(entry)
395             } else if (mFlags.fsiOnDNDUpdate() &&
396                 fsiDecision.equals(FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)) {
397                 // If DND was the only reason this entry was suppressed, note it for potential
398                 // reconsideration on later ranking updates.
399                 addForFSIReconsideration(entry, mSystemClock.currentTimeMillis())
400             }
401 
402             // shouldHeadsUp includes check for whether this notification should be filtered
403             val shouldHeadsUpEver = mNotificationInterruptStateProvider.shouldHeadsUp(entry)
404             mPostedEntries[entry.key] = PostedEntry(
405                 entry,
406                 wasAdded = true,
407                 wasUpdated = false,
408                 shouldHeadsUpEver = shouldHeadsUpEver,
409                 shouldHeadsUpAgain = true,
410                 isAlerting = false,
411                 isBinding = false,
412             )
413 
414             // Record the last updated time for this key
415             setUpdateTime(entry, mSystemClock.currentTimeMillis())
416         }
417 
418         /**
419          * Notification could've updated to be heads up or not heads up. Even if it did update to
420          * heads up, if the notification specified that it only wants to alert once, don't heads
421          * up again.
422          */
423         override fun onEntryUpdated(entry: NotificationEntry) {
424             val shouldHeadsUpEver = mNotificationInterruptStateProvider.shouldHeadsUp(entry)
425             val shouldHeadsUpAgain = shouldHunAgain(entry)
426             val isAlerting = mHeadsUpManager.isAlerting(entry.key)
427             val isBinding = isEntryBinding(entry)
428             val posted = mPostedEntries.compute(entry.key) { _, value ->
429                 value?.also { update ->
430                     update.wasUpdated = true
431                     update.shouldHeadsUpEver = shouldHeadsUpEver
432                     update.shouldHeadsUpAgain = update.shouldHeadsUpAgain || shouldHeadsUpAgain
433                     update.isAlerting = isAlerting
434                     update.isBinding = isBinding
435                 } ?: PostedEntry(
436                     entry,
437                     wasAdded = false,
438                     wasUpdated = true,
439                     shouldHeadsUpEver = shouldHeadsUpEver,
440                     shouldHeadsUpAgain = shouldHeadsUpAgain,
441                     isAlerting = isAlerting,
442                     isBinding = isBinding,
443                 )
444             }
445             // Handle cancelling alerts here, rather than in the OnBeforeFinalizeFilter, so that
446             // work can be done before the ShadeListBuilder is run. This prevents re-entrant
447             // behavior between this Coordinator, HeadsUpManager, and VisualStabilityManager.
448             if (posted?.shouldHeadsUpEver == false) {
449                 if (posted.isAlerting) {
450                     // We don't want this to be interrupting anymore, let's remove it
451                     mHeadsUpManager.removeNotification(posted.key, false /*removeImmediately*/)
452                 } else if (posted.isBinding) {
453                     // Don't let the bind finish
454                     cancelHeadsUpBind(posted.entry)
455                 }
456             }
457 
458             // Update last updated time for this entry
459             setUpdateTime(entry, mSystemClock.currentTimeMillis())
460         }
461 
462         /**
463          * Stop alerting HUNs that are removed from the notification collection
464          */
465         override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
466             mPostedEntries.remove(entry.key)
467             mEntriesUpdateTimes.remove(entry.key)
468             cancelHeadsUpBind(entry)
469             val entryKey = entry.key
470             if (mHeadsUpManager.isAlerting(entryKey)) {
471                 // TODO: This should probably know the RemoteInputCoordinator's conditions,
472                 //  or otherwise reference that coordinator's state, rather than replicate its logic
473                 val removeImmediatelyForRemoteInput = (mRemoteInputManager.isSpinning(entryKey) &&
474                         !NotificationRemoteInputManager.FORCE_REMOTE_INPUT_HISTORY)
475                 mHeadsUpManager.removeNotification(entry.key, removeImmediatelyForRemoteInput)
476             }
477         }
478 
479         override fun onEntryCleanUp(entry: NotificationEntry) {
480             mHeadsUpViewBinder.abortBindCallback(entry)
481         }
482 
483         /**
484          * Identify notifications whose heads-up state changes when the notification rankings are
485          * updated, and have those changed notifications alert if necessary.
486          *
487          * This method will occur after any operations in onEntryAdded or onEntryUpdated, so any
488          * handling of ranking changes needs to take into account that we may have just made a
489          * PostedEntry for some of these notifications.
490          */
491         override fun onRankingApplied() {
492             // Because a ranking update may cause some notifications that are no longer (or were
493             // never) in mPostedEntries to need to alert, we need to check every notification
494             // known to the pipeline.
495             for (entry in mNotifPipeline.allNotifs) {
496                 // Only consider entries that are recent enough, since we want to apply a fairly
497                 // strict threshold for when an entry should be updated via only ranking and not an
498                 // app-provided notification update.
499                 if (!isNewEnoughForRankingUpdate(entry)) continue
500 
501                 // The only entries we consider alerting for here are entries that have never
502                 // interrupted and that now say they should heads up or FSI; if they've alerted in
503                 // the past, we don't want to incorrectly alert a second time if there wasn't an
504                 // explicit notification update.
505                 if (entry.hasInterrupted()) continue
506 
507                 // Before potentially allowing heads-up, check for any candidates for a FSI launch.
508                 // Any entry that is a candidate meets two criteria:
509                 //   - was suppressed from FSI launch only by a DND suppression
510                 //   - is within the recency window for reconsideration
511                 // If any of these entries are no longer suppressed, launch the FSI now.
512                 if (mFlags.fsiOnDNDUpdate() && isCandidateForFSIReconsideration(entry)) {
513                     val decision =
514                         mNotificationInterruptStateProvider.getFullScreenIntentDecision(entry)
515                     if (decision.shouldLaunch) {
516                         // Log both the launch of the full screen and also that this was via a
517                         // ranking update.
518                         mLogger.logEntryUpdatedToFullScreen(entry.key)
519                         mNotificationInterruptStateProvider.logFullScreenIntentDecision(
520                             entry, decision)
521                         mLaunchFullScreenIntentProvider.launchFullScreenIntent(entry)
522 
523                         // if we launch the FSI then this is no longer a candidate for HUN
524                         continue
525                     }
526                 }
527 
528                 // The cases where we should consider this notification to be updated:
529                 // - if this entry is not present in PostedEntries, and is now in a shouldHeadsUp
530                 //   state
531                 // - if it is present in PostedEntries and the previous state of shouldHeadsUp
532                 //   differs from the updated one
533                 val shouldHeadsUpEver = mNotificationInterruptStateProvider.checkHeadsUp(entry,
534                                 /* log= */ false)
535                 val postedShouldHeadsUpEver = mPostedEntries[entry.key]?.shouldHeadsUpEver ?: false
536                 val shouldUpdateEntry = postedShouldHeadsUpEver != shouldHeadsUpEver
537 
538                 if (shouldUpdateEntry) {
539                     mLogger.logEntryUpdatedByRanking(entry.key, shouldHeadsUpEver)
540                     onEntryUpdated(entry)
541                 }
542             }
543         }
544     }
545 
546     /**
547      * Checks whether an update for a notification warrants an alert for the user.
548      */
549     private fun shouldHunAgain(entry: NotificationEntry): Boolean {
550         return (!entry.hasInterrupted() ||
551                 (entry.sbn.notification.flags and Notification.FLAG_ONLY_ALERT_ONCE) == 0)
552     }
553 
554     /**
555      * Sets the updated time for the given entry to the specified time.
556      */
557     @VisibleForTesting
558     fun setUpdateTime(entry: NotificationEntry, time: Long) {
559         mEntriesUpdateTimes[entry.key] = time
560     }
561 
562     /**
563      * Add the entry to the list of entries potentially considerable for FSI ranking update, where
564      * the provided time is the time the entry was added.
565      */
566     @VisibleForTesting
567     fun addForFSIReconsideration(entry: NotificationEntry, time: Long) {
568         mFSIUpdateCandidates[entry.key] = time
569     }
570 
571     /**
572      * Checks whether the entry is new enough to be updated via ranking update.
573      * We want to avoid updating an entry too long after it was originally posted/updated when we're
574      * only reacting to a ranking change, as relevant ranking updates are expected to come in
575      * fairly soon after the posting of a notification.
576      */
577     private fun isNewEnoughForRankingUpdate(entry: NotificationEntry): Boolean {
578         // If we don't have an update time for this key, default to "too old"
579         if (!mEntriesUpdateTimes.containsKey(entry.key)) return false
580 
581         val updateTime = mEntriesUpdateTimes[entry.key] ?: return false
582         return (mSystemClock.currentTimeMillis() - updateTime) <= MAX_RANKING_UPDATE_DELAY_MS
583     }
584 
585     /**
586      * Checks whether the entry is present new enough for reconsideration for full screen launch.
587      * The time window is the same as for ranking update, but this doesn't allow a potential update
588      * to an entry with full screen intent to count for timing purposes.
589      */
590     private fun isCandidateForFSIReconsideration(entry: NotificationEntry): Boolean {
591         val addedTime = mFSIUpdateCandidates[entry.key] ?: return false
592         return (mSystemClock.currentTimeMillis() - addedTime) <= MAX_RANKING_UPDATE_DELAY_MS
593     }
594 
595     private fun cleanUpEntryTimes() {
596         // Because we won't update entries that are older than this amount of time anyway, clean
597         // up any entries that are too old to notify from both the general and FSI specific lists.
598 
599         // Anything newer than this time is still within the window.
600         val timeThreshold = mSystemClock.currentTimeMillis() - MAX_RANKING_UPDATE_DELAY_MS
601 
602         val toRemove = ArraySet<String>()
603         for ((key, updateTime) in mEntriesUpdateTimes) {
604             if (updateTime == null || timeThreshold > updateTime) {
605                 toRemove.add(key)
606             }
607         }
608         mEntriesUpdateTimes.removeAll(toRemove)
609 
610         val toRemoveForFSI = ArraySet<String>()
611         for ((key, addedTime) in mFSIUpdateCandidates) {
612             if (addedTime == null || timeThreshold > addedTime) {
613                 toRemoveForFSI.add(key)
614             }
615         }
616         mFSIUpdateCandidates.removeAll(toRemoveForFSI)
617     }
618 
619     /** When an action is pressed on a notification, end HeadsUp lifetime extension. */
620     private val mActionPressListener = Consumer<NotificationEntry> { entry ->
621         if (mNotifsExtendingLifetime.contains(entry)) {
622             val removeInMillis = mHeadsUpManager.getEarliestRemovalTime(entry.key)
623             mExecutor.executeDelayed({ endNotifLifetimeExtensionIfExtended(entry) }, removeInMillis)
624         }
625     }
626 
627     private val mLifetimeExtender = object : NotifLifetimeExtender {
628         override fun getName() = TAG
629 
630         override fun setCallback(callback: OnEndLifetimeExtensionCallback) {
631             mEndLifetimeExtension = callback
632         }
633 
634         override fun maybeExtendLifetime(entry: NotificationEntry, reason: Int): Boolean {
635             if (mHeadsUpManager.canRemoveImmediately(entry.key)) {
636                 return false
637             }
638             if (isSticky(entry)) {
639                 val removeAfterMillis = mHeadsUpManager.getEarliestRemovalTime(entry.key)
640                 mNotifsExtendingLifetime[entry] = mExecutor.executeDelayed({
641                     mHeadsUpManager.removeNotification(entry.key, /* releaseImmediately */ true)
642                 }, removeAfterMillis)
643             } else {
644                 mExecutor.execute {
645                     mHeadsUpManager.removeNotification(entry.key, /* releaseImmediately */ false)
646                 }
647                 mNotifsExtendingLifetime[entry] = null
648             }
649             return true
650         }
651 
652         override fun cancelLifetimeExtension(entry: NotificationEntry) {
653             mNotifsExtendingLifetime.remove(entry)?.run()
654         }
655     }
656 
657     private val mNotifPromoter = object : NotifPromoter(TAG) {
658         override fun shouldPromoteToTopLevel(entry: NotificationEntry): Boolean =
659             isGoingToShowHunNoRetract(entry)
660     }
661 
662     val sectioner = object : NotifSectioner("HeadsUp", BUCKET_HEADS_UP) {
663         override fun isInSection(entry: ListEntry): Boolean =
664             // TODO: This check won't notice if a child of the group is going to HUN...
665             isGoingToShowHunNoRetract(entry)
666 
667         override fun getComparator(): NotifComparator {
668             return object : NotifComparator("HeadsUp") {
669                 override fun compare(o1: ListEntry, o2: ListEntry): Int =
670                     mHeadsUpManager.compare(o1.representativeEntry, o2.representativeEntry)
671             }
672         }
673 
674         override fun getHeaderNodeController(): NodeController? =
675             // TODO: remove SHOW_ALL_SECTIONS, this redundant method, and mIncomingHeaderController
676             if (RankingCoordinator.SHOW_ALL_SECTIONS) mIncomingHeaderController else null
677     }
678 
679     private val mOnHeadsUpChangedListener = object : OnHeadsUpChangedListener {
680         override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) {
681             if (!isHeadsUp) {
682                 mNotifPromoter.invalidateList("headsUpEnded: ${entry.logKey}")
683                 mHeadsUpViewBinder.unbindHeadsUpView(entry)
684                 endNotifLifetimeExtensionIfExtended(entry)
685             }
686         }
687     }
688 
689     private fun isSticky(entry: NotificationEntry) = mHeadsUpManager.isSticky(entry.key)
690 
691     private fun isEntryBinding(entry: ListEntry): Boolean {
692         val bindingUntil = mEntriesBindingUntil[entry.key]
693         return bindingUntil != null && bindingUntil >= mNow
694     }
695 
696     /**
697      * Whether the notification is already alerting or binding so that it can imminently alert
698      */
699     private fun isAttemptingToShowHun(entry: ListEntry) =
700         mHeadsUpManager.isAlerting(entry.key) || isEntryBinding(entry)
701 
702     /**
703      * Whether the notification is already alerting/binding per [isAttemptingToShowHun] OR if it
704      * has been updated so that it should alert this update.  This method is permissive because it
705      * returns `true` even if the update would (in isolation of its group) cause the alert to be
706      * retracted.  This is important for not retracting transferred group alerts.
707      */
708     private fun isGoingToShowHunNoRetract(entry: ListEntry) =
709         mPostedEntries[entry.key]?.calculateShouldBeHeadsUpNoRetract ?: isAttemptingToShowHun(entry)
710 
711     /**
712      * If the notification has been updated, then whether it should HUN in isolation, otherwise
713      * defers to the already alerting/binding state of [isAttemptingToShowHun].  This method is
714      * strict because any update which would revoke the alert supersedes the current
715      * alerting/binding state.
716      */
717     private fun isGoingToShowHunStrict(entry: ListEntry) =
718         mPostedEntries[entry.key]?.calculateShouldBeHeadsUpStrict ?: isAttemptingToShowHun(entry)
719 
720     private fun endNotifLifetimeExtensionIfExtended(entry: NotificationEntry) {
721         if (mNotifsExtendingLifetime.contains(entry)) {
722             mNotifsExtendingLifetime.remove(entry)?.run()
723             mEndLifetimeExtension?.onEndLifetimeExtension(mLifetimeExtender, entry)
724         }
725     }
726 
727     companion object {
728         private const val TAG = "HeadsUpCoordinator"
729         private const val BIND_TIMEOUT = 1000L
730 
731         // This value is set to match MAX_SOUND_DELAY_MS in NotificationRecord.
732         private const val MAX_RANKING_UPDATE_DELAY_MS: Long = 2000
733     }
734 
735     data class PostedEntry(
736         val entry: NotificationEntry,
737         val wasAdded: Boolean,
738         var wasUpdated: Boolean,
739         var shouldHeadsUpEver: Boolean,
740         var shouldHeadsUpAgain: Boolean,
741         var isAlerting: Boolean,
742         var isBinding: Boolean,
743     ) {
744         val key = entry.key
745         val isHeadsUpAlready: Boolean
746             get() = isAlerting || isBinding
747         val calculateShouldBeHeadsUpStrict: Boolean
748             get() = shouldHeadsUpEver && (wasAdded || shouldHeadsUpAgain || isHeadsUpAlready)
749         val calculateShouldBeHeadsUpNoRetract: Boolean
750             get() = isHeadsUpAlready || (shouldHeadsUpEver && (wasAdded || shouldHeadsUpAgain))
751     }
752 }
753 
754 private enum class GroupLocation { Detached, Isolated, Summary, Child }
755 
getLocationnull756 private fun Map<String, GroupLocation>.getLocation(key: String): GroupLocation =
757     getOrDefault(key, GroupLocation.Detached)
758 
759 /**
760  * Invokes the given block with a [HunMutator] that defers all HUN removals. This ensures that the
761  * HeadsUpManager is notified of additions before removals, which prevents a glitch where the
762  * HeadsUpManager temporarily believes that nothing is alerting, causing bad re-entrant behavior.
763  */
764 private fun <R> HeadsUpManager.modifyHuns(block: (HunMutator) -> R): R {
765     val mutator = HunMutatorImpl(this)
766     return block(mutator).also { mutator.commitModifications() }
767 }
768 
769 /** Mutates the HeadsUp state of notifications. */
770 private interface HunMutator {
updateNotificationnull771     fun updateNotification(key: String, alert: Boolean)
772     fun removeNotification(key: String, releaseImmediately: Boolean)
773 }
774 
775 /**
776  * [HunMutator] implementation that defers removing notifications from the HeadsUpManager until
777  * after additions/updates.
778  */
779 private class HunMutatorImpl(private val headsUpManager: HeadsUpManager) : HunMutator {
780     private val deferred = mutableListOf<Pair<String, Boolean>>()
781 
782     override fun updateNotification(key: String, alert: Boolean) {
783         headsUpManager.updateNotification(key, alert)
784     }
785 
786     override fun removeNotification(key: String, releaseImmediately: Boolean) {
787         val args = Pair(key, releaseImmediately)
788         deferred.add(args)
789     }
790 
791     fun commitModifications() {
792         deferred.forEach { (key, releaseImmediately) ->
793             headsUpManager.removeNotification(key, releaseImmediately)
794         }
795         deferred.clear()
796     }
797 }
798