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