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.view.View
21 import android.view.ViewTreeObserver
22 import androidx.annotation.MainThread
23 import androidx.lifecycle.Lifecycle
24 import androidx.lifecycle.LifecycleOwner
25 import androidx.lifecycle.LifecycleRegistry
26 import androidx.lifecycle.lifecycleScope
27 import com.android.systemui.coroutines.newTracingContext
28 import com.android.systemui.util.Assert
29 import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
30 import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
31 import kotlin.coroutines.CoroutineContext
32 import kotlin.coroutines.EmptyCoroutineContext
33 import kotlinx.coroutines.CoroutineScope
34 import kotlinx.coroutines.Dispatchers
35 import kotlinx.coroutines.DisposableHandle
36 import kotlinx.coroutines.awaitCancellation
37 import kotlinx.coroutines.channels.awaitClose
38 import kotlinx.coroutines.coroutineScope
39 import kotlinx.coroutines.flow.Flow
40 import kotlinx.coroutines.flow.collectLatest
41 import kotlinx.coroutines.flow.emptyFlow
42 import kotlinx.coroutines.flow.map
43 import kotlinx.coroutines.flow.onStart
44 import com.android.app.tracing.coroutines.launchTraced as launch
45
46 /**
47 * Runs the given [block] every time the [View] becomes attached (or immediately after calling this
48 * function, if the view was already attached), automatically canceling the work when the `View`
49 * becomes detached.
50 *
51 * Only use from the main thread.
52 *
53 * When [block] is run, it is run in the context of a [ViewLifecycleOwner] which the caller can use
54 * to launch jobs, with confidence that the jobs will be properly canceled when the view is
55 * detached.
56 *
57 * The [block] may be run multiple times, running once per every time the view is attached. Each
58 * time the block is run for a new attachment event, the [ViewLifecycleOwner] provided will be a
59 * fresh one.
60 *
61 * @param coroutineContext An optional [CoroutineContext] to replace the dispatcher [block] is
62 * invoked on.
63 * @param block The block of code that should be run when the view becomes attached. It can end up
64 * being invoked multiple times if the view is reattached after being detached.
65 * @return A [DisposableHandle] to invoke when the caller of the function destroys its [View] and is
66 * no longer interested in the [block] being run the next time its attached. Calling this is an
67 * optional optimization as the logic will be properly cleaned up and destroyed each time the view
68 * is detached. Using this is not *thread-safe* and should only be used on the main thread.
69 */
70 @MainThread
71 fun View.repeatWhenAttached(
72 coroutineContext: CoroutineContext = EmptyCoroutineContext,
73 block: suspend LifecycleOwner.(View) -> Unit,
74 ): DisposableHandle {
75 Assert.isMainThread()
76 val view = this
77 // The suspend block will run on the app's main thread unless the caller supplies a different
78 // dispatcher to use. We don't want it to run on the Dispatchers.Default thread pool as
79 // default behavior. Instead, we want it to run on the view's UI thread since the user will
80 // presumably want to call view methods that require being called from said UI thread.
81 val lifecycleCoroutineContext = MAIN_DISPATCHER_SINGLETON + coroutineContext
82 var lifecycleOwner: ViewLifecycleOwner? = null
83 val onAttachListener =
84 object : View.OnAttachStateChangeListener {
85 override fun onViewAttachedToWindow(v: View) {
86 Assert.isMainThread()
87 lifecycleOwner?.onDestroy()
88 lifecycleOwner = createLifecycleOwnerAndRun(view, lifecycleCoroutineContext, block)
89 }
90
91 override fun onViewDetachedFromWindow(v: View) {
92 lifecycleOwner?.onDestroy()
93 lifecycleOwner = null
94 }
95 }
96
97 addOnAttachStateChangeListener(onAttachListener)
98 if (view.isAttachedToWindow) {
99 lifecycleOwner = createLifecycleOwnerAndRun(view, lifecycleCoroutineContext, block)
100 }
101
102 return DisposableHandle {
103 Assert.isMainThread()
104
105 lifecycleOwner?.onDestroy()
106 lifecycleOwner = null
107 view.removeOnAttachStateChangeListener(onAttachListener)
108 }
109 }
110
createLifecycleOwnerAndRunnull111 private fun createLifecycleOwnerAndRun(
112 view: View,
113 coroutineContext: CoroutineContext,
114 block: suspend LifecycleOwner.(View) -> Unit,
115 ): ViewLifecycleOwner {
116 return ViewLifecycleOwner(view).apply {
117 onCreate()
118 // TODO(b/370595466): Refactor to support installing CoroutineTracingContext on the
119 // top-level CoroutineScope used as the lifecycleScope
120 lifecycleScope.launch(context = coroutineContext) { block(view) }
121 }
122 }
123
124 /**
125 * A [LifecycleOwner] for a [View] for exclusive use by the [repeatWhenAttached] extension function.
126 *
127 * The implementation requires the caller to call [onCreate] and [onDestroy] when the view is
128 * attached to or detached from a view hierarchy. After [onCreate] and before [onDestroy] is called,
129 * the implementation monitors window state in the following way
130 * * If the window is not visible, we are in the [Lifecycle.State.CREATED] state
131 * * If the window is visible but not focused, we are in the [Lifecycle.State.STARTED] state
132 * * If the window is visible and focused, we are in the [Lifecycle.State.RESUMED] state
133 *
134 * Or in table format:
135 * ```
136 * ┌───────────────┬───────────────────┬──────────────┬─────────────────┐
137 * │ View attached │ Window Visibility │ Window Focus │ Lifecycle State │
138 * ├───────────────┼───────────────────┴──────────────┼─────────────────┤
139 * │ Not attached │ Any │ N/A │
140 * ├───────────────┼───────────────────┬──────────────┼─────────────────┤
141 * │ │ Not visible │ Any │ CREATED │
142 * │ ├───────────────────┼──────────────┼─────────────────┤
143 * │ Attached │ │ No focus │ STARTED │
144 * │ │ Visible ├──────────────┼─────────────────┤
145 * │ │ │ Has focus │ RESUMED │
146 * └───────────────┴───────────────────┴──────────────┴─────────────────┘
147 * ```
148 */
149 class ViewLifecycleOwner(private val view: View) : LifecycleOwner {
150
151 private val windowVisibleListener =
<lambda>null152 ViewTreeObserver.OnWindowVisibilityChangeListener { updateState() }
<lambda>null153 private val windowFocusListener = ViewTreeObserver.OnWindowFocusChangeListener { updateState() }
154
155 private val registry = LifecycleRegistry(this)
156
onCreatenull157 fun onCreate() {
158 registry.currentState = Lifecycle.State.CREATED
159 view.viewTreeObserver.addOnWindowVisibilityChangeListener(windowVisibleListener)
160 view.viewTreeObserver.addOnWindowFocusChangeListener(windowFocusListener)
161 updateState()
162 }
163
onDestroynull164 fun onDestroy() {
165 view.viewTreeObserver.removeOnWindowVisibilityChangeListener(windowVisibleListener)
166 view.viewTreeObserver.removeOnWindowFocusChangeListener(windowFocusListener)
167 registry.currentState = Lifecycle.State.DESTROYED
168 }
169
170 override val lifecycle: Lifecycle
171 get() {
172 return registry
173 }
174
updateStatenull175 private fun updateState() {
176 registry.currentState =
177 when {
178 view.windowVisibility != View.VISIBLE -> Lifecycle.State.CREATED
179 !view.hasWindowFocus() -> Lifecycle.State.STARTED
180 else -> Lifecycle.State.RESUMED
181 }
182 }
183 }
184
185 /**
186 * Runs the given [block] in a new coroutine when `this` [View]'s Window's [WindowLifecycleState] is
187 * at least at [state] (or immediately after calling this function if the window is already at least
188 * at [state]), automatically canceling the work when the window is no longer at least at that
189 * state.
190 *
191 * [block] may be run multiple times, running once per every time this` [View]'s Window's
192 * [WindowLifecycleState] becomes at least at [state].
193 */
repeatOnWindowLifecyclenull194 suspend fun View.repeatOnWindowLifecycle(
195 state: WindowLifecycleState,
196 block: suspend CoroutineScope.() -> Unit,
197 ): Nothing {
198 when (state) {
199 WindowLifecycleState.ATTACHED -> repeatWhenAttachedToWindow(block)
200 WindowLifecycleState.VISIBLE -> repeatWhenWindowIsVisible(block)
201 WindowLifecycleState.FOCUSED -> repeatWhenWindowHasFocus(block)
202 }
203 }
204
205 /**
206 * Runs the given [block] every time the [View] becomes attached (or immediately after calling this
207 * function, if the view was already attached), automatically canceling the work when the view
208 * becomes detached.
209 *
210 * Only use from the main thread.
211 *
212 * [block] may be run multiple times, running once per every time the view is attached.
213 */
214 @MainThread
repeatWhenAttachedToWindownull215 suspend fun View.repeatWhenAttachedToWindow(block: suspend CoroutineScope.() -> Unit): Nothing {
216 Assert.isMainThread()
217 isAttached.collectLatest { if (it) coroutineScope { block() } }
218 awaitCancellation() // satisfies return type of Nothing
219 }
220
221 /**
222 * Runs the given [block] every time the [Window] this [View] is attached to becomes visible (or
223 * immediately after calling this function, if the window is already visible), automatically
224 * canceling the work when the window becomes invisible.
225 *
226 * Only use from the main thread.
227 *
228 * [block] may be run multiple times, running once per every time the window becomes visible.
229 */
230 @MainThread
repeatWhenWindowIsVisiblenull231 suspend fun View.repeatWhenWindowIsVisible(block: suspend CoroutineScope.() -> Unit): Nothing {
232 Assert.isMainThread()
233 isWindowVisible.collectLatest { if (it) coroutineScope { block() } }
234 awaitCancellation() // satisfies return type of Nothing
235 }
236
237 /**
238 * Runs the given [block] every time the [Window] this [View] is attached to has focus (or
239 * immediately after calling this function, if the window is already focused), automatically
240 * canceling the work when the window loses focus.
241 *
242 * Only use from the main thread.
243 *
244 * [block] may be run multiple times, running once per every time the window is focused.
245 */
246 @MainThread
repeatWhenWindowHasFocusnull247 suspend fun View.repeatWhenWindowHasFocus(block: suspend CoroutineScope.() -> Unit): Nothing {
248 Assert.isMainThread()
249 isWindowFocused.collectLatest { if (it) coroutineScope { block() } }
250 awaitCancellation() // satisfies return type of Nothing
251 }
252
253 /** Lifecycle states for a [View]'s interaction with a [android.view.Window]. */
254 enum class WindowLifecycleState {
255 /** Indicates that the [View] is attached to a [android.view.Window]. */
256 ATTACHED,
257 /**
258 * Indicates that the [View] is attached to a [android.view.Window], and the window is visible.
259 */
260 VISIBLE,
261 /**
262 * Indicates that the [View] is attached to a [android.view.Window], and the window is visible
263 * and focused.
264 */
265 FOCUSED
266 }
267
268 private val View.isAttached
<lambda>null269 get() = conflatedCallbackFlow {
270 val onAttachListener =
271 object : View.OnAttachStateChangeListener {
272 override fun onViewAttachedToWindow(v: View) {
273 Assert.isMainThread()
274 trySend(true)
275 }
276
277 override fun onViewDetachedFromWindow(v: View) {
278 trySend(false)
279 }
280 }
281 addOnAttachStateChangeListener(onAttachListener)
282 trySend(isAttachedToWindow)
283 awaitClose { removeOnAttachStateChangeListener(onAttachListener) }
284 }
285
286 private val View.currentViewTreeObserver: Flow<ViewTreeObserver?>
<lambda>null287 get() = isAttached.map { if (it) viewTreeObserver else null }
288
289 private val View.isWindowVisible
290 get() =
vtonull291 currentViewTreeObserver.flatMapLatestConflated { vto ->
292 vto?.isWindowVisible?.onStart { emit(windowVisibility == View.VISIBLE) } ?: emptyFlow()
293 }
294
295 private val View.isWindowFocused
296 get() =
vtonull297 currentViewTreeObserver.flatMapLatestConflated { vto ->
298 vto?.isWindowFocused?.onStart { emit(hasWindowFocus()) } ?: emptyFlow()
299 }
300
301 private val ViewTreeObserver.isWindowFocused
<lambda>null302 get() = conflatedCallbackFlow {
303 val listener = ViewTreeObserver.OnWindowFocusChangeListener { trySend(it) }
304 addOnWindowFocusChangeListener(listener)
305 awaitClose { removeOnWindowFocusChangeListener(listener) }
306 }
307
308 private val ViewTreeObserver.isWindowVisible
<lambda>null309 get() = conflatedCallbackFlow {
310 val listener =
311 ViewTreeObserver.OnWindowVisibilityChangeListener { v -> trySend(v == View.VISIBLE) }
312 addOnWindowVisibilityChangeListener(listener)
313 awaitClose { removeOnWindowVisibilityChangeListener(listener) }
314 }
315
316 /**
317 * Even though there is only has one usage of `Dispatchers.Main` in this file, we cache it in a
318 * top-level property so that we do not unnecessarily create new `CoroutineContext` objects for
319 * tracing on each call to [repeatWhenAttached]. It is okay to reuse a single instance of the
320 * tracing context because it is copied for its children.
321 *
322 * Also, ideally, we would use the injected `@Main CoroutineDispatcher`, but [repeatWhenAttached] is
323 * an extension function, and plumbing dagger-injected instances for static usage has little
324 * benefit.
325 */
326 private val MAIN_DISPATCHER_SINGLETON = Dispatchers.Main + newTracingContext("RepeatWhenAttached")
327 private const val DEFAULT_TRACE_NAME = "repeatWhenAttached"
328 private const val CURRENT_CLASS_NAME = "com.android.systemui.lifecycle.RepeatWhenAttachedKt"
329 private const val JAVA_ADAPTER_CLASS_NAME = "com.android.systemui.util.kotlin.JavaAdapterKt"
330