1 /*
<lambda>null2  * Copyright 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 androidx.xr.compose.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 androidx.compose.runtime.MonotonicFrameClock
25 import androidx.compose.runtime.PausableMonotonicFrameClock
26 import androidx.compose.runtime.Recomposer
27 import androidx.compose.runtime.getValue
28 import androidx.compose.runtime.mutableFloatStateOf
29 import androidx.compose.runtime.setValue
30 import androidx.compose.ui.ExperimentalComposeUiApi
31 import androidx.compose.ui.InternalComposeUiApi
32 import androidx.compose.ui.MotionDurationScale
33 import androidx.compose.ui.platform.AndroidUiDispatcher
34 import androidx.core.os.HandlerCompat
35 import androidx.lifecycle.Lifecycle
36 import androidx.lifecycle.LifecycleEventObserver
37 import androidx.lifecycle.LifecycleOwner
38 import java.util.concurrent.atomic.AtomicReference
39 import kotlin.coroutines.ContinuationInterceptor
40 import kotlin.coroutines.CoroutineContext
41 import kotlin.coroutines.EmptyCoroutineContext
42 import kotlinx.coroutines.CoroutineScope
43 import kotlinx.coroutines.CoroutineStart
44 import kotlinx.coroutines.Job
45 import kotlinx.coroutines.MainScope
46 import kotlinx.coroutines.channels.Channel
47 import kotlinx.coroutines.channels.Channel.Factory.CONFLATED
48 import kotlinx.coroutines.flow.SharingStarted
49 import kotlinx.coroutines.flow.StateFlow
50 import kotlinx.coroutines.flow.callbackFlow
51 import kotlinx.coroutines.flow.stateIn
52 import kotlinx.coroutines.launch
53 
54 /** See [androidx.compose.ui.platform.WindowRecomposerPolicy] */
55 @InternalComposeUiApi
56 internal object SubspaceRecomposerPolicy {
57     private val factory = AtomicReference(SubspaceRecomposerFactory.LifecycleAware)
58 
59     internal fun createAndInstallSubspaceRecomposer(
60         rootElement: AbstractComposeElement
61     ): Recomposer {
62         val newRecomposer = factory.get().createRecomposer(rootElement)
63         rootElement.compositionContext = newRecomposer
64 
65         // If the Recomposer shuts down, unregister it so that a future request for a subspace
66         // recomposer will consult the factory for a new one.
67         // TODO: update to GlobalScope.launch(AndroidUiDispatcher.CurrentThread) when migrating
68         //       to androidx-main
69         val scope = MainScope()
70         val unsetJob =
71             scope.launch {
72                 try {
73                     newRecomposer.join()
74                 } finally {
75                     // Unset if the element is detached. (See below for the attach state change
76                     // listener). Since this is in a finally in this coroutine, even if this job is
77                     // cancelled we will resume on the window's UI thread and perform this
78                     // manipulation
79                     // there.
80                     if (rootElement.compositionContext === newRecomposer) {
81                         rootElement.compositionContext = null
82                     }
83                 }
84             }
85 
86         // If the root element is detached, cancel the await for recomposer shutdown above.
87         // This will also unset the tag reference to this recomposer during its cleanup.
88         rootElement.addOnAttachStateChangeListener(
89             object : SpatialElement.OnAttachStateChangeListener {
90                 override fun onElementAttachedToSubspace(
91                     spatialComposeScene: SpatialComposeScene
92                 ) {}
93 
94                 override fun onElementDetachedFromSubspace(
95                     spatialComposeScene: SpatialComposeScene
96                 ) {
97                     rootElement.removeOnAttachStateChangeListener(this)
98                     // Cancel the job to clean up the composition context reference in the element.
99                     unsetJob.cancel()
100 
101                     // Also cancel the recomposer as it is not shared with another tree.
102                     newRecomposer.cancel()
103                 }
104             }
105         )
106 
107         return newRecomposer
108     }
109 }
110 
111 /**
112  * A factory for creating an subspace-scoped [Recomposer].
113  *
114  * See [createRecomposer] for more info.
115  */
116 @InternalComposeUiApi
interfacenull117 private fun interface SubspaceRecomposerFactory {
118     /**
119      * Creates a [Recomposer] for a subspace with [rootElement] being the root of the Compose
120      * hierarchy.
121      *
122      * The factory is responsible for establishing a policy for [shutting down][Recomposer.cancel]
123      * the returned [Recomposer]. [rootElement] will hold a hard reference to the returned
124      * [Recomposer] until it [joins][Recomposer.join] after shutting down.
125      */
126     fun createRecomposer(rootElement: AbstractComposeElement): Recomposer
127 
128     companion object {
129         /**
130          * A [SubspaceRecomposerFactory] that creates **lifecycle-aware** [Recomposer]s.
131          *
132          * Returned [Recomposer]s will be bound to the [SpatialComposeScene] of the 'rootElement'
133          * argument of [createRecomposer] and will be destroyed once the [SpatialComposeScene]
134          * lifecycle ends.
135          *
136          * The recomposer will run [recomposition][Recomposer.runRecomposeAndApplyChanges] and
137          * composition effects on the [AndroidUiDispatcher.CurrentThread]. The associated
138          * [MonotonicFrameClock] will only produce frames when the [Lifecycle] is at least
139          * [Lifecycle.State.STARTED], causing animations and other uses of [MonotonicFrameClock]
140          * APIs to suspend until a **visible** frame will be produced.
141          */
142         @OptIn(ExperimentalComposeUiApi::class)
143         val LifecycleAware: SubspaceRecomposerFactory = SubspaceRecomposerFactory { rootElement ->
144             createLifecycleAwareSubspaceRecomposer(rootElement)
145         }
146     }
147 }
148 
149 /**
150  * Create a [Lifecycle] and
151  * [subspace attachment][SpatialElement.isAttachedToSpatialComposeScene]-aware [Recomposer] for the
152  * [rootElement] with the same behavior as [SubspaceRecomposerFactory.LifecycleAware].
153  *
154  * [coroutineContext] will override any [CoroutineContext] elements from the default configuration
155  * normally used for this content element. The default [CoroutineContext] contains
156  * [AndroidUiDispatcher.CurrentThread];
157  *
158  * This function should only be called from the UI thread of this [SpatialElement] or its intended
159  * UI thread if it is currently detached. It must also only be called when the element is attached
160  * to a subspace, i.e., [SpatialElement.spatialComposeScene] must not be `null`. If the
161  * [SpatialElement.spatialComposeScene] is `null`, an [IllegalStateException] will be thrown.
162  *
163  * The returned [Recomposer] will be [cancelled][Recomposer.cancel] when the [rootElement] is
164  * detached from its subspace or if its determined that the subspace is destroyed and its
165  * [Lifecycle] has [ended][Lifecycle.Event.ON_DESTROY].
166  *
167  * Recomposition and associated [frame-based][MonotonicFrameClock] effects may be throttled or
168  * paused while the [Lifecycle] is not at least [Lifecycle.State.STARTED].
169  */
170 @ExperimentalComposeUiApi
createLifecycleAwareSubspaceRecomposernull171 private fun createLifecycleAwareSubspaceRecomposer(
172     rootElement: AbstractComposeElement,
173     coroutineContext: CoroutineContext = EmptyCoroutineContext,
174 ): Recomposer {
175     val subspace =
176         checkNotNull(rootElement.spatialComposeScene) {
177             "Element $rootElement is not attached to a subspace."
178         }
179 
180     // Only access AndroidUiDispatcher.CurrentThread if we would use an element from it,
181     // otherwise prevent lazy initialization.
182     val baseContext =
183         if (
184             coroutineContext[ContinuationInterceptor] == null ||
185                 coroutineContext[MonotonicFrameClock] == null
186         ) {
187             AndroidUiDispatcher.CurrentThread + coroutineContext
188         } else {
189             coroutineContext
190         }
191 
192     val pausableClock =
193         baseContext[MonotonicFrameClock]?.let { PausableMonotonicFrameClock(it).apply { pause() } }
194 
195     var systemDurationScaleSettingConsumer: MotionDurationScaleImpl? = null
196     val motionDurationScale =
197         baseContext[MotionDurationScale]
198             ?: MotionDurationScaleImpl().also { systemDurationScaleSettingConsumer = it }
199 
200     val contextWithClockAndMotionScale =
201         baseContext + (pausableClock ?: EmptyCoroutineContext) + motionDurationScale
202     val recomposer =
203         Recomposer(contextWithClockAndMotionScale).also { it.pauseCompositionFrameClock() }
204     val runRecomposeScope = CoroutineScope(contextWithClockAndMotionScale)
205 
206     // Removing the element that holds the spatial scene graph means it may never be reattached
207     // again.
208     // Since this factory function is used to create a new recomposer for each invocation and
209     // does not reuse a single instance like other factories might, shut it down whenever it
210     // becomes detached. This can easily happen as part of subspace content.
211     rootElement.onDetachedFromSubspaceOnce { recomposer.cancel() }
212 
213     subspace.lifecycle.addObserver(
214         object : LifecycleEventObserver {
215             override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
216                 val self = this
217                 when (event) {
218                     Lifecycle.Event.ON_CREATE -> {
219                         // UNDISPATCHED launch since we've configured this scope to be on the UI
220                         // thread.
221                         runRecomposeScope.launch(start = CoroutineStart.UNDISPATCHED) {
222                             var durationScaleJob: Job? = null
223                             try {
224                                 durationScaleJob =
225                                     systemDurationScaleSettingConsumer?.let {
226                                         val durationScaleStateFlow =
227                                             getAnimationScaleFlowFor(
228                                                 subspace.ownerActivity.applicationContext
229                                             )
230                                         it.scaleFactor = durationScaleStateFlow.value
231                                         launch {
232                                             durationScaleStateFlow.collect { scaleFactor ->
233                                                 it.scaleFactor = scaleFactor
234                                             }
235                                         }
236                                     }
237                                 recomposer.runRecomposeAndApplyChanges()
238                             } finally {
239                                 durationScaleJob?.cancel()
240                                 // If runRecomposeAndApplyChanges returns or this coroutine is
241                                 // cancelled
242                                 // it means we no longer care about this lifecycle. Clean up the
243                                 // dangling references tied to this observer.
244                                 source.lifecycle.removeObserver(self)
245                             }
246                         }
247                     }
248                     Lifecycle.Event.ON_START -> {
249                         // The clock starts life as paused so resume it when starting. If it is
250                         // already
251                         // running (this ON_START is after an ON_STOP) then the resume is ignored.
252                         pausableClock?.resume()
253 
254                         // Resumes the frame clock dispatching if this is an ON_START after an
255                         // ON_STOP
256                         // that paused it. If the recomposer is not paused  calling
257                         // `resumeFrameClock()`
258                         // is ignored.
259                         recomposer.resumeCompositionFrameClock()
260                     }
261                     Lifecycle.Event.ON_STOP -> {
262                         // Pause the recomposer's frame clock which will pause all calls to
263                         // `withFrameNanos` (e.g. animations) while the window is stopped.
264                         recomposer.pauseCompositionFrameClock()
265                     }
266                     Lifecycle.Event.ON_DESTROY -> {
267                         recomposer.cancel()
268                     }
269                     Lifecycle.Event.ON_PAUSE -> {
270                         // Nothing
271                     }
272                     Lifecycle.Event.ON_RESUME -> {
273                         // Nothing
274                     }
275                     Lifecycle.Event.ON_ANY -> {
276                         // Nothing
277                     }
278                 }
279             }
280         }
281     )
282 
283     return recomposer
284 }
285 
286 private class MotionDurationScaleImpl : MotionDurationScale {
287     override var scaleFactor by mutableFloatStateOf(1f)
288 }
289 
290 private val animationScale = mutableMapOf<Context, StateFlow<Float>>()
291 
292 // Callers of this function should pass an application context. Passing an activity context might
293 // result in activity leaks.
getAnimationScaleFlowFornull294 private fun getAnimationScaleFlowFor(applicationContext: Context): StateFlow<Float> {
295     return synchronized(animationScale) {
296         animationScale.getOrPut(applicationContext) {
297             val resolver = applicationContext.contentResolver
298             val animationScaleUri =
299                 Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE)
300             val channel = Channel<Unit>(CONFLATED)
301             val contentObserver =
302                 object : ContentObserver(HandlerCompat.createAsync(Looper.getMainLooper())) {
303                     override fun onChange(selfChange: Boolean, uri: Uri?) {
304                         @Suppress("UNUSED_VARIABLE") val unused = channel.trySend(Unit)
305                     }
306                 }
307 
308             callbackFlow {
309                     resolver.registerContentObserver(animationScaleUri, false, contentObserver)
310                     try {
311                         for (value in channel) {
312                             val newValue =
313                                 Settings.Global.getFloat(
314                                     applicationContext.contentResolver,
315                                     Settings.Global.ANIMATOR_DURATION_SCALE,
316                                     1f,
317                                 )
318                             send(newValue)
319                         }
320                     } finally {
321                         resolver.unregisterContentObserver(contentObserver)
322                     }
323                 }
324                 .stateIn(
325                     MainScope(),
326                     SharingStarted.WhileSubscribed(),
327                     Settings.Global.getFloat(
328                         applicationContext.contentResolver,
329                         Settings.Global.ANIMATOR_DURATION_SCALE,
330                         1f,
331                     ),
332                 )
333         }
334     }
335 }
336