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