1 /*
<lambda>null2 * Copyright 2020 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 androidx.compose.ui.platform
18
19 import android.content.Context
20 import android.database.ContentObserver
21 import android.net.Uri
22 import android.os.Looper
23 import android.provider.Settings
24 import android.view.View
25 import android.view.ViewParent
26 import androidx.compose.runtime.CompositionContext
27 import androidx.compose.runtime.MonotonicFrameClock
28 import androidx.compose.runtime.PausableMonotonicFrameClock
29 import androidx.compose.runtime.Recomposer
30 import androidx.compose.runtime.getValue
31 import androidx.compose.runtime.mutableFloatStateOf
32 import androidx.compose.runtime.setValue
33 import androidx.compose.ui.InternalComposeUiApi
34 import androidx.compose.ui.MotionDurationScale
35 import androidx.compose.ui.R
36 import androidx.compose.ui.internal.checkPrecondition
37 import androidx.compose.ui.internal.checkPreconditionNotNull
38 import androidx.core.os.HandlerCompat
39 import androidx.lifecycle.Lifecycle
40 import androidx.lifecycle.LifecycleEventObserver
41 import androidx.lifecycle.LifecycleOwner
42 import androidx.lifecycle.findViewTreeLifecycleOwner
43 import java.util.concurrent.atomic.AtomicReference
44 import kotlin.coroutines.ContinuationInterceptor
45 import kotlin.coroutines.CoroutineContext
46 import kotlin.coroutines.EmptyCoroutineContext
47 import kotlinx.coroutines.CoroutineScope
48 import kotlinx.coroutines.CoroutineStart
49 import kotlinx.coroutines.DelicateCoroutinesApi
50 import kotlinx.coroutines.GlobalScope
51 import kotlinx.coroutines.Job
52 import kotlinx.coroutines.MainScope
53 import kotlinx.coroutines.android.asCoroutineDispatcher
54 import kotlinx.coroutines.channels.Channel
55 import kotlinx.coroutines.channels.Channel.Factory.CONFLATED
56 import kotlinx.coroutines.flow.SharingStarted
57 import kotlinx.coroutines.flow.StateFlow
58 import kotlinx.coroutines.flow.flow
59 import kotlinx.coroutines.flow.stateIn
60 import kotlinx.coroutines.launch
61
62 /**
63 * The [CompositionContext] that should be used as a parent for compositions at or below this view
64 * in the hierarchy. Set to non-`null` to provide a [CompositionContext] for compositions created by
65 * child views, or `null` to fall back to any [CompositionContext] provided by ancestor views.
66 *
67 * See [findViewTreeCompositionContext].
68 */
69 var View.compositionContext: CompositionContext?
70 get() = getTag(R.id.androidx_compose_ui_view_composition_context) as? CompositionContext
71 set(value) {
72 setTag(R.id.androidx_compose_ui_view_composition_context, value)
73 }
74
75 /**
76 * Returns the parent [CompositionContext] for this point in the view hierarchy, or `null` if none
77 * can be found.
78 *
79 * See [compositionContext] to get or set the parent [CompositionContext] for a specific view.
80 */
findViewTreeCompositionContextnull81 fun View.findViewTreeCompositionContext(): CompositionContext? {
82 var found: CompositionContext? = compositionContext
83 if (found != null) return found
84 var parent: ViewParent? = parent
85 while (found == null && parent is View) {
86 found = parent.compositionContext
87 parent = parent.getParent()
88 }
89 return found
90 }
91
92 private val animationScale = mutableMapOf<Context, StateFlow<Float>>()
93
94 // Callers of this function should pass an application context. Passing an activity context might
95 // result in activity leaks.
getAnimationScaleFlowFornull96 private fun getAnimationScaleFlowFor(applicationContext: Context): StateFlow<Float> {
97 return synchronized(animationScale) {
98 animationScale.getOrPut(applicationContext) {
99 val resolver = applicationContext.contentResolver
100 val animationScaleUri =
101 Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE)
102 val channel = Channel<Unit>(CONFLATED)
103 val contentObserver =
104 object : ContentObserver(HandlerCompat.createAsync(Looper.getMainLooper())) {
105 override fun onChange(selfChange: Boolean, uri: Uri?) {
106 channel.trySend(Unit)
107 }
108 }
109
110 // TODO: Switch to callbackFlow when it becomes stable
111 flow {
112 resolver.registerContentObserver(animationScaleUri, false, contentObserver)
113 try {
114 for (value in channel) {
115 val newValue =
116 Settings.Global.getFloat(
117 applicationContext.contentResolver,
118 Settings.Global.ANIMATOR_DURATION_SCALE,
119 1f
120 )
121 emit(newValue)
122 }
123 } finally {
124 resolver.unregisterContentObserver(contentObserver)
125 }
126 }
127 .stateIn(
128 MainScope(),
129 SharingStarted.WhileSubscribed(),
130 Settings.Global.getFloat(
131 applicationContext.contentResolver,
132 Settings.Global.ANIMATOR_DURATION_SCALE,
133 1f
134 )
135 )
136 }
137 }
138 }
139
140 /** A factory for creating an Android window-scoped [Recomposer]. See [createRecomposer]. */
141 @InternalComposeUiApi
interfacenull142 fun interface WindowRecomposerFactory {
143 /**
144 * Get a [Recomposer] for the window where [windowRootView] is at the root of the window's
145 * [View] hierarchy. The factory is responsible for establishing a policy for
146 * [shutting down][Recomposer.cancel] the returned [Recomposer]. [windowRootView] will hold a
147 * hard reference to the returned [Recomposer] until it [joins][Recomposer.join] after shutting
148 * down.
149 */
150 fun createRecomposer(windowRootView: View): Recomposer
151
152 companion object {
153 /**
154 * A [WindowRecomposerFactory] that creates **lifecycle-aware** [Recomposer]s.
155 *
156 * Returned [Recomposer]s will be bound to the [LifecycleOwner] returned by
157 * [findViewTreeLifecycleOwner] registered at the [root][View.getRootView] of the view
158 * hierarchy and run [recomposition][Recomposer.runRecomposeAndApplyChanges] and composition
159 * effects on the [AndroidUiDispatcher.CurrentThread] for the window's UI thread. The
160 * associated [MonotonicFrameClock] will only produce frames when the [Lifecycle] is at
161 * least [Lifecycle.State.STARTED], causing animations and other uses of
162 * [MonotonicFrameClock] APIs to suspend until a **visible** frame will be produced.
163 */
164 val LifecycleAware: WindowRecomposerFactory = WindowRecomposerFactory { rootView ->
165 rootView.createLifecycleAwareWindowRecomposer()
166 }
167 }
168 }
169
170 @InternalComposeUiApi
171 object WindowRecomposerPolicy {
172
173 private val factory =
174 AtomicReference<WindowRecomposerFactory>(WindowRecomposerFactory.LifecycleAware)
175
176 // Don't expose the actual AtomicReference as @PublishedApi; we might convert to atomicfu later
177 @Suppress("ShowingMemberInHiddenClass")
178 @PublishedApi
getAndSetFactorynull179 internal fun getAndSetFactory(factory: WindowRecomposerFactory): WindowRecomposerFactory =
180 this.factory.getAndSet(factory)
181
182 @Suppress("ShowingMemberInHiddenClass")
183 @PublishedApi
184 internal fun compareAndSetFactory(
185 expected: WindowRecomposerFactory,
186 factory: WindowRecomposerFactory
187 ): Boolean = this.factory.compareAndSet(expected, factory)
188
189 fun setFactory(factory: WindowRecomposerFactory) {
190 this.factory.set(factory)
191 }
192
withFactorynull193 inline fun <R> withFactory(factory: WindowRecomposerFactory, block: () -> R): R {
194 var cause: Throwable? = null
195 val oldFactory = getAndSetFactory(factory)
196 return try {
197 block()
198 } catch (t: Throwable) {
199 cause = t
200 throw t
201 } finally {
202 if (!compareAndSetFactory(factory, oldFactory)) {
203 val err =
204 IllegalStateException(
205 "WindowRecomposerFactory was set to unexpected value; cannot safely restore " +
206 "old state"
207 )
208 if (cause == null) throw err
209 cause.addSuppressed(err)
210 throw cause
211 }
212 }
213 }
214
215 @OptIn(DelicateCoroutinesApi::class)
createAndInstallWindowRecomposernull216 internal fun createAndInstallWindowRecomposer(rootView: View): Recomposer {
217 val newRecomposer = factory.get().createRecomposer(rootView)
218 rootView.compositionContext = newRecomposer
219
220 // If the Recomposer shuts down, unregister it so that a future request for a window
221 // recomposer will consult the factory for a new one.
222 val unsetJob =
223 GlobalScope.launch(
224 rootView.handler.asCoroutineDispatcher("windowRecomposer cleanup").immediate
225 ) {
226 try {
227 newRecomposer.join()
228 } finally {
229 // Unset if the view is detached. (See below for the attach state change
230 // listener.)
231 // Since this is in a finally in this coroutine, even if this job is cancelled
232 // we
233 // will resume on the window's UI thread and perform this manipulation there.
234 val viewTagRecomposer = rootView.compositionContext
235 if (viewTagRecomposer === newRecomposer) {
236 rootView.compositionContext = null
237 }
238 }
239 }
240
241 // If the root view is detached, cancel the await for recomposer shutdown above.
242 // This will also unset the tag reference to this recomposer during its cleanup.
243 rootView.addOnAttachStateChangeListener(
244 object : View.OnAttachStateChangeListener {
245 override fun onViewAttachedToWindow(v: View) {}
246
247 override fun onViewDetachedFromWindow(v: View) {
248 v.removeOnAttachStateChangeListener(this)
249 // cancel the job to clean up the view tags.
250 // this will happen immediately since unsetJob is on an immediate dispatcher
251 // for this view's UI thread instead of waiting for the recomposer to join.
252 // NOTE: This does NOT cancel the returned recomposer itself, as it may be
253 // a shared-instance recomposer that should remain running/is reused elsewhere.
254 unsetJob.cancel()
255 }
256 }
257 )
258 return newRecomposer
259 }
260 }
261
262 /**
263 * Find the "content child" for this view. The content child is the view that is either a direct
264 * child of the view with id [android.R.id.content] (and was therefore set as a content view into an
265 * activity or dialog window) or the root view of the window.
266 *
267 * This is used as opposed to [View.getRootView] as the Android framework can reuse an activity
268 * window's decor views across activity recreation events. Since a window recomposer is associated
269 * with the lifecycle of the host activity, we want that recomposer to shut down and create a new
270 * one for the new activity instance.
271 */
272 private val View.contentChild: View
273 get() {
274 var self: View = this
275 var parent: ViewParent? = self.parent
276 while (parent is View) {
277 if (parent.id == android.R.id.content) return self
278 self = parent
279 parent = self.parent
280 }
281 return self
282 }
283
284 /**
285 * Get or lazily create a [Recomposer] for this view's window. The view must be attached to a window
286 * with the [LifecycleOwner] returned by [findViewTreeLifecycleOwner] registered at the root to
287 * access this property.
288 */
289 @OptIn(InternalComposeUiApi::class)
290 internal val View.windowRecomposer: Recomposer
291 get() {
<lambda>null292 checkPrecondition(isAttachedToWindow) {
293 "Cannot locate windowRecomposer; View $this is not attached to a window"
294 }
295 val rootView = contentChild
296 return when (val rootParentRef = rootView.compositionContext) {
297 null -> WindowRecomposerPolicy.createAndInstallWindowRecomposer(rootView)
298 is Recomposer -> rootParentRef
299 else -> error("root viewTreeParentCompositionContext is not a Recomposer")
300 }
301 }
302
303 /**
304 * Create a [Lifecycle] and [window attachment][View.isAttachedToWindow]-aware [Recomposer] for this
305 * [View] with the same behavior as [WindowRecomposerFactory.LifecycleAware].
306 *
307 * [coroutineContext] will override any [CoroutineContext] elements from the default configuration
308 * normally used for this content view. The default [CoroutineContext] contains
309 * [AndroidUiDispatcher.CurrentThread]; this function should only be called from the UI thread of
310 * this [View] or its intended UI thread if it is currently detached.
311 *
312 * If [lifecycle] is `null` or not supplied the [LifecycleOwner] returned by
313 * [findViewTreeLifecycleOwner] will be used; if a non-null [lifecycle] is not provided and a
314 * ViewTreeLifecycleOwner is not present an [IllegalStateException] will be thrown.
315 *
316 * The returned [Recomposer] will be [cancelled][Recomposer.cancel] when this [View] is detached
317 * from a window or if its determined [Lifecycle] is [destroyed][Lifecycle.Event.ON_DESTROY].
318 * Recomposition and associated [frame-based][MonotonicFrameClock] effects may be throttled or
319 * paused while the [Lifecycle] is not at least [Lifecycle.State.STARTED].
320 */
createLifecycleAwareWindowRecomposernull321 fun View.createLifecycleAwareWindowRecomposer(
322 coroutineContext: CoroutineContext = EmptyCoroutineContext,
323 lifecycle: Lifecycle? = null
324 ): Recomposer {
325 // Only access AndroidUiDispatcher.CurrentThread if we would use an element from it,
326 // otherwise prevent lazy initialization.
327 val baseContext =
328 if (
329 coroutineContext[ContinuationInterceptor] == null ||
330 coroutineContext[MonotonicFrameClock] == null
331 ) {
332 AndroidUiDispatcher.CurrentThread + coroutineContext
333 } else coroutineContext
334 val pausableClock =
335 baseContext[MonotonicFrameClock]?.let { PausableMonotonicFrameClock(it).apply { pause() } }
336
337 var systemDurationScaleSettingConsumer: MotionDurationScaleImpl? = null
338 val motionDurationScale =
339 baseContext[MotionDurationScale]
340 ?: MotionDurationScaleImpl().also { systemDurationScaleSettingConsumer = it }
341
342 val contextWithClockAndMotionScale =
343 baseContext + (pausableClock ?: EmptyCoroutineContext) + motionDurationScale
344 val recomposer =
345 Recomposer(contextWithClockAndMotionScale).also { it.pauseCompositionFrameClock() }
346 val runRecomposeScope = CoroutineScope(contextWithClockAndMotionScale)
347 val viewTreeLifecycle =
348 checkPreconditionNotNull(lifecycle ?: findViewTreeLifecycleOwner()?.lifecycle) {
349 "ViewTreeLifecycleOwner not found from $this"
350 }
351
352 // Removing the view holding the ViewTreeRecomposer means we may never be reattached again.
353 // Since this factory function is used to create a new recomposer for each invocation and
354 // doesn't reuse a single instance like other factories might, shut it down whenever it
355 // becomes detached. This can easily happen as part of setting a new content view.
356 addOnAttachStateChangeListener(
357 object : View.OnAttachStateChangeListener {
358 override fun onViewAttachedToWindow(v: View) {}
359
360 override fun onViewDetachedFromWindow(v: View) {
361 removeOnAttachStateChangeListener(this)
362 recomposer.cancel()
363 }
364 }
365 )
366 viewTreeLifecycle.addObserver(
367 object : LifecycleEventObserver {
368 override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
369 val self = this
370 when (event) {
371 Lifecycle.Event.ON_CREATE -> {
372 // Undispatched launch since we've configured this scope
373 // to be on the UI thread
374 runRecomposeScope.launch(start = CoroutineStart.UNDISPATCHED) {
375 var durationScaleJob: Job? = null
376 try {
377 durationScaleJob =
378 systemDurationScaleSettingConsumer?.let {
379 val durationScaleStateFlow =
380 getAnimationScaleFlowFor(context.applicationContext)
381 it.scaleFactor = durationScaleStateFlow.value
382 launch {
383 durationScaleStateFlow.collect { scaleFactor ->
384 it.scaleFactor = scaleFactor
385 }
386 }
387 }
388 recomposer.runRecomposeAndApplyChanges()
389 } finally {
390 durationScaleJob?.cancel()
391 // If runRecomposeAndApplyChanges returns or this coroutine is
392 // cancelled it means we no longer care about this lifecycle.
393 // Clean up the dangling references tied to this observer.
394 source.lifecycle.removeObserver(self)
395 }
396 }
397 }
398 Lifecycle.Event.ON_START -> {
399 // The clock starts life as paused so resume it when starting. If it is
400 // already running (this ON_START is after an ON_STOP) then the resume is
401 // ignored.
402 pausableClock?.resume()
403
404 // Resumes the frame clock dispatching If this is an ON_START after an
405 // ON_STOP that paused it. If the recomposer is not paused calling
406 // `resumeFrameClock()` is ignored.
407 recomposer.resumeCompositionFrameClock()
408 }
409 Lifecycle.Event.ON_STOP -> {
410 // Pause the recomposer's frame clock which will pause all calls to
411 // `withFrameNanos` (e.g. animations) while the window is stopped.
412 recomposer.pauseCompositionFrameClock()
413 }
414 Lifecycle.Event.ON_DESTROY -> {
415 recomposer.cancel()
416 }
417 Lifecycle.Event.ON_PAUSE -> {
418 // Nothing
419 }
420 Lifecycle.Event.ON_RESUME -> {
421 // Nothing
422 }
423 Lifecycle.Event.ON_ANY -> {
424 // Nothing
425 }
426 }
427 }
428 }
429 )
430 return recomposer
431 }
432
433 private class MotionDurationScaleImpl : MotionDurationScale {
434 override var scaleFactor by mutableFloatStateOf(1f)
435 }
436