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