1 /* <lambda>null2 * 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.notification.headsup 17 18 import android.os.Handler 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.dagger.qualifiers.Background 25 import com.android.systemui.dump.DumpManager 26 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips 27 import com.android.systemui.statusbar.notification.headsup.HeadsUpManagerImpl.HeadsUpEntry 28 import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun 29 import java.io.PrintWriter 30 import javax.inject.Inject 31 32 /* 33 * Control when heads up notifications show during an avalanche where notifications arrive in fast 34 * succession, by delaying visual listener side effects and removal handling from 35 * [HeadsUpManagerImpl]. 36 * 37 * Dev note: disable suppression so avoid 2min period of no HUNs after every build 38 * Settings > Notifications > General > Notification cooldown 39 */ 40 @SysUISingleton 41 class AvalancheController 42 @Inject 43 constructor( 44 dumpManager: DumpManager, 45 private val uiEventLogger: UiEventLogger, 46 private val headsUpManagerLogger: HeadsUpManagerLogger, 47 @Background private val bgHandler: Handler, 48 ) : Dumpable { 49 50 private val tag = "AvalancheController" 51 private val debug = false // Compile.IS_DEBUG && Log.isLoggable(tag, Log.DEBUG) 52 var baseEntryMapStr: () -> String = { "baseEntryMapStr not initialized" } 53 54 var enableAtRuntime = true 55 set(value) { 56 if (!value) { 57 // Waiting HUNs in AvalancheController are shown in the HUN section in open shade. 58 // Clear them so we don't show them again when the shade closes and reordering is 59 // allowed again. 60 logDroppedHunsInBackground(getWaitingKeys().size) 61 clearNext() 62 headsUpEntryShowing = null 63 } 64 if (field != value) { 65 field = value 66 } 67 } 68 69 // HUN showing right now, in the floating state where full shade is hidden, on launcher or AOD 70 @VisibleForTesting var headsUpEntryShowing: HeadsUpEntry? = null 71 72 // Key of HUN previously showing, is being removed or was removed 73 var previousHunKey: String = "" 74 75 // List of runnables to run for the HUN showing right now 76 private var headsUpEntryShowingRunnableList: MutableList<Runnable> = ArrayList() 77 78 // HeadsUpEntry waiting to show 79 // Use sortable list instead of priority queue for debugging 80 private val nextList: MutableList<HeadsUpEntry> = ArrayList() 81 82 // Map of HeadsUpEntry waiting to show, and runnables to run when it shows. 83 // Use HashMap instead of SortedMap for faster lookup, and also because the ordering 84 // provided by HeadsUpEntry.compareTo is not consistent over time or with HeadsUpEntry.equals 85 @VisibleForTesting var nextMap: MutableMap<HeadsUpEntry, MutableList<Runnable>> = HashMap() 86 87 // Map of Runnable to label for debugging only 88 private val debugRunnableLabelMap: MutableMap<Runnable, String> = HashMap() 89 90 enum class ThrottleEvent(private val id: Int) : UiEventLogger.UiEventEnum { 91 @UiEvent(doc = "HUN was shown.") AVALANCHE_THROTTLING_HUN_SHOWN(1821), 92 @UiEvent(doc = "HUN was dropped to show higher priority HUNs.") 93 AVALANCHE_THROTTLING_HUN_DROPPED(1822), 94 @UiEvent(doc = "HUN was removed while waiting to show.") 95 AVALANCHE_THROTTLING_HUN_REMOVED(1823); 96 97 override fun getId(): Int { 98 return id 99 } 100 } 101 102 init { 103 dumpManager.registerNormalDumpable(tag, /* module */ this) 104 } 105 106 fun getShowingHunKey(): String { 107 return getKey(headsUpEntryShowing) 108 } 109 110 fun isEnabled(): Boolean { 111 return NotificationThrottleHun.isEnabled && enableAtRuntime 112 } 113 114 /** Run or delay Runnable for given HeadsUpEntry */ 115 fun update(entry: HeadsUpEntry?, runnable: Runnable?, caller: String) { 116 val isEnabled = isEnabled() 117 val key = getKey(entry) 118 119 if (runnable == null) { 120 headsUpManagerLogger.logAvalancheUpdate( 121 caller, 122 isEnabled, 123 key, 124 "Runnable NULL, stop. ${getStateStr()}", 125 ) 126 return 127 } 128 if (!isEnabled) { 129 headsUpManagerLogger.logAvalancheUpdate( 130 caller, 131 isEnabled, 132 key, 133 "NOT ENABLED, run runnable. ${getStateStr()}", 134 ) 135 runnable.run() 136 return 137 } 138 if (entry == null) { 139 headsUpManagerLogger.logAvalancheUpdate( 140 caller, 141 isEnabled, 142 key, 143 "Entry NULL, stop. ${getStateStr()}", 144 ) 145 return 146 } 147 if (debug) { 148 debugRunnableLabelMap[runnable] = caller 149 } 150 var outcome = "" 151 if (isShowing(entry)) { 152 outcome = "update showing" 153 runnable.run() 154 } else if (entry in nextMap) { 155 outcome = "update next" 156 nextMap[entry]?.add(runnable) 157 checkNextPinnedByUser(entry)?.let { outcome = "$outcome & $it" } 158 } else if (headsUpEntryShowing == null) { 159 outcome = "show now" 160 showNow(entry, arrayListOf(runnable)) 161 } else { 162 // Clean up invalid state when entry is in list but not map and vice versa 163 if (entry in nextMap) nextMap.remove(entry) 164 if (entry in nextList) nextList.remove(entry) 165 166 outcome = "add next" 167 addToNext(entry, runnable) 168 169 val nextIsPinnedByUserResult = checkNextPinnedByUser(entry) 170 if (nextIsPinnedByUserResult != null) { 171 outcome = "$outcome & $nextIsPinnedByUserResult" 172 } else { 173 // Shorten headsUpEntryShowing display time 174 val nextIndex = nextList.indexOf(entry) 175 val isOnlyNextEntry = nextIndex == 0 && nextList.size == 1 176 if (isOnlyNextEntry) { 177 // HeadsUpEntry.updateEntry recursively calls AvalancheController#update 178 // and goes to the isShowing case above 179 headsUpEntryShowing!!.updateEntry( 180 /* updatePostTime= */ false, 181 /* updateEarliestRemovalTime= */ false, 182 /* reason= */ "shorten duration of previously-last HUN", 183 ) 184 } 185 } 186 } 187 outcome += getStateStr() 188 headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled, key, outcome) 189 } 190 191 @VisibleForTesting 192 fun addToNext(entry: HeadsUpEntry, runnable: Runnable) { 193 nextMap[entry] = arrayListOf(runnable) 194 nextList.add(entry) 195 } 196 197 /** 198 * Checks if the given entry is requesting [PinnedStatus.PinnedByUser] status and makes the 199 * correct updates if needed. 200 * 201 * @return a string representing the outcome, or null if nothing changed. 202 */ 203 private fun checkNextPinnedByUser(entry: HeadsUpEntry): String? { 204 if ( 205 StatusBarNotifChips.isEnabled && 206 entry.requestedPinnedStatus == PinnedStatus.PinnedByUser 207 ) { 208 val string = "next is PinnedByUser" 209 headsUpEntryShowing?.updateEntry( 210 /* updatePostTime= */ false, 211 /* updateEarliestRemovalTime= */ false, 212 /* reason= */ string, 213 ) 214 return string 215 } 216 return null 217 } 218 219 /** 220 * Run or ignore Runnable for given HeadsUpEntry. If entry was never shown, ignore and delete 221 * all Runnables associated with that entry. 222 */ 223 fun delete(entry: HeadsUpEntry?, runnable: Runnable?, caller: String) { 224 val isEnabled = isEnabled() 225 val key = getKey(entry) 226 227 if (runnable == null) { 228 headsUpManagerLogger.logAvalancheDelete( 229 caller, 230 isEnabled, 231 key, 232 "Runnable NULL, stop. ${getStateStr()}", 233 ) 234 return 235 } 236 if (!isEnabled) { 237 runnable.run() 238 headsUpManagerLogger.logAvalancheDelete( 239 caller, 240 isEnabled = false, 241 key, 242 "NOT ENABLED, run runnable. ${getStateStr()}", 243 ) 244 return 245 } 246 if (entry == null) { 247 runnable.run() 248 headsUpManagerLogger.logAvalancheDelete( 249 caller, 250 isEnabled = true, 251 key, 252 "Entry NULL, run runnable. ${getStateStr()}", 253 ) 254 return 255 } 256 val outcome: String 257 if (entry in nextMap) { 258 if (entry in nextMap) nextMap.remove(entry) 259 if (entry in nextList) nextList.remove(entry) 260 uiEventLogger.log(ThrottleEvent.AVALANCHE_THROTTLING_HUN_REMOVED) 261 outcome = "remove from next. ${getStateStr()}" 262 } else if (isShowing(entry)) { 263 previousHunKey = getKey(headsUpEntryShowing) 264 // Show the next HUN before removing this one, so that we don't tell listeners 265 // onHeadsUpPinnedModeChanged, which causes 266 // NotificationPanelViewController.updateTouchableRegion to hide the window while the 267 // HUN is animating out, resulting in a flicker. 268 showNext() 269 runnable.run() 270 outcome = "remove showing. ${getStateStr()}" 271 } else { 272 runnable.run() 273 outcome = 274 "run runnable for untracked HUN " + 275 "(was dropped or shown when AC was disabled). ${getStateStr()}" 276 } 277 headsUpManagerLogger.logAvalancheDelete(caller, isEnabled(), getKey(entry), outcome) 278 } 279 280 /** 281 * Returns how much longer the given entry should show based on: 282 * 1) Whether HeadsUpEntry is the last one tracked by AvalancheController 283 * 2) The priority of the top HUN in the next batch 284 * 285 * Used by [HeadsUpManagerImpl.HeadsUpEntry]'s finishTimeCalculator to shorten display duration. 286 */ 287 fun getDuration(entry: HeadsUpEntry?, autoDismissMsValue: Int): RemainingDuration { 288 val autoDismissMs = RemainingDuration.UpdatedDuration(autoDismissMsValue) 289 if (!isEnabled()) { 290 // Use default duration, like we did before AvalancheController existed 291 return autoDismissMs 292 } 293 if (entry == null) { 294 // This should never happen 295 return autoDismissMs 296 } 297 val showingList: MutableList<HeadsUpEntry> = mutableListOf() 298 if (headsUpEntryShowing != null) { 299 showingList.add(headsUpEntryShowing!!) 300 } 301 nextList.sort() 302 val entryList = showingList + nextList 303 val thisKey = getKey(entry) 304 if (entryList.isEmpty()) { 305 headsUpManagerLogger.logAvalancheDuration( 306 thisKey, 307 autoDismissMs, 308 "No avalanche HUNs, use default", 309 nextKey = "", 310 ) 311 return autoDismissMs 312 } 313 // entryList.indexOf(entry) returns -1 even when the entry is in entryList 314 var thisEntryIndex = -1 315 for ((i, e) in entryList.withIndex()) { 316 if (e == entry) { 317 thisEntryIndex = i 318 } 319 } 320 if (thisEntryIndex == -1) { 321 headsUpManagerLogger.logAvalancheDuration( 322 thisKey, 323 autoDismissMs, 324 "Untracked entry, use default", 325 nextKey = "", 326 ) 327 return autoDismissMs 328 } 329 val nextEntryIndex = thisEntryIndex + 1 330 if (nextEntryIndex >= entryList.size) { 331 headsUpManagerLogger.logAvalancheDuration( 332 thisKey, 333 autoDismissMs, 334 "Last entry, use default", 335 nextKey = "", 336 ) 337 return autoDismissMs 338 } 339 val nextEntry = entryList[nextEntryIndex] 340 val nextKey = getKey(nextEntry) 341 342 if ( 343 StatusBarNotifChips.isEnabled && 344 nextEntry.requestedPinnedStatus == PinnedStatus.PinnedByUser 345 ) { 346 return RemainingDuration.HideImmediately.also { 347 headsUpManagerLogger.logAvalancheDuration( 348 thisKey, 349 duration = it, 350 "next is PinnedByUser", 351 nextKey, 352 ) 353 } 354 } 355 if (nextEntry.compareNonTimeFields(entry) == -1) { 356 return RemainingDuration.UpdatedDuration(500).also { 357 headsUpManagerLogger.logAvalancheDuration( 358 thisKey, 359 duration = it, 360 "LOWER priority than next: ", 361 nextKey, 362 ) 363 } 364 } else if (nextEntry.compareNonTimeFields(entry) == 0) { 365 return RemainingDuration.UpdatedDuration(1000).also { 366 headsUpManagerLogger.logAvalancheDuration( 367 thisKey, 368 duration = it, 369 "SAME priority as next: ", 370 nextKey, 371 ) 372 } 373 } else { 374 headsUpManagerLogger.logAvalancheDuration( 375 thisKey, 376 autoDismissMs, 377 "HIGHER priority than next: ", 378 nextKey, 379 ) 380 return autoDismissMs 381 } 382 } 383 384 /** Return true if entry is waiting to show. */ 385 fun isWaiting(key: String): Boolean { 386 if (!isEnabled()) { 387 return false 388 } 389 for (entry in nextMap.keys) { 390 if (entry.mEntry?.key.equals(key)) { 391 return true 392 } 393 } 394 return false 395 } 396 397 /** Return list of keys for huns waiting */ 398 fun getWaitingKeys(): MutableList<String> { 399 if (!isEnabled()) { 400 return mutableListOf() 401 } 402 val keyList = mutableListOf<String>() 403 for (entry in nextMap.keys) { 404 entry.mEntry?.let { keyList.add(entry.mEntry!!.key) } 405 } 406 return keyList 407 } 408 409 fun getWaitingEntry(key: String): HeadsUpEntry? { 410 if (!isEnabled()) { 411 return null 412 } 413 for (headsUpEntry in nextMap.keys) { 414 if (headsUpEntry.mEntry?.key.equals(key)) { 415 return headsUpEntry 416 } 417 } 418 return null 419 } 420 421 fun getWaitingEntryList(): List<HeadsUpEntry> { 422 if (!isEnabled()) { 423 return mutableListOf() 424 } 425 return nextMap.keys.toList() 426 } 427 428 private fun isShowing(entry: HeadsUpEntry): Boolean { 429 return headsUpEntryShowing != null && entry.mEntry?.key == headsUpEntryShowing?.mEntry?.key 430 } 431 432 private fun showNow(entry: HeadsUpEntry, runnableList: MutableList<Runnable>) { 433 headsUpManagerLogger.logAvalancheStage("show", getKey(entry)) 434 uiEventLogger.log(ThrottleEvent.AVALANCHE_THROTTLING_HUN_SHOWN) 435 headsUpEntryShowing = entry 436 437 runnableList.forEach { runnable -> 438 if (debug) { 439 debugRunnableLabelMap[runnable]?.let { label -> 440 headsUpManagerLogger.logAvalancheStage("run", label) 441 // Remove label after logging to avoid memory leak 442 debugRunnableLabelMap.remove(runnable) 443 } 444 } 445 runnable.run() 446 } 447 } 448 449 private fun showNext() { 450 headsUpManagerLogger.logAvalancheStage("show next", key = "") 451 headsUpEntryShowing = null 452 453 if (nextList.isEmpty()) { 454 headsUpManagerLogger.logAvalancheStage("no more", key = "") 455 previousHunKey = "" 456 return 457 } 458 459 // Only show first (top priority) entry in next batch 460 nextList.sort() 461 headsUpEntryShowing = nextList[0] 462 headsUpEntryShowingRunnableList = nextMap[headsUpEntryShowing]!! 463 464 // Remove runnable labels for dropped huns 465 val listToDrop = nextList.subList(1, nextList.size) 466 logDroppedHunsInBackground(listToDrop.size) 467 468 if (debug) { 469 // Clear runnable labels 470 for (e in listToDrop) { 471 val runnableList = nextMap[e]!! 472 for (r in runnableList) { 473 debugRunnableLabelMap.remove(r) 474 } 475 } 476 } 477 478 val dropListStr = listToDrop.joinToString("\n ") { getKey(it) } 479 headsUpManagerLogger.logDroppedHuns(dropListStr) 480 481 clearNext() 482 showNow(headsUpEntryShowing!!, headsUpEntryShowingRunnableList) 483 } 484 485 private fun logDroppedHunsInBackground(numDropped: Int) { 486 bgHandler.post( 487 Runnable { 488 // Do this in the background to avoid missing frames when closing the shade 489 for (n in 1..numDropped) { 490 uiEventLogger.log(ThrottleEvent.AVALANCHE_THROTTLING_HUN_DROPPED) 491 } 492 } 493 ) 494 } 495 496 fun clearNext() { 497 nextList.clear() 498 nextMap.clear() 499 } 500 501 // Methods below are for logging only ========================================================== 502 503 private fun getStateStr(): String { 504 return "\n[AC state]" + 505 "\nshow: ${getKey(headsUpEntryShowing)}" + 506 "\nprevious: $previousHunKey" + 507 "\n$nextStr" + 508 "\n[HeadsUpManagerImpl.mHeadsUpEntryMap] " + 509 baseEntryMapStr() + 510 "\n" 511 } 512 513 private val nextStr: String 514 get() { 515 val nextListStr = nextList.joinToString("\n ") { getKey(it) } 516 if (nextList.toSet() == nextMap.keys.toSet()) { 517 return "next (${nextList.size}):\n $nextListStr" 518 } 519 // This should never happen 520 val nextMapStr = nextMap.keys.joinToString("\n ") { getKey(it) } 521 return "next list (${nextList.size}):\n $nextListStr" + 522 "\nnext map (${nextMap.size}):\n $nextMapStr" 523 } 524 525 fun getKey(entry: HeadsUpEntry?): String { 526 if (entry == null) { 527 return "HeadsUpEntry null" 528 } 529 if (entry.mEntry == null) { 530 return "HeadsUpEntry.mEntry null" 531 } 532 return entry.mEntry!!.key 533 } 534 535 override fun dump(pw: PrintWriter, args: Array<out String>) { 536 pw.println("AvalancheController: ${getStateStr()}") 537 } 538 } 539