1 /*
<lambda>null2 * Copyright (C) 2024 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 package com.android.systemui.lifecycle
18
19 import android.os.Handler
20 import android.os.Looper
21 import android.view.Choreographer
22 import android.view.View
23 import androidx.collection.MutableScatterSet
24 import androidx.compose.runtime.snapshots.Snapshot
25 import androidx.compose.runtime.snapshots.SnapshotStateObserver
26 import androidx.core.os.HandlerCompat
27 import com.android.systemui.res.R
28
29 /**
30 * [SnapshotViewBindingRoot] is installed on the root view of an attached view hierarchy and
31 * coordinates all [SnapshotViewBinding]s for the window.
32 *
33 * This class is not thread-safe. It should only be accessed from the thread corresponding to the UI
34 * thread referenced by the [handler] and [choreographer] constructor parameters. These two
35 * parameters must refer to the same UI thread.
36 *
37 * Lazily created and installed on a root attached view by [bindingRoot].
38 */
39 private class SnapshotViewBindingRoot(
40 private val handler: Handler,
41 private val choreographer: Choreographer
42 ) {
43 /** Multiplexer for all snapshot state observations; see [start] and [stop] */
44 private val observer = SnapshotStateObserver { task ->
45 if (Looper.myLooper() === handler.looper) task() else handler.post(task)
46 }
47
48 /** `true` if a [Choreographer] frame is currently scheduled */
49 private var isFrameScheduled = false
50
51 /**
52 * Unordered set of [SnapshotViewBinding]s that have been invalidated and are awaiting handling
53 * by an upcoming frame.
54 */
55 private val invalidatedBindings = MutableScatterSet<SnapshotViewBinding>()
56
57 /**
58 * Callback for [SnapshotStateObserver.observeReads] allocated once for the life of the
59 * [SnapshotViewBindingRoot] and reused to avoid extra allocations during frame operations.
60 */
61 private val onBindingChanged: (SnapshotViewBinding) -> Unit = {
62 invalidatedBindings += it
63 if (!isFrameScheduled) {
64 choreographer.postFrameCallback(frameCallback)
65 isFrameScheduled = true
66 }
67 }
68
69 /** Callback for [Choreographer.postFrameCallback] */
70 private val frameCallback =
71 Choreographer.FrameCallback {
72 try {
73 bindInvalidatedBindings()
74 } finally {
75 isFrameScheduled = false
76 }
77 }
78
79 /**
80 * Perform binding of all [SnapshotViewBinding]s in [invalidatedBindings] within a single
81 * mutable snapshot. The snapshot will be committed if no exceptions are thrown from any
82 * binding's `onError` handler.
83 */
84 private fun bindInvalidatedBindings() {
85 Snapshot.withMutableSnapshot {
86 // removeIf is used here to perform a forEach where each element is removed
87 // as the invalid bindings are traversed. If a performBindOf throws we want
88 // the rest of the unhandled invalidations to remain.
89 invalidatedBindings.removeIf { binding ->
90 performBindOf(binding)
91 true
92 }
93 }
94 }
95
96 /**
97 * Perform the view binding for [binding] while observing its snapshot reads. Once this method
98 * is called for a [binding] this [SnapshotViewBindingRoot] may retain hard references back to
99 * [binding] via [observer], [invalidatedBindings] or both. Use [forgetBinding] to drop these
100 * references once a [SnapshotViewBinding] is no longer relevant.
101 *
102 * This method should only be called after [start] has been called and before [stop] has been
103 * called; failing to obey this constraint may result in lingering hard references to [binding]
104 * or missed invalidations in response to snapshot state that was changed prior to [start] being
105 * called.
106 */
107 fun performBindOf(binding: SnapshotViewBinding) {
108 try {
109 observer.observeReads(binding, onBindingChanged, binding.performBind)
110 } catch (error: Throwable) {
111 // Note: it is valid (and the default) for this call to re-throw the error
112 binding.onError(error)
113 }
114 }
115
116 /**
117 * Forget about [binding], dropping all observed tracking and invalidation state. After calling
118 * this method it is safe to abandon [binding] to the garbage collector.
119 */
120 fun forgetBinding(binding: SnapshotViewBinding) {
121 observer.clear(binding)
122 invalidatedBindings.remove(binding)
123 }
124
125 /**
126 * Start tracking snapshot commits that may affect [SnapshotViewBinding]s passed to
127 * [performBindOf] calls. Call this method before invoking [performBindOf].
128 *
129 * Once this method has been called, [stop] must be called prior to abandoning this
130 * [SnapshotViewBindingRoot] to the garbage collector, as a hard reference to it will be
131 * retained by the snapshot system until [stop] is invoked.
132 */
133 fun start() {
134 observer.start()
135 }
136
137 /**
138 * Stop tracking snapshot commits that may affect [SnapshotViewBinding]s that have been passed
139 * to [performBindOf], cancel any pending [choreographer] frame callback, and forget all
140 * [invalidatedBindings].
141 *
142 * Call [stop] prior to abandoning this [SnapshotViewBindingRoot] to the garbage collector.
143 *
144 * Calling [start] again after [stop] will begin tracking invalidations again, but any
145 * [SnapshotViewBinding]s must be re-bound using [performBindOf] after the [start] call returns.
146 */
147 fun stop() {
148 observer.stop()
149 choreographer.removeFrameCallback(frameCallback)
150 isFrameScheduled = false
151 invalidatedBindings.clear()
152 }
153 }
154
155 /**
156 * Return the [SnapshotViewBindingRoot] for this [View], lazily creating it if it does not yet
157 * exist. This [View] must be currently attached to a window and this property should only be
158 * accessed from this [View]'s UI thread.
159 *
160 * The [SnapshotViewBindingRoot] will be [started][SnapshotViewBindingRoot.start] before this
161 * property get returns, making it safe to call [SnapshotViewBindingRoot.performBindOf] for the
162 * [bindingRoot] of an attached [View].
163 *
164 * When the [View] becomes attached to a window the [SnapshotViewBindingRoot] will automatically be
165 * [started][SnapshotViewBindingRoot.start]. When it becomes detached from its window it will
166 * automatically be [stopped][SnapshotViewBindingRoot.stop].
167 *
168 * This should generally only be called on the [View] returned by [View.getRootView] for an attached
169 * [View].
170 */
171 private val View.bindingRoot: SnapshotViewBindingRoot
172 get() {
173 val tag = getTag(R.id.snapshot_view_binding_root) as? SnapshotViewBindingRoot
174 if (tag != null) return tag
175 val newRoot =
176 SnapshotViewBindingRoot(
177 // Use an async handler for processing invalidations; this ensures invalidations
178 // are tracked for the upcoming frame and not the next frame.
179 handler =
180 HandlerCompat.createAsync(
181 handler?.looper ?: error("$this is not attached to a window")
182 ),
183 choreographer = Choreographer.getInstance()
184 )
185 setTag(R.id.snapshot_view_binding_root, newRoot)
186 addOnAttachStateChangeListener(
187 object : View.OnAttachStateChangeListener {
onViewAttachedToWindownull188 override fun onViewAttachedToWindow(view: View) {
189 newRoot.start()
190 }
191
onViewDetachedFromWindownull192 override fun onViewDetachedFromWindow(view: View) {
193 newRoot.stop()
194 }
195 }
196 )
197 if (isAttachedToWindow) newRoot.start()
198 return newRoot
199 }
200
201 /**
202 * A single [SnapshotViewBinding] set on a [View] by [setSnapshotBinding]. The [SnapshotViewBinding]
203 * is responsible for invoking [SnapshotViewBindingRoot.performBindOf] when the associated [View]
204 * becomes attached to a window in order to register it for invalidation tracking and rebinding as
205 * relevant snapshot state changes. When the [View] becomes detached the binding will invoke
206 * [SnapshotViewBindingRoot.forgetBinding] for itself.
207 */
208 private class SnapshotViewBinding(
209 val performBind: () -> Unit,
210 val onError: (Throwable) -> Unit,
211 ) : View.OnAttachStateChangeListener {
212
onViewAttachedToWindownull213 override fun onViewAttachedToWindow(view: View) {
214 Snapshot.withMutableSnapshot { view.rootView.bindingRoot.performBindOf(this) }
215 }
216
onViewDetachedFromWindownull217 override fun onViewDetachedFromWindow(view: View) {
218 view.rootView.bindingRoot.forgetBinding(this)
219 }
220 }
221
222 /**
223 * Set binding logic for this [View] that will be re-invoked for UI frames where relevant [Snapshot]
224 * state has changed. This can be especially useful for codebases with mixed usage of both Views and
225 * [Jetpack Compose](https://d.android.com/compose), enabling the same patterns of snapshot-backed
226 * state management when using either UI toolkit.
227 *
228 * In the following example the sender name and message text of a message item view will be kept up
229 * to date with the snapshot-backed `model.senderName` and `model.messageText` properties:
230 * ```
231 * val view = layoutInflater.inflate(R.layout.single_message, parent, false)
232 * val senderNameView = view.findViewById<TextView>(R.id.sender_name)
233 * val messageTextView = view.findViewById<TextView>(R.id.message_text)
234 * view.setSnapshotBinding {
235 * senderNameView.text = model.senderName
236 * messageTextView.text = model.messageText
237 * }
238 * ```
239 *
240 * Snapshot binding may also be used in concert with
241 * [View binding](https://developer.android.com/topic/libraries/view-binding):
242 * ```
243 * val binding = SingleMessageBinding.inflate(layoutInflater)
244 * binding.root.setSnapshotBinding {
245 * binding.senderName.text = model.senderName
246 * binding.messageText.text = model.messageText
247 * }
248 * ```
249 *
250 * When a snapshot binding is set [performBind] will be invoked immediately before
251 * [setSnapshotBinding] returns if this [View] is currently attached to a window. If the view is not
252 * currently attached, [performBind] will be invoked when the view becomes attached to a window.
253 *
254 * If a snapshot commit changes state accessed by [performBind] changes while the view remains
255 * attached to its window and the snapshot binding is not replaced or [cleared][clearBinding], the
256 * binding will be considered _invalidated,_ a rebinding will be scheduled for the upcoming UI
257 * frame, and [performBind] will be re-executed prior to the layout and draw phases for the frame.
258 * [performBind] will only be re-executed **once** for any given UI frame provided that
259 * [setSnapshotBinding] is not called again.
260 *
261 * [performBind] is always invoked from a [mutable snapshot][Snapshot.takeMutableSnapshot], ensuring
262 * atomic consistency of all snapshot state reads within it. **All** rebinding performed for
263 * invalidations of bindings within the same window for a given UI frame are performed within the
264 * **same** snapshot, ensuring that same atomic consistency of snapshot state for **all** snapshot
265 * bindings within the same window.
266 *
267 * As [performBind] is invoked for rebinding as part of the UI frame itself, [performBind]
268 * implementations should be both fast and idempotent to avoid delaying the UI frame.
269 *
270 * There are no mutual ordering guarantees between separate snapshot bindings; the [performBind] of
271 * separate snapshot bindings may be executed in any order. Similarly, no ordering guarantees exist
272 * between snapshot binding rebinding and Jetpack Compose recomposition. Snapshot bindings and
273 * Compose UIs both should obey
274 * [unidirectional data flow](https://developer.android.com/topic/architecture/ui-layer#udf)
275 * principles, consuming state from mutual single sources of truth and avoid consuming state
276 * produced by the rebinding or recomposition of other UI components.
277 */
setSnapshotBindingnull278 fun View.setSnapshotBinding(onError: (Throwable) -> Unit = { throw it }, performBind: () -> Unit) {
279 clearBinding()
280 val newBinding = SnapshotViewBinding(performBind, onError)
281 setTag(R.id.snapshot_view_binding, newBinding)
282 addOnAttachStateChangeListener(newBinding)
283 if (isAttachedToWindow) newBinding.onViewAttachedToWindow(this)
284 }
285
286 /**
287 * Remove a snapshot binding that was set by [setSnapshotBinding]. It is not necessary to call this
288 * function before abandoning a [View] with a snapshot binding to the garbage collector.
289 */
clearBindingnull290 fun View.clearBinding() {
291 val oldBinding = getTag(R.id.snapshot_view_binding) as? SnapshotViewBinding
292 if (oldBinding != null) {
293 removeOnAttachStateChangeListener(oldBinding)
294 if (isAttachedToWindow) {
295 oldBinding.onViewDetachedFromWindow(this)
296 }
297 }
298 }
299