• 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.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