• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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