1 /* <lambda>null2 * Copyright (C) 2021 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 17 package com.android.systemui.temporarydisplay 18 19 import android.annotation.LayoutRes 20 import android.content.Context 21 import android.graphics.PixelFormat 22 import android.graphics.Rect 23 import android.graphics.drawable.Drawable 24 import android.os.PowerManager 25 import android.view.LayoutInflater 26 import android.view.View 27 import android.view.ViewGroup 28 import android.view.WindowManager 29 import android.view.accessibility.AccessibilityManager 30 import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS 31 import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS 32 import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_TEXT 33 import androidx.annotation.CallSuper 34 import androidx.annotation.VisibleForTesting 35 import com.android.systemui.CoreStartable 36 import com.android.systemui.Dumpable 37 import com.android.systemui.dagger.qualifiers.Main 38 import com.android.systemui.dump.DumpManager 39 import com.android.systemui.statusbar.policy.ConfigurationController 40 import com.android.systemui.util.concurrency.DelayableExecutor 41 import com.android.systemui.util.time.SystemClock 42 import com.android.systemui.util.wakelock.WakeLock 43 import java.io.PrintWriter 44 45 /** 46 * A generic controller that can temporarily display a new view in a new window. 47 * 48 * Subclasses need to override and implement [updateView], which is where they can control what 49 * gets displayed to the user. 50 * 51 * The generic type T is expected to contain all the information necessary for the subclasses to 52 * display the view in a certain state, since they receive <T> in [updateView]. 53 * 54 * Some information about display ordering: 55 * 56 * [ViewPriority] defines different priorities for the incoming views. The incoming view will be 57 * displayed so long as its priority is equal to or greater than the currently displayed view. 58 * (Concretely, this means that a [ViewPriority.NORMAL] won't be displayed if a 59 * [ViewPriority.CRITICAL] is currently displayed. But otherwise, the incoming view will get 60 * displayed and kick out the old view). 61 * 62 * Once the currently displayed view times out, we *may* display a previously requested view if it 63 * still has enough time left before its own timeout. The same priority ordering applies. 64 * 65 * Note: [TemporaryViewInfo.id] is the identifier that we use to determine if a call to 66 * [displayView] will just update the current view with new information, or display a completely new 67 * view. This means that you *cannot* change the [TemporaryViewInfo.priority] or 68 * [TemporaryViewInfo.windowTitle] while using the same ID. 69 */ 70 abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : TemporaryViewLogger<T>>( 71 internal val context: Context, 72 internal val logger: U, 73 internal val windowManager: WindowManager, 74 @Main private val mainExecutor: DelayableExecutor, 75 private val accessibilityManager: AccessibilityManager, 76 private val configurationController: ConfigurationController, 77 private val dumpManager: DumpManager, 78 private val powerManager: PowerManager, 79 @LayoutRes private val viewLayoutRes: Int, 80 private val wakeLockBuilder: WakeLock.Builder, 81 private val systemClock: SystemClock, 82 internal val tempViewUiEventLogger: TemporaryViewUiEventLogger, 83 ) : CoreStartable, Dumpable { 84 /** 85 * Window layout params that will be used as a starting point for the [windowLayoutParams] of 86 * all subclasses. 87 */ 88 internal val commonWindowLayoutParams = WindowManager.LayoutParams().apply { 89 width = WindowManager.LayoutParams.WRAP_CONTENT 90 height = WindowManager.LayoutParams.WRAP_CONTENT 91 type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR 92 flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or 93 WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL 94 format = PixelFormat.TRANSLUCENT 95 setTrustedOverlay() 96 } 97 98 /** 99 * The window layout parameters we'll use when attaching the view to a window. 100 * 101 * Subclasses must override this to provide their specific layout params, and they should use 102 * [commonWindowLayoutParams] as part of their layout params. 103 */ 104 internal abstract val windowLayoutParams: WindowManager.LayoutParams 105 106 /** 107 * A list of the currently active views, ordered from highest priority in the beginning to 108 * lowest priority at the end. 109 * 110 * Whenever the current view disappears, the next-priority view will be displayed if it's still 111 * valid. 112 */ 113 @VisibleForTesting 114 internal val activeViews: MutableList<DisplayInfo> = mutableListOf() 115 116 internal fun getCurrentDisplayInfo(): DisplayInfo? { 117 return activeViews.getOrNull(0) 118 } 119 120 @CallSuper 121 override fun start() { 122 dumpManager.registerNormalDumpable(this) 123 } 124 125 private val listeners: MutableSet<Listener> = mutableSetOf() 126 127 /** Registers a listener. */ 128 fun registerListener(listener: Listener) { 129 listeners.add(listener) 130 } 131 132 /** Unregisters a listener. */ 133 fun unregisterListener(listener: Listener) { 134 listeners.remove(listener) 135 } 136 137 /** 138 * Displays the view with the provided [newInfo]. 139 * 140 * This method handles inflating and attaching the view, then delegates to [updateView] to 141 * display the correct information in the view. 142 */ 143 @Synchronized 144 fun displayView(newInfo: T) { 145 val timeout = accessibilityManager.getRecommendedTimeoutMillis( 146 newInfo.timeoutMs, 147 // Not all views have controls so FLAG_CONTENT_CONTROLS might be superfluous, but 148 // include it just to be safe. 149 FLAG_CONTENT_ICONS or FLAG_CONTENT_TEXT or FLAG_CONTENT_CONTROLS 150 ) 151 val timeExpirationMillis = systemClock.currentTimeMillis() + timeout 152 153 val currentDisplayInfo = getCurrentDisplayInfo() 154 155 // We're current displaying a chipbar with the same ID, we just need to update its info 156 if (currentDisplayInfo != null && currentDisplayInfo.info.id == newInfo.id) { 157 val view = checkNotNull(currentDisplayInfo.view) { 158 "First item in activeViews list must have a valid view" 159 } 160 logger.logViewUpdate(newInfo) 161 currentDisplayInfo.info = newInfo 162 currentDisplayInfo.timeExpirationMillis = timeExpirationMillis 163 updateTimeout(currentDisplayInfo, timeout) 164 updateView(newInfo, view) 165 return 166 } 167 168 val newDisplayInfo = DisplayInfo( 169 info = newInfo, 170 timeExpirationMillis = timeExpirationMillis, 171 // Null values will be updated to non-null if/when this view actually gets displayed 172 view = null, 173 wakeLock = null, 174 cancelViewTimeout = null, 175 ) 176 177 // We're not displaying anything, so just render this new info 178 if (currentDisplayInfo == null) { 179 addCallbacks() 180 activeViews.add(newDisplayInfo) 181 showNewView(newDisplayInfo, timeout) 182 return 183 } 184 185 // The currently displayed info takes higher priority than the new one. 186 // So, just store the new one in case the current one disappears. 187 if (currentDisplayInfo.info.priority > newInfo.priority) { 188 logger.logViewAdditionDelayed(newInfo) 189 // Remove any old information for this id (if it exists) and re-add it to the list in 190 // the right priority spot 191 removeFromActivesIfNeeded(newInfo.id) 192 var insertIndex = 0 193 while (insertIndex < activeViews.size && 194 activeViews[insertIndex].info.priority > newInfo.priority) { 195 insertIndex++ 196 } 197 activeViews.add(insertIndex, newDisplayInfo) 198 return 199 } 200 201 // Else: The newInfo should be displayed and the currentInfo should be hidden 202 hideView(currentDisplayInfo) 203 // Remove any old information for this id (if it exists) and put this info at the beginning 204 removeFromActivesIfNeeded(newDisplayInfo.info.id) 205 activeViews.add(0, newDisplayInfo) 206 showNewView(newDisplayInfo, timeout) 207 } 208 209 private fun showNewView(newDisplayInfo: DisplayInfo, timeout: Int) { 210 logger.logViewAddition(newDisplayInfo.info) 211 tempViewUiEventLogger.logViewAdded(newDisplayInfo.info.instanceId) 212 createAndAcquireWakeLock(newDisplayInfo) 213 updateTimeout(newDisplayInfo, timeout) 214 inflateAndUpdateView(newDisplayInfo) 215 } 216 217 private fun createAndAcquireWakeLock(displayInfo: DisplayInfo) { 218 // TODO(b/262009503): Migrate off of isScrenOn, since it's deprecated. 219 val newWakeLock = if (!powerManager.isScreenOn) { 220 // If the screen is off, fully wake it so the user can see the view. 221 wakeLockBuilder 222 .setTag(displayInfo.info.windowTitle) 223 .setLevelsAndFlags( 224 PowerManager.FULL_WAKE_LOCK or 225 PowerManager.ACQUIRE_CAUSES_WAKEUP 226 ) 227 .build() 228 } else { 229 // Per b/239426653, we want the view to show over the dream state. 230 // If the screen is on, using screen bright level will leave screen on the dream 231 // state but ensure the screen will not go off before wake lock is released. 232 wakeLockBuilder 233 .setTag(displayInfo.info.windowTitle) 234 .setLevelsAndFlags(PowerManager.SCREEN_BRIGHT_WAKE_LOCK) 235 .build() 236 } 237 displayInfo.wakeLock = newWakeLock 238 newWakeLock.acquire(displayInfo.info.wakeReason) 239 } 240 241 /** 242 * Creates a runnable that will remove [displayInfo] in [timeout] ms from now. 243 * 244 * @return a runnable that, when run, will *cancel* the view's timeout. 245 */ 246 private fun updateTimeout(displayInfo: DisplayInfo, timeout: Int) { 247 val cancelViewTimeout = mainExecutor.executeDelayed( 248 { 249 removeView(displayInfo.info.id, REMOVAL_REASON_TIMEOUT) 250 }, 251 timeout.toLong() 252 ) 253 254 // Cancel old view timeout and re-set it. 255 displayInfo.cancelViewTimeout?.run() 256 displayInfo.cancelViewTimeout = cancelViewTimeout 257 } 258 259 /** Inflates a new view, updates it with [DisplayInfo.info], and adds the view to the window. */ 260 private fun inflateAndUpdateView(displayInfo: DisplayInfo) { 261 val newInfo = displayInfo.info 262 val newView = LayoutInflater 263 .from(context) 264 .inflate(viewLayoutRes, null) as ViewGroup 265 displayInfo.view = newView 266 267 // We don't need to hold on to the view controller since we never set anything additional 268 // on it -- it will be automatically cleaned up when the view is detached. 269 val newViewController = TouchableRegionViewController(newView, this::getTouchableRegion) 270 newViewController.init() 271 272 updateView(newInfo, newView) 273 274 val paramsWithTitle = WindowManager.LayoutParams().also { 275 it.copyFrom(windowLayoutParams) 276 it.title = newInfo.windowTitle 277 } 278 newView.keepScreenOn = true 279 logger.logViewAddedToWindowManager(displayInfo.info, newView) 280 windowManager.addView(newView, paramsWithTitle) 281 animateViewIn(newView) 282 } 283 284 /** Removes then re-inflates the view. */ 285 @Synchronized 286 private fun reinflateView() { 287 val currentDisplayInfo = getCurrentDisplayInfo() ?: return 288 289 val view = checkNotNull(currentDisplayInfo.view) { 290 "First item in activeViews list must have a valid view" 291 } 292 logger.logViewRemovedFromWindowManager( 293 currentDisplayInfo.info, 294 view, 295 isReinflation = true, 296 ) 297 windowManager.removeView(view) 298 inflateAndUpdateView(currentDisplayInfo) 299 } 300 301 private val displayScaleListener = object : ConfigurationController.ConfigurationListener { 302 override fun onDensityOrFontScaleChanged() { 303 reinflateView() 304 } 305 306 override fun onThemeChanged() { 307 reinflateView() 308 } 309 } 310 311 private fun addCallbacks() { 312 configurationController.addCallback(displayScaleListener) 313 } 314 315 private fun removeCallbacks() { 316 configurationController.removeCallback(displayScaleListener) 317 } 318 319 /** 320 * Completely removes the view for the given [id], both visually and from our internal store. 321 * 322 * @param id the id of the device responsible of displaying the temp view. 323 * @param removalReason a short string describing why the view was removed (timeout, state 324 * change, etc.) 325 */ 326 @Synchronized 327 fun removeView(id: String, removalReason: String) { 328 logger.logViewRemoval(id, removalReason) 329 330 val displayInfo = activeViews.firstOrNull { it.info.id == id } 331 if (displayInfo == null) { 332 logger.logViewRemovalIgnored(id, "View not found in list") 333 return 334 } 335 336 val currentlyDisplayedView = activeViews[0] 337 // Remove immediately (instead as part of the animation end runnable) so that if a new view 338 // event comes in while this view is animating out, we still display the new view 339 // appropriately. 340 activeViews.remove(displayInfo) 341 listeners.forEach { 342 it.onInfoPermanentlyRemoved(id, removalReason) 343 } 344 345 // No need to time the view out since it's already gone 346 displayInfo.cancelViewTimeout?.run() 347 348 if (displayInfo.view == null) { 349 logger.logViewRemovalIgnored(id, "No view to remove") 350 return 351 } 352 353 if (currentlyDisplayedView.info.id != id) { 354 logger.logViewRemovalIgnored(id, "View isn't the currently displayed view") 355 return 356 } 357 358 removeViewFromWindow(displayInfo, removalReason) 359 360 // Prune anything that's already timed out before determining if we should re-display a 361 // different chipbar. 362 removeTimedOutViews() 363 val newViewToDisplay = getCurrentDisplayInfo() 364 365 if (newViewToDisplay != null) { 366 val timeout = newViewToDisplay.timeExpirationMillis - systemClock.currentTimeMillis() 367 // TODO(b/258019006): We may want to have a delay before showing the new view so 368 // that the UI translation looks a bit smoother. But, we expect this to happen 369 // rarely so it may not be worth the extra complexity. 370 showNewView(newViewToDisplay, timeout.toInt()) 371 } else { 372 removeCallbacks() 373 } 374 } 375 376 /** 377 * Hides the view from the window, but keeps [displayInfo] around in [activeViews] in case it 378 * should be re-displayed later. 379 */ 380 private fun hideView(displayInfo: DisplayInfo) { 381 logger.logViewHidden(displayInfo.info) 382 removeViewFromWindow(displayInfo) 383 } 384 385 private fun removeViewFromWindow(displayInfo: DisplayInfo, removalReason: String? = null) { 386 val view = displayInfo.view 387 if (view == null) { 388 logger.logViewRemovalIgnored(displayInfo.info.id, "View is null") 389 return 390 } 391 displayInfo.view = null // Need other places?? 392 animateViewOut(view, removalReason) { 393 logger.logViewRemovedFromWindowManager(displayInfo.info, view) 394 windowManager.removeView(view) 395 displayInfo.wakeLock?.release(displayInfo.info.wakeReason) 396 } 397 } 398 399 @Synchronized 400 private fun removeTimedOutViews() { 401 val invalidViews = activeViews 402 .filter { it.timeExpirationMillis < 403 systemClock.currentTimeMillis() + MIN_REQUIRED_TIME_FOR_REDISPLAY } 404 405 invalidViews.forEach { 406 activeViews.remove(it) 407 logger.logViewExpiration(it.info) 408 listeners.forEach { listener -> 409 listener.onInfoPermanentlyRemoved(it.info.id, REMOVAL_REASON_TIME_EXPIRED) 410 } 411 } 412 } 413 414 @Synchronized 415 private fun removeFromActivesIfNeeded(id: String) { 416 val toRemove = activeViews.find { it.info.id == id } 417 toRemove?.let { 418 it.cancelViewTimeout?.run() 419 activeViews.remove(it) 420 } 421 } 422 423 @Synchronized 424 @CallSuper 425 override fun dump(pw: PrintWriter, args: Array<out String>) { 426 pw.println("Current time millis: ${systemClock.currentTimeMillis()}") 427 pw.println("Active views size: ${activeViews.size}") 428 activeViews.forEachIndexed { index, displayInfo -> 429 pw.println("View[$index]:") 430 pw.println(" info=${displayInfo.info}") 431 pw.println(" hasView=${displayInfo.view != null}") 432 pw.println(" timeExpiration=${displayInfo.timeExpirationMillis}") 433 } 434 } 435 436 /** 437 * A method implemented by subclasses to update [currentView] based on [newInfo]. 438 */ 439 abstract fun updateView(newInfo: T, currentView: ViewGroup) 440 441 /** 442 * Fills [outRect] with the touchable region of this view. This will be used by WindowManager 443 * to decide which touch events go to the view. 444 */ 445 abstract fun getTouchableRegion(view: View, outRect: Rect) 446 447 /** 448 * A method that can be implemented by subclasses to do custom animations for when the view 449 * appears. 450 */ 451 internal open fun animateViewIn(view: ViewGroup) {} 452 453 /** 454 * A method that can be implemented by subclasses to do custom animations for when the view 455 * disappears. 456 * 457 * @param onAnimationEnd an action that *must* be run once the animation finishes successfully. 458 */ 459 internal open fun animateViewOut( 460 view: ViewGroup, 461 removalReason: String? = null, 462 onAnimationEnd: Runnable 463 ) { 464 onAnimationEnd.run() 465 } 466 467 /** A listener interface to be notified of various view events. */ 468 fun interface Listener { 469 /** 470 * Called whenever a [DisplayInfo] with the given [id] has been removed and will never be 471 * displayed again (unless another call to [updateView] is made). 472 */ 473 fun onInfoPermanentlyRemoved(id: String, reason: String) 474 } 475 476 /** A container for all the display-related state objects. */ 477 inner class DisplayInfo( 478 /** 479 * The view currently being displayed. 480 * 481 * Null if this info isn't currently being displayed. 482 */ 483 var view: ViewGroup?, 484 485 /** The info that should be displayed if/when this is the highest priority view. */ 486 var info: T, 487 488 /** 489 * The system time at which this display info should expire and never be displayed again. 490 */ 491 var timeExpirationMillis: Long, 492 493 /** 494 * The wake lock currently held by this view. Must be released when the view disappears. 495 * 496 * Null if this info isn't currently being displayed. 497 */ 498 var wakeLock: WakeLock?, 499 500 /** 501 * A runnable that, when run, will cancel this view's timeout. 502 * 503 * Null if this info isn't currently being displayed. 504 */ 505 var cancelViewTimeout: Runnable?, 506 ) 507 } 508 509 private const val REMOVAL_REASON_TIMEOUT = "TIMEOUT" 510 private const val REMOVAL_REASON_TIME_EXPIRED = "TIMEOUT_EXPIRED_BEFORE_REDISPLAY" 511 private const val MIN_REQUIRED_TIME_FOR_REDISPLAY = 1000 512 513 private data class IconInfo( 514 val iconName: String, 515 val icon: Drawable, 516 /** True if [icon] is the app's icon, and false if [icon] is some generic default icon. */ 517 val isAppIcon: Boolean 518 ) 519