1 /*
2  * Copyright (C) 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 com.android.systemui.statusbar.notification.collection.render
18 
19 import android.annotation.MainThread
20 import android.view.View
21 import com.android.systemui.util.traceSection
22 
23 /**
24  * Given a "spec" that describes a "tree" of views, adds and removes views from the
25  * [rootController] and its children until the actual tree matches the spec.
26  *
27  * Every node in the spec tree must specify both a view and its associated [NodeController].
28  * Commands to add/remove/reorder children are sent to the controller. How the controller
29  * interprets these commands is left to its own discretion -- it might add them directly to its
30  * associated view or to some subview container.
31  *
32  * It's possible for nodes to mix "unmanaged" views in alongside managed ones within the same
33  * container. In this case, whenever the differ runs it will move all unmanaged views to the end
34  * of the node's child list.
35  */
36 @MainThread
37 class ShadeViewDiffer(
38     rootController: NodeController,
39     private val logger: ShadeViewDifferLogger
40 ) {
41     private val rootNode = ShadeNode(rootController)
42     private val nodes = mutableMapOf(rootController to rootNode)
43 
44     /**
45      * Adds and removes views from the root (and its children) until their structure matches the
46      * provided [spec]. The root node of the spec must match the root controller passed to the
47      * differ's constructor.
48      */
49     fun applySpec(spec: NodeSpec) = traceSection("ShadeViewDiffer.applySpec") {
50         val specMap = treeToMap(spec)
51 
52         if (spec.controller != rootNode.controller) {
53             throw IllegalArgumentException("Tree root ${spec.controller.nodeLabel} does not " +
54                     "match own root at ${rootNode.label}")
55         }
56 
57         detachChildren(rootNode, specMap)
58         attachChildren(rootNode, specMap)
59     }
60 
61     /**
62      * If [view] is managed by this differ, then returns the label of the view's controller.
63      * Otherwise returns View.toString().
64      *
65      * For debugging purposes.
66      */
67     fun getViewLabel(view: View): String =
68             nodes.values.firstOrNull { node -> node.view === view }?.label ?: view.toString()
69 
70     private fun detachChildren(
71         parentNode: ShadeNode,
72         specMap: Map<NodeController, NodeSpec>
73     ) = traceSection("detachChildren") {
74         val views = nodes.values.associateBy { it.view }
75         fun detachRecursively(parentNode: ShadeNode, specMap: Map<NodeController, NodeSpec>) {
76             val parentSpec = specMap[parentNode.controller]
77             for (i in parentNode.getChildCount() - 1 downTo 0) {
78                 val childView = parentNode.getChildAt(i)
79                 views[childView]?.let { childNode ->
80                     val childSpec = specMap[childNode.controller]
81                     maybeDetachChild(parentNode, parentSpec, childNode, childSpec)
82                     if (childNode.controller.getChildCount() > 0) {
83                         detachRecursively(childNode, specMap)
84                     }
85                 }
86             }
87         }
88         detachRecursively(parentNode, specMap)
89     }
90 
91     private fun maybeDetachChild(
92             parentNode: ShadeNode,
93             parentSpec: NodeSpec?,
94             childNode: ShadeNode,
95             childSpec: NodeSpec?
96     ) {
97         val newParentNode = childSpec?.parent?.let { getNode(it) }
98 
99         if (newParentNode != parentNode) {
100             val childCompletelyRemoved = newParentNode == null
101 
102             if (childCompletelyRemoved) {
103                 nodes.remove(childNode.controller)
104             }
105 
106             if (childCompletelyRemoved && parentSpec == null &&
107                     childNode.offerToKeepInParentForAnimation()) {
108                 // If both the child and the parent are being removed at the same time, then
109                 // keep the child attached to the parent for animation purposes
110                 logger.logSkipDetachingChild(
111                         key = childNode.label,
112                         parentKey = parentNode.label,
113                         isTransfer = !childCompletelyRemoved,
114                         isParentRemoved = true
115                 )
116             } else {
117                 logger.logDetachingChild(
118                         key = childNode.label,
119                         isTransfer = !childCompletelyRemoved,
120                         isParentRemoved = parentSpec == null,
121                         oldParent = parentNode.label,
122                         newParent = newParentNode?.label
123                 )
124                 parentNode.removeChild(childNode, isTransfer = !childCompletelyRemoved)
125                 childNode.parent = null
126             }
127         }
128     }
129 
130     private fun attachChildren(
131         parentNode: ShadeNode,
132         specMap: Map<NodeController, NodeSpec>
133     ): Unit = traceSection("attachChildren") {
134         val parentSpec = checkNotNull(specMap[parentNode.controller])
135 
136         for ((index, childSpec) in parentSpec.children.withIndex()) {
137             val currView = parentNode.getChildAt(index)
138             val childNode = getNode(childSpec)
139 
140             if (childNode.view != currView) {
141                 val removedFromParent = childNode.removeFromParentIfKeptForAnimation()
142                 if (removedFromParent) {
143                     logger.logDetachingChild(
144                             key = childNode.label,
145                             isTransfer = false,
146                             isParentRemoved = true,
147                             oldParent = null,
148                             newParent = null
149                     )
150                 }
151 
152                 when (childNode.parent) {
153                     null -> {
154                         // A new child (either newly created or coming from some other parent)
155                         logger.logAttachingChild(childNode.label, parentNode.label, index)
156                         parentNode.addChildAt(childNode, index)
157                         childNode.parent = parentNode
158                     }
159                     parentNode -> {
160                         // A pre-existing child, just in the wrong position. Move it into place
161                         logger.logMovingChild(childNode.label, parentNode.label, index)
162                         parentNode.moveChildTo(childNode, index)
163                     }
164                     else -> {
165                         // Error: child still has a parent. We should have detached it in the
166                         // previous step.
167                         throw IllegalStateException("Child ${childNode.label} should have " +
168                                 "parent ${parentNode.label} but is actually " +
169                                 "${childNode.parent?.label}")
170                     }
171                 }
172             }
173 
174             childNode.resetKeepInParentForAnimation()
175 
176             if (childSpec.children.isNotEmpty()) {
177                 attachChildren(childNode, specMap)
178             }
179         }
180     }
181 
182     private fun getNode(spec: NodeSpec): ShadeNode {
183         var node = nodes[spec.controller]
184         if (node == null) {
185             node = ShadeNode(spec.controller)
186             nodes[node.controller] = node
187         }
188         return node
189     }
190 
191     private fun treeToMap(tree: NodeSpec): Map<NodeController, NodeSpec> {
192         val map = mutableMapOf<NodeController, NodeSpec>()
193 
194         try {
195             registerNodes(tree, map)
196         } catch (ex: DuplicateNodeException) {
197             logger.logDuplicateNodeInTree(tree, ex)
198             throw ex
199         }
200 
201         return map
202     }
203 
204     private fun registerNodes(node: NodeSpec, map: MutableMap<NodeController, NodeSpec>) {
205         if (map.containsKey(node.controller)) {
206             throw DuplicateNodeException("Node ${node.controller.nodeLabel} appears more than once")
207         }
208         map[node.controller] = node
209 
210         if (node.children.isNotEmpty()) {
211             for (child in node.children) {
212                 registerNodes(child, map)
213             }
214         }
215     }
216 }
217 
218 private class DuplicateNodeException(message: String) : RuntimeException(message)
219 
220 private class ShadeNode(val controller: NodeController) {
221     val view: View
222         get() = controller.view
223 
224     var parent: ShadeNode? = null
225 
226     val label: String
227         get() = controller.nodeLabel
228 
229     fun getChildAt(index: Int): View? = controller.getChildAt(index)
230 
231     fun getChildCount(): Int = controller.getChildCount()
232 
233     fun addChildAt(child: ShadeNode, index: Int) {
234         controller.addChildAt(child.controller, index)
235         child.controller.onViewAdded()
236     }
237 
238     fun moveChildTo(child: ShadeNode, index: Int) {
239         controller.moveChildTo(child.controller, index)
240         child.controller.onViewMoved()
241     }
242 
243     fun removeChild(child: ShadeNode, isTransfer: Boolean) {
244         controller.removeChild(child.controller, isTransfer)
245         child.controller.onViewRemoved()
246     }
247 
248     fun offerToKeepInParentForAnimation(): Boolean {
249         return controller.offerToKeepInParentForAnimation()
250     }
251 
252     fun removeFromParentIfKeptForAnimation(): Boolean {
253         return controller.removeFromParentIfKeptForAnimation()
254     }
255 
256     fun resetKeepInParentForAnimation() {
257         controller.resetKeepInParentForAnimation()
258     }
259 }
260