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