1 /*
2  * 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.node
18 
19 import androidx.compose.ui.util.fastAny
20 import androidx.compose.ui.util.fastFirstOrNull
21 import androidx.compose.ui.util.fastForEach
22 
23 /**
24  * There are some contracts between the tree of LayoutNodes and the state of AndroidComposeView
25  * which is hard to enforce but important to maintain. This method is intended to do the work only
26  * during our tests and will iterate through the tree to validate the states consistency.
27  */
28 internal class LayoutTreeConsistencyChecker(
29     private val root: LayoutNode,
30     private val relayoutNodes: DepthSortedSetsForDifferentPasses,
31     private val postponedMeasureRequests: List<MeasureAndLayoutDelegate.PostponedRequest>
32 ) {
assertConsistentnull33     fun assertConsistent() {
34         val inconsistencyFound = !isTreeConsistent(root)
35         if (inconsistencyFound) {
36             println(logTree())
37             throw IllegalStateException("Inconsistency found!")
38         }
39     }
40 
isTreeConsistentnull41     private fun isTreeConsistent(node: LayoutNode): Boolean {
42         if (!node.consistentLayoutState()) {
43             return false
44         }
45         node.children.fastForEach {
46             if (!isTreeConsistent(it)) {
47                 return@isTreeConsistent false
48             }
49         }
50         return true
51     }
52 
LayoutNodenull53     private fun LayoutNode.consistentLayoutState(): Boolean {
54         val parent = this.parent
55         val parentLayoutState = parent?.layoutState
56         if (isPlaced || placeOrder != LayoutNode.NotPlacedPlaceOrder && parent?.isPlaced == true) {
57             if (
58                 measurePending &&
59                     postponedMeasureRequests.fastFirstOrNull {
60                         it.node == this && !it.isLookahead
61                     } != null
62             ) {
63                 // this node is waiting to be measured by parent or if this will not happen
64                 // `onRequestMeasure` will be called for all items in `postponedMeasureRequests`
65                 return true
66             }
67             if (isDeactivated) {
68                 // remeasure/relayout requests for deactivated nodes are ignored
69                 return true
70             }
71             // remeasure or relayout is scheduled
72             if (measurePending) {
73                 return relayoutNodes.contains(this) ||
74                     layoutState == LayoutNode.LayoutState.LookaheadMeasuring ||
75                     parent?.measurePending == true ||
76                     parent?.lookaheadMeasurePending == true ||
77                     parentLayoutState == LayoutNode.LayoutState.Measuring
78             }
79             if (layoutPending) {
80                 return relayoutNodes.contains(this) ||
81                     parent == null ||
82                     parent.measurePending ||
83                     parent.layoutPending ||
84                     parentLayoutState == LayoutNode.LayoutState.Measuring ||
85                     parentLayoutState == LayoutNode.LayoutState.LayingOut ||
86                     postponedMeasureRequests.fastAny { it.node == this } ||
87                     layoutState == LayoutNode.LayoutState.Measuring
88             }
89         }
90         if (isPlacedInLookahead == true) {
91             if (
92                 lookaheadMeasurePending &&
93                     postponedMeasureRequests.fastFirstOrNull {
94                         it.node == this && it.isLookahead
95                     } != null
96             ) {
97                 // this node is waiting to be lookahead measured by parent or if this will not
98                 // happen `onRequestLookaheadMeasure` will be called for all items in
99                 // `postponedLookaheadMeasureRequests`
100                 return true
101             }
102             if (lookaheadMeasurePending) {
103                 return relayoutNodes.contains(this, true) ||
104                     parent?.lookaheadMeasurePending == true ||
105                     parentLayoutState == LayoutNode.LayoutState.LookaheadMeasuring ||
106                     (parent?.measurePending == true && lookaheadRoot == this)
107             }
108             if (lookaheadLayoutPending) {
109                 return relayoutNodes.contains(this, true) ||
110                     parent == null ||
111                     parent.lookaheadMeasurePending ||
112                     parent.lookaheadLayoutPending ||
113                     parentLayoutState == LayoutNode.LayoutState.LookaheadMeasuring ||
114                     parentLayoutState == LayoutNode.LayoutState.LookaheadLayingOut ||
115                     (parent.layoutPending && lookaheadRoot == this)
116             }
117         }
118         return true
119     }
120 
nodeToStringnull121     private fun nodeToString(node: LayoutNode): String {
122         return with(StringBuilder()) {
123             append(node)
124             append("[${node.layoutState}]")
125             if (!node.isPlaced) append("[!isPlaced]")
126             append("[measuredByParent=${node.measuredByParent}]")
127             if (!node.consistentLayoutState()) {
128                 append("[INCONSISTENT]")
129             }
130             toString()
131         }
132     }
133 
134     /** Prints the nodes tree into the logs. */
logTreenull135     private fun logTree(): String {
136         val stringBuilder = StringBuilder()
137         fun printSubTree(node: LayoutNode, depth: Int) {
138             var childrenDepth = depth
139             val nodeRepresentation = nodeToString(node)
140             if (nodeRepresentation.isNotEmpty()) {
141                 for (i in 0 until depth) {
142                     stringBuilder.append("..")
143                 }
144                 stringBuilder.appendLine(nodeRepresentation)
145                 childrenDepth += 1
146             }
147             node.children.fastForEach { printSubTree(it, childrenDepth) }
148         }
149         stringBuilder.appendLine("Tree state:")
150         printSubTree(root, 0)
151         return stringBuilder.toString()
152     }
153 }
154