1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
|
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.android.systemui.lifecycle
import android.view.View
import android.view.ViewTreeObserver
import androidx.annotation.MainThread
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.lifecycleScope
import com.android.systemui.util.Assert
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.launch
/**
* Runs the given [block] every time the [View] becomes attached (or immediately after calling this
* function, if the view was already attached), automatically canceling the work when the `View`
* becomes detached.
*
* Only use from the main thread.
*
* When [block] is run, it is run in the context of a [ViewLifecycleOwner] which the caller can use
* to launch jobs, with confidence that the jobs will be properly canceled when the view is
* detached.
*
* The [block] may be run multiple times, running once per every time the view is attached. Each
* time the block is run for a new attachment event, the [ViewLifecycleOwner] provided will be a
* fresh one.
*
* @param coroutineContext An optional [CoroutineContext] to replace the dispatcher [block] is
* invoked on.
* @param block The block of code that should be run when the view becomes attached. It can end up
* being invoked multiple times if the view is reattached after being detached.
* @return A [DisposableHandle] to invoke when the caller of the function destroys its [View] and is
* no longer interested in the [block] being run the next time its attached. Calling this is an
* optional optimization as the logic will be properly cleaned up and destroyed each time the view
* is detached. Using this is not *thread-safe* and should only be used on the main thread.
*/
@MainThread
fun View.repeatWhenAttached(
coroutineContext: CoroutineContext = EmptyCoroutineContext,
block: suspend LifecycleOwner.(View) -> Unit,
): DisposableHandle {
Assert.isMainThread()
val view = this
// The suspend block will run on the app's main thread unless the caller supplies a different
// dispatcher to use. We don't want it to run on the Dispatchers.Default thread pool as
// default behavior. Instead, we want it to run on the view's UI thread since the user will
// presumably want to call view methods that require being called from said UI thread.
val lifecycleCoroutineContext = Dispatchers.Main + coroutineContext
var lifecycleOwner: ViewLifecycleOwner? = null
val onAttachListener =
object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View?) {
Assert.isMainThread()
lifecycleOwner?.onDestroy()
lifecycleOwner =
createLifecycleOwnerAndRun(
view,
lifecycleCoroutineContext,
block,
)
}
override fun onViewDetachedFromWindow(v: View?) {
lifecycleOwner?.onDestroy()
lifecycleOwner = null
}
}
addOnAttachStateChangeListener(onAttachListener)
if (view.isAttachedToWindow) {
lifecycleOwner =
createLifecycleOwnerAndRun(
view,
lifecycleCoroutineContext,
block,
)
}
return object : DisposableHandle {
override fun dispose() {
Assert.isMainThread()
lifecycleOwner?.onDestroy()
lifecycleOwner = null
view.removeOnAttachStateChangeListener(onAttachListener)
}
}
}
private fun createLifecycleOwnerAndRun(
view: View,
coroutineContext: CoroutineContext,
block: suspend LifecycleOwner.(View) -> Unit,
): ViewLifecycleOwner {
return ViewLifecycleOwner(view).apply {
onCreate()
lifecycleScope.launch(coroutineContext) { block(view) }
}
}
/**
* A [LifecycleOwner] for a [View] for exclusive use by the [repeatWhenAttached] extension function.
*
* The implementation requires the caller to call [onCreate] and [onDestroy] when the view is
* attached to or detached from a view hierarchy. After [onCreate] and before [onDestroy] is called,
* the implementation monitors window state in the following way
* * If the window is not visible, we are in the [Lifecycle.State.CREATED] state
* * If the window is visible but not focused, we are in the [Lifecycle.State.STARTED] state
* * If the window is visible and focused, we are in the [Lifecycle.State.RESUMED] state
*
* Or in table format:
* ```
* ┌───────────────┬───────────────────┬──────────────┬─────────────────┐
* │ View attached │ Window Visibility │ Window Focus │ Lifecycle State │
* ├───────────────┼───────────────────┴──────────────┼─────────────────┤
* │ Not attached │ Any │ N/A │
* ├───────────────┼───────────────────┬──────────────┼─────────────────┤
* │ │ Not visible │ Any │ CREATED │
* │ ├───────────────────┼──────────────┼─────────────────┤
* │ Attached │ │ No focus │ STARTED │
* │ │ Visible ├──────────────┼─────────────────┤
* │ │ │ Has focus │ RESUMED │
* └───────────────┴───────────────────┴──────────────┴─────────────────┘
* ```
*/
class ViewLifecycleOwner(
private val view: View,
) : LifecycleOwner {
private val windowVisibleListener =
ViewTreeObserver.OnWindowVisibilityChangeListener { updateState() }
private val windowFocusListener = ViewTreeObserver.OnWindowFocusChangeListener { updateState() }
private val registry = LifecycleRegistry(this)
fun onCreate() {
registry.currentState = Lifecycle.State.CREATED
view.viewTreeObserver.addOnWindowVisibilityChangeListener(windowVisibleListener)
view.viewTreeObserver.addOnWindowFocusChangeListener(windowFocusListener)
updateState()
}
fun onDestroy() {
view.viewTreeObserver.removeOnWindowVisibilityChangeListener(windowVisibleListener)
view.viewTreeObserver.removeOnWindowFocusChangeListener(windowFocusListener)
registry.currentState = Lifecycle.State.DESTROYED
}
override fun getLifecycle(): Lifecycle {
return registry
}
private fun updateState() {
registry.currentState =
when {
view.windowVisibility != View.VISIBLE -> Lifecycle.State.CREATED
!view.hasWindowFocus() -> Lifecycle.State.STARTED
else -> Lifecycle.State.RESUMED
}
}
}
|