1 /*
2  * Copyright (C) 2024 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.policy
17 
18 import android.util.Log
19 import androidx.annotation.VisibleForTesting
20 import com.android.internal.logging.UiEvent
21 import com.android.internal.logging.UiEventLogger
22 import com.android.systemui.Dumpable
23 import com.android.systemui.dagger.SysUISingleton
24 import com.android.systemui.dump.DumpManager
25 import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
26 import com.android.systemui.statusbar.policy.BaseHeadsUpManager.HeadsUpEntry
27 import com.android.systemui.util.Compile
28 import java.io.PrintWriter
29 import javax.inject.Inject
30 
31 /*
32  * Control when heads up notifications show during an avalanche where notifications arrive in fast
33  * succession, by delaying visual listener side effects and removal handling from BaseHeadsUpManager
34  */
35 @SysUISingleton
36 class AvalancheController
37 @Inject
38 constructor(dumpManager: DumpManager, private val uiEventLogger: UiEventLogger) : Dumpable {
39 
40     private val tag = "AvalancheController"
41     private val debug = Compile.IS_DEBUG && Log.isLoggable(tag, Log.DEBUG)
42 
43     // HUN showing right now, in the floating state where full shade is hidden, on launcher or AOD
44     @VisibleForTesting var headsUpEntryShowing: HeadsUpEntry? = null
45 
46     // Key of HUN previously showing, is being removed or was removed
47     var previousHunKey: String = ""
48 
49     // List of runnables to run for the HUN showing right now
50     private var headsUpEntryShowingRunnableList: MutableList<Runnable> = ArrayList()
51 
52     // HeadsUpEntry waiting to show
53     // Use sortable list instead of priority queue for debugging
54     private val nextList: MutableList<HeadsUpEntry> = ArrayList()
55 
56     // Map of HeadsUpEntry waiting to show, and runnables to run when it shows.
57     // Use HashMap instead of SortedMap for faster lookup, and also because the ordering
58     // provided by HeadsUpEntry.compareTo is not consistent over time or with HeadsUpEntry.equals
59     @VisibleForTesting var nextMap: MutableMap<HeadsUpEntry, MutableList<Runnable>> = HashMap()
60 
61     // Map of Runnable to label for debugging only
62     private val debugRunnableLabelMap: MutableMap<Runnable, String> = HashMap()
63 
64     // HeadsUpEntry we did not show at all because they are not the top priority hun in their batch
65     // For debugging only
66     @VisibleForTesting var debugDropSet: MutableSet<HeadsUpEntry> = HashSet()
67 
68     enum class ThrottleEvent(private val id: Int) : UiEventLogger.UiEventEnum {
69         @UiEvent(doc = "HUN was shown.") AVALANCHE_THROTTLING_HUN_SHOWN(1821),
70         @UiEvent(doc = "HUN was dropped to show higher priority HUNs.")
71         AVALANCHE_THROTTLING_HUN_DROPPED(1822),
72         @UiEvent(doc = "HUN was removed while waiting to show.")
73         AVALANCHE_THROTTLING_HUN_REMOVED(1823);
74 
getIdnull75         override fun getId(): Int {
76             return id
77         }
78     }
79 
80     init {
81         dumpManager.registerNormalDumpable(tag, /* module */ this)
82     }
83 
getShowingHunKeynull84     fun getShowingHunKey(): String {
85         return getKey(headsUpEntryShowing)
86     }
87 
88     /** Run or delay Runnable for given HeadsUpEntry */
updatenull89     fun update(entry: HeadsUpEntry?, runnable: Runnable, label: String) {
90         if (!NotificationThrottleHun.isEnabled) {
91             runnable.run()
92             return
93         }
94         log { "\n " }
95         val fn = "$label => AvalancheController.update ${getKey(entry)}"
96         if (entry == null) {
97             log { "Entry is NULL, stop update." }
98             return
99         }
100         if (debug) {
101             debugRunnableLabelMap[runnable] = label
102         }
103         if (isShowing(entry)) {
104             log { "\n$fn => update showing" }
105             runnable.run()
106         } else if (entry in nextMap) {
107             log { "\n$fn => update next" }
108             nextMap[entry]?.add(runnable)
109         } else if (headsUpEntryShowing == null) {
110             log { "\n$fn => showNow" }
111             showNow(entry, arrayListOf(runnable))
112         } else {
113             // Clean up invalid state when entry is in list but not map and vice versa
114             if (entry in nextMap) nextMap.remove(entry)
115             if (entry in nextList) nextList.remove(entry)
116 
117             addToNext(entry, runnable)
118 
119             // Shorten headsUpEntryShowing display time
120             val nextIndex = nextList.indexOf(entry)
121             val isOnlyNextEntry = nextIndex == 0 && nextList.size == 1
122             if (isOnlyNextEntry) {
123                 // HeadsUpEntry.updateEntry recursively calls AvalancheController#update
124                 // and goes to the isShowing case above
125                 headsUpEntryShowing!!.updateEntry(
126                     /* updatePostTime= */ false,
127                     /* updateEarliestRemovalTime= */ false,
128                     /* reason= */ "avalanche duration update"
129                 )
130             }
131         }
132         logState("after $fn")
133     }
134 
135     @VisibleForTesting
addToNextnull136     fun addToNext(entry: HeadsUpEntry, runnable: Runnable) {
137         nextMap[entry] = arrayListOf(runnable)
138         nextList.add(entry)
139     }
140 
141     /**
142      * Run or ignore Runnable for given HeadsUpEntry. If entry was never shown, ignore and delete
143      * all Runnables associated with that entry.
144      */
deletenull145     fun delete(entry: HeadsUpEntry?, runnable: Runnable, label: String) {
146         if (!NotificationThrottleHun.isEnabled) {
147             runnable.run()
148             return
149         }
150         log { "\n " }
151         val fn = "$label => AvalancheController.delete " + getKey(entry)
152         if (entry == null) {
153             log { "$fn => entry NULL, running runnable" }
154             runnable.run()
155             return
156         }
157         if (entry in nextMap) {
158             log { "$fn => remove from next" }
159             if (entry in nextMap) nextMap.remove(entry)
160             if (entry in nextList) nextList.remove(entry)
161             uiEventLogger.log(ThrottleEvent.AVALANCHE_THROTTLING_HUN_REMOVED)
162         } else if (entry in debugDropSet) {
163             log { "$fn => remove from dropset" }
164             debugDropSet.remove(entry)
165         } else if (isShowing(entry)) {
166             log { "$fn => remove showing ${getKey(entry)}" }
167             previousHunKey = getKey(headsUpEntryShowing)
168             // Show the next HUN before removing this one, so that we don't tell listeners
169             // onHeadsUpPinnedModeChanged, which causes
170             // NotificationPanelViewController.updateTouchableRegion to hide the window while the
171             // HUN is animating out, resulting in a flicker.
172             showNext()
173             runnable.run()
174         } else {
175             log { "$fn => removing untracked ${getKey(entry)}" }
176         }
177         logState("after $fn")
178     }
179 
180     /**
181      * Returns duration based on
182      * 1) Whether HeadsUpEntry is the last one tracked byAvalancheController
183      * 2) The priority of the top HUN in the next batch Used by
184      *    BaseHeadsUpManager.HeadsUpEntry.calculateFinishTime to shorten display duration.
185      */
getDurationMsnull186     fun getDurationMs(entry: HeadsUpEntry, autoDismissMs: Int): Int {
187         if (!NotificationThrottleHun.isEnabled) {
188             // Use default duration, like we did before AvalancheController existed
189             return autoDismissMs
190         }
191         val showingList: MutableList<HeadsUpEntry> = mutableListOf()
192         if (headsUpEntryShowing != null) {
193             showingList.add(headsUpEntryShowing!!)
194         }
195         nextList.sort()
196         val entryList = showingList + nextList
197         if (entryList.isEmpty()) {
198             log { "No avalanche HUNs, use default ms: $autoDismissMs" }
199             return autoDismissMs
200         }
201         // entryList.indexOf(entry) returns -1 even when the entry is in entryList
202         var thisEntryIndex = -1
203         for ((i, e) in entryList.withIndex()) {
204             if (e == entry) {
205                 thisEntryIndex = i
206             }
207         }
208         if (thisEntryIndex == -1) {
209             log { "Untracked entry, use default ms: $autoDismissMs" }
210             return autoDismissMs
211         }
212         val nextEntryIndex = thisEntryIndex + 1
213 
214         // If last entry, use default duration
215         if (nextEntryIndex >= entryList.size) {
216             log { "Last entry, use default ms: $autoDismissMs" }
217             return autoDismissMs
218         }
219         val nextEntry = entryList[nextEntryIndex]
220         if (nextEntry.compareNonTimeFields(entry) == -1) {
221             // Next entry is higher priority
222             log { "Next entry is higher priority: 500ms" }
223             return 500
224         } else if (nextEntry.compareNonTimeFields(entry) == 0) {
225             // Next entry is same priority
226             log { "Next entry is same priority: 1000ms" }
227             return 1000
228         } else {
229             log { "Next entry is lower priority, use default ms: $autoDismissMs" }
230             return autoDismissMs
231         }
232     }
233 
234     /** Return true if entry is waiting to show. */
isWaitingnull235     fun isWaiting(key: String): Boolean {
236         if (!NotificationThrottleHun.isEnabled) {
237             return false
238         }
239         for (entry in nextMap.keys) {
240             if (entry.mEntry?.key.equals(key)) {
241                 return true
242             }
243         }
244         return false
245     }
246 
247     /** Return list of keys for huns waiting */
getWaitingKeysnull248     fun getWaitingKeys(): MutableList<String> {
249         if (!NotificationThrottleHun.isEnabled) {
250             return mutableListOf()
251         }
252         val keyList = mutableListOf<String>()
253         for (entry in nextMap.keys) {
254             entry.mEntry?.let { keyList.add(entry.mEntry!!.key) }
255         }
256         return keyList
257     }
258 
getWaitingEntrynull259     fun getWaitingEntry(key: String): HeadsUpEntry? {
260         if (!NotificationThrottleHun.isEnabled) {
261             return null
262         }
263         for (headsUpEntry in nextMap.keys) {
264             if (headsUpEntry.mEntry?.key.equals(key)) {
265                 return headsUpEntry
266             }
267         }
268         return null
269     }
270 
getWaitingEntryListnull271     fun getWaitingEntryList(): List<HeadsUpEntry> {
272         if (!NotificationThrottleHun.isEnabled) {
273             return mutableListOf()
274         }
275         return nextMap.keys.toList()
276     }
277 
isShowingnull278     private fun isShowing(entry: HeadsUpEntry): Boolean {
279         return headsUpEntryShowing != null && entry.mEntry?.key == headsUpEntryShowing?.mEntry?.key
280     }
281 
showNownull282     private fun showNow(entry: HeadsUpEntry, runnableList: MutableList<Runnable>) {
283         log { "SHOW: " + getKey(entry) }
284 
285         uiEventLogger.log(ThrottleEvent.AVALANCHE_THROTTLING_HUN_SHOWN)
286         headsUpEntryShowing = entry
287 
288         runnableList.forEach {
289             if (it in debugRunnableLabelMap) {
290                 log { "RUNNABLE: ${debugRunnableLabelMap[it]}" }
291             }
292             it.run()
293         }
294     }
295 
showNextnull296     private fun showNext() {
297         log { "SHOW NEXT" }
298         headsUpEntryShowing = null
299 
300         if (nextList.isEmpty()) {
301             log { "NO MORE TO SHOW" }
302             previousHunKey = ""
303             return
304         }
305 
306         // Only show first (top priority) entry in next batch
307         nextList.sort()
308         headsUpEntryShowing = nextList[0]
309         headsUpEntryShowingRunnableList = nextMap[headsUpEntryShowing]!!
310 
311         // Remove runnable labels for dropped huns
312         val listToDrop = nextList.subList(1, nextList.size)
313 
314         // Log dropped HUNs
315         for (e in listToDrop) {
316             uiEventLogger.log(ThrottleEvent.AVALANCHE_THROTTLING_HUN_DROPPED)
317         }
318 
319         if (debug) {
320             // Clear runnable labels
321             for (e in listToDrop) {
322                 val runnableList = nextMap[e]!!
323                 for (r in runnableList) {
324                     debugRunnableLabelMap.remove(r)
325                 }
326             }
327             debugDropSet.addAll(listToDrop)
328         }
329 
330         clearNext()
331         showNow(headsUpEntryShowing!!, headsUpEntryShowingRunnableList)
332     }
333 
clearNextnull334     fun clearNext() {
335         nextList.clear()
336         nextMap.clear()
337     }
338 
339     // Methods below are for logging only ==========================================================
340 
lognull341     private inline fun log(s: () -> String) {
342         if (debug) {
343             Log.d(tag, s())
344         }
345     }
346 
getStateStrnull347     private fun getStateStr(): String {
348         return "SHOWING: [${getKey(headsUpEntryShowing)}]" +
349             "\nPREVIOUS: [$previousHunKey]" +
350             "\nNEXT LIST: $nextListStr" +
351             "\nNEXT MAP: $nextMapStr" +
352             "\nDROPPED: $dropSetStr"
353     }
354 
logStatenull355     private fun logState(reason: String) {
356         log {
357             "\n================================================================================="
358         }
359         log { "STATE $reason" }
360         log { getStateStr() }
361         log {
362             "=================================================================================\n"
363         }
364     }
365 
366     private val dropSetStr: String
367         get() {
368             val queue = ArrayList<String>()
369             for (entry in debugDropSet) {
370                 queue.add("[${getKey(entry)}]")
371             }
372             return java.lang.String.join("\n", queue)
373         }
374 
375     private val nextListStr: String
376         get() {
377             val queue = ArrayList<String>()
378             for (entry in nextList) {
379                 queue.add("[${getKey(entry)}]")
380             }
381             return java.lang.String.join("\n", queue)
382         }
383 
384     private val nextMapStr: String
385         get() {
386             val queue = ArrayList<String>()
387             for (entry in nextMap.keys) {
388                 queue.add("[${getKey(entry)}]")
389             }
390             return java.lang.String.join("\n", queue)
391         }
392 
getKeynull393     fun getKey(entry: HeadsUpEntry?): String {
394         if (entry == null) {
395             return "HeadsUpEntry null"
396         }
397         if (entry.mEntry == null) {
398             return "HeadsUpEntry.mEntry null"
399         }
400         return entry.mEntry!!.key
401     }
402 
dumpnull403     override fun dump(pw: PrintWriter, args: Array<out String>) {
404         pw.println("AvalancheController: ${getStateStr()}")
405     }
406 }
407