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