1 /*
<lambda>null2  * Copyright 2019 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.semantics
18 
19 import androidx.collection.IntObjectMap
20 import androidx.collection.MutableIntObjectMap
21 import androidx.collection.MutableObjectList
22 import androidx.collection.emptyIntObjectMap
23 import androidx.compose.ui.geometry.Rect
24 import androidx.compose.ui.node.LayoutNode
25 import androidx.compose.ui.semantics.SemanticsProperties.HideFromAccessibility
26 import androidx.compose.ui.semantics.SemanticsProperties.InvisibleToUser
27 import androidx.compose.ui.unit.IntRect
28 import androidx.compose.ui.unit.roundToIntRect
29 import androidx.compose.ui.util.fastForEach
30 
31 /** Owns [SemanticsNode] objects and notifies listeners of changes to the semantics tree */
32 class SemanticsOwner
33 internal constructor(
34     private val rootNode: LayoutNode,
35     private val outerSemanticsNode: EmptySemanticsModifier,
36     private val nodes: IntObjectMap<LayoutNode>
37 ) {
38     /**
39      * The root node of the semantics tree. Does not contain any unmerged data. May contain merged
40      * data.
41      */
42     val rootSemanticsNode: SemanticsNode
43         get() {
44             return SemanticsNode(rootNode, mergingEnabled = true)
45         }
46 
47     val unmergedRootSemanticsNode: SemanticsNode
48         get() {
49             return SemanticsNode(
50                 outerSemanticsNode = outerSemanticsNode,
51                 layoutNode = rootNode,
52                 mergingEnabled = false,
53                 // Forcing an empty SemanticsConfiguration here since the root node will always
54                 // have an empty config, but if we don't pass this in explicitly here it will try
55                 // to call `rootNode.collapsedSemantics` which will fail because the LayoutNode
56                 // is not yet attached when this getter is first called.
57                 unmergedConfig = SemanticsConfiguration()
58             )
59         }
60 
61     internal val listeners = MutableObjectList<SemanticsListener>(2)
62 
63     internal val rootInfo: SemanticsInfo
64         get() = rootNode
65 
66     internal operator fun get(semanticsId: Int): SemanticsInfo? {
67         return nodes[semanticsId]
68     }
69 
70     internal fun notifySemanticsChange(
71         semanticsInfo: SemanticsInfo,
72         previousSemanticsConfiguration: SemanticsConfiguration?
73     ) {
74         listeners.forEach { it.onSemanticsChanged(semanticsInfo, previousSemanticsConfiguration) }
75     }
76 }
77 
78 /**
79  * Finds all [SemanticsNode]s in the tree owned by this [SemanticsOwner]. Return the results in a
80  * list.
81  *
82  * @param mergingEnabled set to true if you want the data to be merged.
83  * @param skipDeactivatedNodes set to false if you want to collect the nodes which are deactivated.
84  *   For example, the children of [androidx.compose.ui.layout.SubcomposeLayout] which are retained
85  *   to be reused in future are considered deactivated.
86  */
getAllSemanticsNodesnull87 fun SemanticsOwner.getAllSemanticsNodes(
88     mergingEnabled: Boolean,
89     skipDeactivatedNodes: Boolean = true
90 ): List<SemanticsNode> {
91     return getAllSemanticsNodesToMap(
92             useUnmergedTree = !mergingEnabled,
93             skipDeactivatedNodes = skipDeactivatedNodes
94         )
95         .values
96         .toList()
97 }
98 
99 @Suppress("unused")
100 @Deprecated(message = "Use a new overload instead", level = DeprecationLevel.HIDDEN)
getAllSemanticsNodesnull101 fun SemanticsOwner.getAllSemanticsNodes(mergingEnabled: Boolean) =
102     getAllSemanticsNodes(mergingEnabled, true)
103 
104 /**
105  * Finds all [SemanticsNode]s in the tree owned by this [SemanticsOwner]. Return the results in a
106  * map.
107  */
108 internal fun SemanticsOwner.getAllSemanticsNodesToMap(
109     useUnmergedTree: Boolean = false,
110     skipDeactivatedNodes: Boolean = true
111 ): Map<Int, SemanticsNode> {
112     val nodes = mutableMapOf<Int, SemanticsNode>()
113 
114     fun findAllSemanticNodesRecursive(currentNode: SemanticsNode) {
115         nodes[currentNode.id] = currentNode
116         currentNode.getChildren(includeDeactivatedNodes = !skipDeactivatedNodes).fastForEach { child
117             ->
118             findAllSemanticNodesRecursive(child)
119         }
120     }
121 
122     val root = if (useUnmergedTree) unmergedRootSemanticsNode else rootSemanticsNode
123     if (!skipDeactivatedNodes || !root.layoutNode.isDeactivated) {
124         findAllSemanticNodesRecursive(root)
125     }
126     return nodes
127 }
128 
isImportantForAccessibilitynull129 internal fun SemanticsNode.isImportantForAccessibility() =
130     !isHidden &&
131         (unmergedConfig.isMergingSemanticsOfDescendants ||
132             unmergedConfig.containsImportantForAccessibility())
133 
134 @Suppress("DEPRECATION")
135 internal val SemanticsNode.isHidden: Boolean
136     // A node is considered hidden if it is transparent, or explicitly is hidden from accessibility.
137     // This also checks if the node has been marked as `invisibleToUser`, which is what the
138     // `hiddenFromAccessibility` API used to  be named.
139     get() =
140         isTransparent ||
141             (unmergedConfig.contains(HideFromAccessibility) ||
142                 unmergedConfig.contains(InvisibleToUser))
143 
144 private val DefaultFakeNodeBounds = Rect(0f, 0f, 10f, 10f)
145 
146 /** Semantics node with adjusted bounds for the uncovered(by siblings) part. */
147 internal class SemanticsNodeWithAdjustedBounds(
148     val semanticsNode: SemanticsNode,
149     val adjustedBounds: IntRect
150 )
151 
152 /**
153  * Finds pruned [SemanticsNode]s in the tree owned by this [SemanticsOwner]. A semantics node
154  * completely covered by siblings drawn on top of it will be pruned. Return the results in a map.
155  */
156 internal fun SemanticsOwner.getAllUncoveredSemanticsNodesToIntObjectMap(
157     customRootNodeId: Int
158 ): IntObjectMap<SemanticsNodeWithAdjustedBounds> {
159     val root = unmergedRootSemanticsNode
160     if (!root.layoutNode.isPlaced || !root.layoutNode.isAttached) {
161         return emptyIntObjectMap()
162     }
163 
164     // Default capacity chosen to accommodate common scenarios
165     val nodes = MutableIntObjectMap<SemanticsNodeWithAdjustedBounds>(48)
166 
167     val unaccountedSpace = SemanticsRegion()
168     unaccountedSpace.set(root.boundsInRoot.roundToIntRect())
169 
170     fun findAllSemanticNodesRecursive(currentNode: SemanticsNode, region: SemanticsRegion) {
171         val notAttachedOrPlaced =
172             !currentNode.layoutNode.isPlaced || !currentNode.layoutNode.isAttached
173         if (
174             (unaccountedSpace.isEmpty && currentNode.id != root.id) ||
175                 (notAttachedOrPlaced && !currentNode.isFake)
176         ) {
177             return
178         }
179         val touchBoundsInRoot = currentNode.touchBoundsInRoot.roundToIntRect()
180 
181         region.set(touchBoundsInRoot)
182 
183         val virtualViewId =
184             if (currentNode.id == root.id) {
185                 customRootNodeId
186             } else {
187                 currentNode.id
188             }
189         if (region.intersect(unaccountedSpace)) {
190             nodes[virtualViewId] = SemanticsNodeWithAdjustedBounds(currentNode, region.bounds)
191             // Children could be drawn outside of parent, but we are using clipped bounds for
192             // accessibility now, so let's put the children recursion inside of this if. If later
193             // we decide to support children drawn outside of parent, we can move it out of the
194             // if block.
195             val children = currentNode.replacedChildren
196             for (i in children.size - 1 downTo 0) {
197                 // Links in text nodes are semantics children. But for Android accessibility support
198                 // we don't publish them to the accessibility services because they are exposed
199                 // as UrlSpan/ClickableSpan spans instead
200                 if (children[i].config.contains(SemanticsProperties.LinkTestMarker)) {
201                     continue
202                 }
203                 findAllSemanticNodesRecursive(children[i], region)
204             }
205             if (currentNode.isImportantForAccessibility()) {
206                 unaccountedSpace.difference(touchBoundsInRoot)
207             }
208         } else {
209             if (currentNode.isFake) {
210                 val parentNode = currentNode.parent
211                 // use parent bounds for fake node
212                 val boundsForFakeNode =
213                     if (parentNode?.layoutInfo?.isPlaced == true) {
214                         parentNode.boundsInRoot
215                     } else {
216                         DefaultFakeNodeBounds
217                     }
218                 nodes[virtualViewId] =
219                     SemanticsNodeWithAdjustedBounds(currentNode, boundsForFakeNode.roundToIntRect())
220             } else if (virtualViewId == customRootNodeId) {
221                 // Root view might have WRAP_CONTENT layout params in which case it will have zero
222                 // bounds if there is no other content with semantics. But we need to always send
223                 // the
224                 // root view info as there are some other apps (e.g. Google Assistant) that depend
225                 // on accessibility info
226                 nodes[virtualViewId] = SemanticsNodeWithAdjustedBounds(currentNode, region.bounds)
227             }
228         }
229     }
230 
231     findAllSemanticNodesRecursive(root, SemanticsRegion())
232     return nodes
233 }
234