• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  *  Copyright (C) 2022 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 
18 package com.android.systemui.lifecycle
19 
20 import android.os.Trace
21 import android.view.View
22 import android.view.ViewTreeObserver
23 import androidx.annotation.MainThread
24 import androidx.lifecycle.Lifecycle
25 import androidx.lifecycle.LifecycleOwner
26 import androidx.lifecycle.LifecycleRegistry
27 import androidx.lifecycle.lifecycleScope
28 import com.android.app.tracing.coroutines.createCoroutineTracingContext
29 import com.android.app.tracing.coroutines.launch
30 import com.android.systemui.Flags.coroutineTracing
31 import com.android.systemui.util.Assert
32 import com.android.systemui.util.Compile
33 import kotlin.coroutines.CoroutineContext
34 import kotlin.coroutines.EmptyCoroutineContext
35 import kotlinx.coroutines.Dispatchers
36 import kotlinx.coroutines.DisposableHandle
37 
38 /**
39  * Runs the given [block] every time the [View] becomes attached (or immediately after calling this
40  * function, if the view was already attached), automatically canceling the work when the `View`
41  * becomes detached.
42  *
43  * Only use from the main thread.
44  *
45  * When [block] is run, it is run in the context of a [ViewLifecycleOwner] which the caller can use
46  * to launch jobs, with confidence that the jobs will be properly canceled when the view is
47  * detached.
48  *
49  * The [block] may be run multiple times, running once per every time the view is attached. Each
50  * time the block is run for a new attachment event, the [ViewLifecycleOwner] provided will be a
51  * fresh one.
52  *
53  * @param coroutineContext An optional [CoroutineContext] to replace the dispatcher [block] is
54  *   invoked on.
55  * @param block The block of code that should be run when the view becomes attached. It can end up
56  *   being invoked multiple times if the view is reattached after being detached.
57  * @return A [DisposableHandle] to invoke when the caller of the function destroys its [View] and is
58  *   no longer interested in the [block] being run the next time its attached. Calling this is an
59  *   optional optimization as the logic will be properly cleaned up and destroyed each time the view
60  *   is detached. Using this is not *thread-safe* and should only be used on the main thread.
61  */
62 @MainThread
63 fun View.repeatWhenAttached(
64     coroutineContext: CoroutineContext = EmptyCoroutineContext,
65     block: suspend LifecycleOwner.(View) -> Unit,
66 ): DisposableHandle {
67     Assert.isMainThread()
68     val view = this
69     // The suspend block will run on the app's main thread unless the caller supplies a different
70     // dispatcher to use. We don't want it to run on the Dispatchers.Default thread pool as
71     // default behavior. Instead, we want it to run on the view's UI thread since the user will
72     // presumably want to call view methods that require being called from said UI thread.
73     val lifecycleCoroutineContext = MAIN_DISPATCHER_SINGLETON + coroutineContext
74     val traceName =
75         if (Compile.IS_DEBUG && coroutineTracing()) {
76             inferTraceSectionName()
77         } else {
78             DEFAULT_TRACE_NAME
79         }
80     var lifecycleOwner: ViewLifecycleOwner? = null
81     val onAttachListener =
82         object : View.OnAttachStateChangeListener {
83             override fun onViewAttachedToWindow(v: View) {
84                 Assert.isMainThread()
85                 lifecycleOwner?.onDestroy()
86                 lifecycleOwner =
87                     createLifecycleOwnerAndRun(
88                         traceName,
89                         view,
90                         lifecycleCoroutineContext,
91                         block,
92                     )
93             }
94 
95             override fun onViewDetachedFromWindow(v: View) {
96                 lifecycleOwner?.onDestroy()
97                 lifecycleOwner = null
98             }
99         }
100 
101     addOnAttachStateChangeListener(onAttachListener)
102     if (view.isAttachedToWindow) {
103         lifecycleOwner =
104             createLifecycleOwnerAndRun(
105                 traceName,
106                 view,
107                 lifecycleCoroutineContext,
108                 block,
109             )
110     }
111 
112     return DisposableHandle {
113         Assert.isMainThread()
114 
115         lifecycleOwner?.onDestroy()
116         lifecycleOwner = null
117         view.removeOnAttachStateChangeListener(onAttachListener)
118     }
119 }
120 
createLifecycleOwnerAndRunnull121 private fun createLifecycleOwnerAndRun(
122     nameForTrace: String,
123     view: View,
124     coroutineContext: CoroutineContext,
125     block: suspend LifecycleOwner.(View) -> Unit,
126 ): ViewLifecycleOwner {
127     return ViewLifecycleOwner(view).apply {
128         onCreate()
129         lifecycleScope.launch(nameForTrace, coroutineContext) { block(view) }
130     }
131 }
132 
133 /**
134  * A [LifecycleOwner] for a [View] for exclusive use by the [repeatWhenAttached] extension function.
135  *
136  * The implementation requires the caller to call [onCreate] and [onDestroy] when the view is
137  * attached to or detached from a view hierarchy. After [onCreate] and before [onDestroy] is called,
138  * the implementation monitors window state in the following way
139  * * If the window is not visible, we are in the [Lifecycle.State.CREATED] state
140  * * If the window is visible but not focused, we are in the [Lifecycle.State.STARTED] state
141  * * If the window is visible and focused, we are in the [Lifecycle.State.RESUMED] state
142  *
143  * Or in table format:
144  * ```
145  * ┌───────────────┬───────────────────┬──────────────┬─────────────────┐
146  * │ View attached │ Window Visibility │ Window Focus │ Lifecycle State │
147  * ├───────────────┼───────────────────┴──────────────┼─────────────────┤
148  * │ Not attached  │                 Any              │       N/A       │
149  * ├───────────────┼───────────────────┬──────────────┼─────────────────┤
150  * │               │    Not visible    │     Any      │     CREATED     │
151  * │               ├───────────────────┼──────────────┼─────────────────┤
152  * │   Attached    │                   │   No focus   │     STARTED     │
153  * │               │      Visible      ├──────────────┼─────────────────┤
154  * │               │                   │  Has focus   │     RESUMED     │
155  * └───────────────┴───────────────────┴──────────────┴─────────────────┘
156  * ```
157  */
158 class ViewLifecycleOwner(
159     private val view: View,
160 ) : LifecycleOwner {
161 
162     private val windowVisibleListener =
<lambda>null163         ViewTreeObserver.OnWindowVisibilityChangeListener { updateState() }
<lambda>null164     private val windowFocusListener = ViewTreeObserver.OnWindowFocusChangeListener { updateState() }
165 
166     private val registry = LifecycleRegistry(this)
167 
onCreatenull168     fun onCreate() {
169         registry.currentState = Lifecycle.State.CREATED
170         view.viewTreeObserver.addOnWindowVisibilityChangeListener(windowVisibleListener)
171         view.viewTreeObserver.addOnWindowFocusChangeListener(windowFocusListener)
172         updateState()
173     }
174 
onDestroynull175     fun onDestroy() {
176         view.viewTreeObserver.removeOnWindowVisibilityChangeListener(windowVisibleListener)
177         view.viewTreeObserver.removeOnWindowFocusChangeListener(windowFocusListener)
178         registry.currentState = Lifecycle.State.DESTROYED
179     }
180 
181     override val lifecycle: Lifecycle
182         get() {
183             return registry
184         }
185 
updateStatenull186     private fun updateState() {
187         registry.currentState =
188             when {
189                 view.windowVisibility != View.VISIBLE -> Lifecycle.State.CREATED
190                 !view.hasWindowFocus() -> Lifecycle.State.STARTED
191                 else -> Lifecycle.State.RESUMED
192             }
193     }
194 }
195 
isFrameInterestingnull196 private fun isFrameInteresting(frame: StackWalker.StackFrame): Boolean =
197     frame.className != CURRENT_CLASS_NAME && frame.className != JAVA_ADAPTER_CLASS_NAME
198 
199 /** Get a name for the trace section include the name of the call site. */
200 private fun inferTraceSectionName(): String {
201     try {
202         Trace.traceBegin(Trace.TRACE_TAG_APP, "RepeatWhenAttachedKt#inferTraceSectionName")
203         val interestingFrame =
204             StackWalker.getInstance().walk { stream ->
205                 stream.filter(::isFrameInteresting).limit(5).findFirst()
206             }
207         return if (interestingFrame.isPresent) {
208             val f = interestingFrame.get()
209             "${f.className}#${f.methodName}:${f.lineNumber} [$DEFAULT_TRACE_NAME]"
210         } else {
211             DEFAULT_TRACE_NAME
212         }
213     } finally {
214         Trace.traceEnd(Trace.TRACE_TAG_APP)
215     }
216 }
217 
218 /**
219  * Even though there is only has one usage of `Dispatchers.Main` in this file, we cache it in a
220  * top-level property so that we do not unnecessarily create new `CoroutineContext` objects for
221  * tracing on each call to [repeatWhenAttached]. It is okay to reuse a single instance of the
222  * tracing context because it is copied for its children.
223  *
224  * Also, ideally, we would use the injected `@Main CoroutineDispatcher`, but [repeatWhenAttached] is
225  * an extension function, and plumbing dagger-injected instances for static usage has little
226  * benefit.
227  */
228 private val MAIN_DISPATCHER_SINGLETON = Dispatchers.Main + createCoroutineTracingContext()
229 private const val DEFAULT_TRACE_NAME = "repeatWhenAttached"
230 private const val CURRENT_CLASS_NAME = "com.android.systemui.lifecycle.RepeatWhenAttachedKt"
231 private const val JAVA_ADAPTER_CLASS_NAME = "com.android.systemui.util.kotlin.JavaAdapterKt"
232