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