1 /*
2 * 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.util.Assert
28 import kotlin.coroutines.CoroutineContext
29 import kotlin.coroutines.EmptyCoroutineContext
30 import kotlinx.coroutines.Dispatchers
31 import kotlinx.coroutines.DisposableHandle
32 import kotlinx.coroutines.launch
33
34 /**
35 * Runs the given [block] every time the [View] becomes attached (or immediately after calling this
36 * function, if the view was already attached), automatically canceling the work when the `View`
37 * becomes detached.
38 *
39 * Only use from the main thread.
40 *
41 * When [block] is run, it is run in the context of a [ViewLifecycleOwner] which the caller can use
42 * to launch jobs, with confidence that the jobs will be properly canceled when the view is
43 * detached.
44 *
45 * The [block] may be run multiple times, running once per every time the view is attached. Each
46 * time the block is run for a new attachment event, the [ViewLifecycleOwner] provided will be a
47 * fresh one.
48 *
49 * @param coroutineContext An optional [CoroutineContext] to replace the dispatcher [block] is
50 * invoked on.
51 * @param block The block of code that should be run when the view becomes attached. It can end up
52 * being invoked multiple times if the view is reattached after being detached.
53 * @return A [DisposableHandle] to invoke when the caller of the function destroys its [View] and is
54 * no longer interested in the [block] being run the next time its attached. Calling this is an
55 * optional optimization as the logic will be properly cleaned up and destroyed each time the view
56 * is detached. Using this is not *thread-safe* and should only be used on the main thread.
57 */
58 @MainThread
repeatWhenAttachednull59 fun View.repeatWhenAttached(
60 coroutineContext: CoroutineContext = EmptyCoroutineContext,
61 block: suspend LifecycleOwner.(View) -> Unit,
62 ): DisposableHandle {
63 Assert.isMainThread()
64 val view = this
65 // The suspend block will run on the app's main thread unless the caller supplies a different
66 // dispatcher to use. We don't want it to run on the Dispatchers.Default thread pool as
67 // default behavior. Instead, we want it to run on the view's UI thread since the user will
68 // presumably want to call view methods that require being called from said UI thread.
69 val lifecycleCoroutineContext = Dispatchers.Main + coroutineContext
70 var lifecycleOwner: ViewLifecycleOwner? = null
71 val onAttachListener =
72 object : View.OnAttachStateChangeListener {
73 override fun onViewAttachedToWindow(v: View?) {
74 Assert.isMainThread()
75 lifecycleOwner?.onDestroy()
76 lifecycleOwner =
77 createLifecycleOwnerAndRun(
78 view,
79 lifecycleCoroutineContext,
80 block,
81 )
82 }
83
84 override fun onViewDetachedFromWindow(v: View?) {
85 lifecycleOwner?.onDestroy()
86 lifecycleOwner = null
87 }
88 }
89
90 addOnAttachStateChangeListener(onAttachListener)
91 if (view.isAttachedToWindow) {
92 lifecycleOwner =
93 createLifecycleOwnerAndRun(
94 view,
95 lifecycleCoroutineContext,
96 block,
97 )
98 }
99
100 return object : DisposableHandle {
101 override fun dispose() {
102 Assert.isMainThread()
103
104 lifecycleOwner?.onDestroy()
105 lifecycleOwner = null
106 view.removeOnAttachStateChangeListener(onAttachListener)
107 }
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 lifecycleScope.launch(coroutineContext) { block(view) }
119 }
120 }
121
122 /**
123 * A [LifecycleOwner] for a [View] for exclusive use by the [repeatWhenAttached] extension function.
124 *
125 * The implementation requires the caller to call [onCreate] and [onDestroy] when the view is
126 * attached to or detached from a view hierarchy. After [onCreate] and before [onDestroy] is called,
127 * the implementation monitors window state in the following way
128 * * If the window is not visible, we are in the [Lifecycle.State.CREATED] state
129 * * If the window is visible but not focused, we are in the [Lifecycle.State.STARTED] state
130 * * If the window is visible and focused, we are in the [Lifecycle.State.RESUMED] state
131 *
132 * Or in table format:
133 * ```
134 * ┌───────────────┬───────────────────┬──────────────┬─────────────────┐
135 * │ View attached │ Window Visibility │ Window Focus │ Lifecycle State │
136 * ├───────────────┼───────────────────┴──────────────┼─────────────────┤
137 * │ Not attached │ Any │ N/A │
138 * ├───────────────┼───────────────────┬──────────────┼─────────────────┤
139 * │ │ Not visible │ Any │ CREATED │
140 * │ ├───────────────────┼──────────────┼─────────────────┤
141 * │ Attached │ │ No focus │ STARTED │
142 * │ │ Visible ├──────────────┼─────────────────┤
143 * │ │ │ Has focus │ RESUMED │
144 * └───────────────┴───────────────────┴──────────────┴─────────────────┘
145 * ```
146 */
147 class ViewLifecycleOwner(
148 private val view: View,
149 ) : 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
getLifecyclenull170 override fun getLifecycle(): Lifecycle {
171 return registry
172 }
173
updateStatenull174 private fun updateState() {
175 registry.currentState =
176 when {
177 view.windowVisibility != View.VISIBLE -> Lifecycle.State.CREATED
178 !view.hasWindowFocus() -> Lifecycle.State.STARTED
179 else -> Lifecycle.State.RESUMED
180 }
181 }
182 }
183