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