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.os.Handler
20 import android.os.Looper
21 import androidx.compose.runtime.snapshots.SnapshotStateObserver
22 import androidx.lifecycle.DefaultLifecycleObserver
23 import androidx.lifecycle.LifecycleOwner
24 import androidx.xr.compose.subspace.node.SubspaceLayoutNode
25 import androidx.xr.compose.subspace.node.SubspaceOwner
26 import androidx.xr.compose.unit.VolumeConstraints
27 import androidx.xr.runtime.math.Pose
28 import androidx.xr.scenecore.PanelEntity
29 
30 /**
31  * An implementation of the [SubspaceOwner] interface, bridging the Compose layout and rendering
32  * phases.
33  *
34  * Compose decouples layout computation from rendering. This necessitates two distinct trees that
35  * [SubspaceOwner] defines and owns:
36  * 1. The layout tree: Created by [SubspaceLayoutNode], this represents the app's defined hierarchy
37  *    and is used for layout calculations.
38  * 2. The rendering tree: A flat tree rooted at the [SubspaceOwner]. This is used for rendering. The
39  *    hierarchy is necessary to ensure [SpatialElement] instances are visible on a screen (detached
40  *    [SpatialElement] instances are always hidden). The positions of [SpatialElement] are updated
41  *    post-layout computation, during the placement phase.
42  *
43  * This class draws inspiration from the [androidx/compose/ui/platform/AndroidComposeView].
44  */
45 internal class AndroidComposeSpatialElement :
46     SpatialElement(), SubspaceOwner, DefaultLifecycleObserver {
47     override val root: SubspaceLayoutNode = SubspaceLayoutNode()
48 
49     private val handler by lazy { Handler(Looper.getMainLooper()) }
50     private val snapshotStateObserver: SnapshotStateObserver = SnapshotStateObserver {
51         if (handler.looper === Looper.myLooper()) {
52             it()
53         } else {
54             handler.post(it)
55         }
56     }
57 
58     internal var wrappedComposition: WrappedComposition? = null
59 
60     /**
61      * Callback that is registered in [setOnSubspaceAvailable] to be executed when this element is
62      * attached a subspace.
63      */
64     private var onSubspaceAvailable: ((LifecycleOwner) -> Unit)? = null
65 
66     private var windowLeashLayoutNode: SubspaceLayoutNode? = null
67 
68     /**
69      * Whether a layout request has been made. If a layout request is made while a layout is in
70      * progress, the new request will be handled after the current layout is complete.
71      */
72     private var isLayoutRequested = false
73 
74     /**
75      * Tracks whether a layout is currently in progress to avoid recursively triggering a layout.
76      */
77     private var isLayoutInProgress = false
78 
79     internal var rootVolumeConstraints: VolumeConstraints = VolumeConstraints.Unbounded
80 
81     init {
82         root.attach(this)
83     }
84 
85     /**
86      * Registers the [callback] to be executed when this element is attached to a
87      * [spatialComposeScene].
88      *
89      * Note that the [callback] will be invoked immediately if [spatialComposeScene] is already
90      * available.
91      */
92     internal fun setOnSubspaceAvailable(callback: (LifecycleOwner) -> Unit) {
93         if (spatialComposeScene != null) {
94             callback(spatialComposeScene!!)
95         } else {
96             onSubspaceAvailable = callback
97         }
98     }
99 
100     override fun onAttachedToSubspace(spatialComposeScene: SpatialComposeScene) {
101         super.onAttachedToSubspace(spatialComposeScene)
102 
103         spatialComposeScene.lifecycle.addObserver(this)
104         snapshotStateObserver.start()
105         onSubspaceAvailable?.invoke(spatialComposeScene)
106         onSubspaceAvailable = null
107     }
108 
109     override fun onDetachedFromSubspace(spatialComposeScene: SpatialComposeScene) {
110         super.onDetachedFromSubspace(spatialComposeScene)
111 
112         spatialComposeScene.lifecycle.removeObserver(this)
113         snapshotStateObserver.stop()
114         snapshotStateObserver.clear()
115     }
116 
117     override fun onAttach(node: SubspaceLayoutNode) {
118         node.coreEntity?.entity.let { entity ->
119             if (entity is PanelEntity && entity.isMainPanelEntity) {
120                 check(windowLeashLayoutNode == null) {
121                     "Cannot add $node as there is already another SubspaceLayoutNode for the Window Leash Node"
122                 }
123                 windowLeashLayoutNode = node
124             }
125         }
126     }
127 
128     override fun onDetach(node: SubspaceLayoutNode) {
129         node.coreEntity?.entity.let { entity ->
130             if (entity is PanelEntity && entity.isMainPanelEntity) {
131                 windowLeashLayoutNode = null
132             }
133         }
134     }
135 
136     override fun onResume(owner: LifecycleOwner) {
137         super.onResume(owner)
138         // TODO: "Refresh the layout hierarchy." <- Can we just call refreshLayout() here?
139     }
140 
141     override fun onDestroy(owner: LifecycleOwner) {
142         super.onDestroy(owner)
143         root.detach()
144     }
145 
146     // TODO: Consider adding stricter control over how this is called here, or at call sites, if it
147     // becomes too easy to generate superfluous layouts.
148     override fun requestRelayout() {
149         refreshLayout()
150     }
151 
152     // TODO: Add unit tests.
153     private fun refreshLayout() {
154         if (isLayoutInProgress) {
155             isLayoutRequested = true
156             return
157         }
158 
159         isLayoutRequested = false
160         isLayoutInProgress = true
161 
162         snapshotStateObserver.observeReads(this, onLayoutStateValueChanged) {
163             val measureResults = root.measurableLayout.measure(rootVolumeConstraints)
164             (measureResults as SubspaceLayoutNode.MeasurableLayout).placeAt(Pose.Identity)
165         }
166 
167         Logger.log("AndroidComposeSpatialElement") { root.debugTreeToString() }
168         Logger.log("AndroidComposeSpatialElement") { root.debugEntityTreeToString() }
169 
170         isLayoutInProgress = false
171         if (isLayoutRequested) {
172             refreshLayout()
173         }
174     }
175 
176     public companion object {
177         private val onLayoutStateValueChanged: (AndroidComposeSpatialElement) -> Unit = {
178             it.requestRelayout()
179         }
180     }
181 }
182