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 package com.android.systemui.statusbar.notification.collection.render 17 18 import android.content.Context 19 import android.testing.AndroidTestingRunner 20 import android.view.View 21 import android.view.ViewGroup 22 import android.widget.FrameLayout 23 import androidx.test.filters.SmallTest 24 import com.android.systemui.SysuiTestCase 25 import com.android.systemui.dump.logcatLogBuffer 26 import org.junit.Assert 27 import org.junit.Before 28 import org.junit.Test 29 import org.junit.runner.RunWith 30 import org.mockito.ArgumentMatchers.isNull 31 import org.mockito.Mockito.anyBoolean 32 import org.mockito.Mockito.matches 33 import org.mockito.Mockito.spy 34 import org.mockito.Mockito.verify 35 36 @SmallTest 37 @RunWith(AndroidTestingRunner::class) 38 class ShadeViewDifferTest : SysuiTestCase() { 39 private lateinit var differ: ShadeViewDiffer 40 private val rootController = FakeController(mContext, "RootController") 41 private val controller1 = FakeController(mContext, "Controller1") 42 private val controller2 = FakeController(mContext, "Controller2") 43 private val controller3 = FakeController(mContext, "Controller3") 44 private val controller4 = FakeController(mContext, "Controller4") 45 private val controller5 = FakeController(mContext, "Controller5") 46 private val controller6 = FakeController(mContext, "Controller6") 47 private val controller7 = FakeController(mContext, "Controller7") 48 private val logger = spy(ShadeViewDifferLogger(logcatLogBuffer())) 49 50 @Before 51 fun setUp() { 52 differ = ShadeViewDiffer(rootController, logger) 53 } 54 55 @Test 56 fun testAddInitialViews() { 57 // WHEN a spec is applied to an empty root 58 // THEN the final tree matches the spec 59 applySpecAndCheck( 60 node(controller1), 61 node(controller2, node(controller3), node(controller4)), 62 node(controller5) 63 ) 64 } 65 66 @Test 67 fun testDetachViews() { 68 // GIVEN a preexisting tree of controllers 69 applySpecAndCheck( 70 node(controller1), 71 node(controller2, node(controller3), node(controller4)), 72 node(controller5) 73 ) 74 75 // WHEN the new spec removes nodes 76 // THEN the final tree matches the spec 77 applySpecAndCheck(node(controller5)) 78 } 79 80 @Test 81 fun testReparentChildren() { 82 // GIVEN a preexisting tree of controllers 83 applySpecAndCheck( 84 node(controller1), 85 node(controller2, node(controller3), node(controller4)), 86 node(controller5) 87 ) 88 89 // WHEN the parents of the controllers are all shuffled around 90 // THEN the final tree matches the spec 91 applySpecAndCheck( 92 node(controller1), 93 node(controller4), 94 node(controller3, node(controller2)) 95 ) 96 } 97 98 @Test 99 fun testReorderChildren() { 100 // GIVEN a preexisting tree of controllers 101 applySpecAndCheck( 102 node(controller1), 103 node(controller2), 104 node(controller3), 105 node(controller4) 106 ) 107 108 // WHEN the children change order 109 // THEN the final tree matches the spec 110 applySpecAndCheck( 111 node(controller3), 112 node(controller2), 113 node(controller4), 114 node(controller1) 115 ) 116 } 117 118 @Test 119 fun testRemovedGroupsAreBrokenApart() { 120 // GIVEN a preexisting tree with a group 121 applySpecAndCheck( 122 node(controller1), 123 node(controller2, node(controller3), node(controller4), node(controller5)) 124 ) 125 126 // WHEN the new spec removes the entire group 127 applySpecAndCheck(node(controller1)) 128 129 // THEN the group children are no longer attached to their parent 130 Assert.assertNull(controller3.view.parent) 131 Assert.assertNull(controller4.view.parent) 132 Assert.assertNull(controller5.view.parent) 133 verifyDetachingChildLogged(controller3, oldParent = controller2) 134 verifyDetachingChildLogged(controller4, oldParent = controller2) 135 verifyDetachingChildLogged(controller5, oldParent = controller2) 136 } 137 138 @Test 139 fun testRemovedGroupsWithKeepInParentAreKeptTogether() { 140 // GIVEN a preexisting tree with a group 141 // AND the group children supports keepInParent 142 applySpecAndCheck( 143 node(controller1), 144 node(controller2, node(controller3), node(controller4), node(controller5)) 145 ) 146 controller3.supportsKeepInParent = true 147 controller4.supportsKeepInParent = true 148 controller5.supportsKeepInParent = true 149 150 // WHEN the new spec removes the entire group 151 applySpecAndCheck(node(controller1)) 152 153 // THEN the group children are still attached to their parent 154 Assert.assertEquals(controller2.view, controller3.view.parent) 155 Assert.assertEquals(controller2.view, controller4.view.parent) 156 Assert.assertEquals(controller2.view, controller5.view.parent) 157 verifySkipDetachingChildLogged(controller3, parent = controller2) 158 verifySkipDetachingChildLogged(controller4, parent = controller2) 159 verifySkipDetachingChildLogged(controller5, parent = controller2) 160 } 161 162 @Test 163 fun testReuseRemovedGroupsWithKeepInParent() { 164 // GIVEN a preexisting tree with a dismissed group 165 // AND the group children supports keepInParent 166 controller3.supportsKeepInParent = true 167 controller4.supportsKeepInParent = true 168 controller5.supportsKeepInParent = true 169 applySpecAndCheck( 170 node(controller1), 171 node(controller2, node(controller3), node(controller4), node(controller5)) 172 ) 173 applySpecAndCheck(node(controller1)) 174 175 // WHEN a new spec is applied which reuses the dismissed views 176 applySpecAndCheck( 177 node(controller1), 178 node(controller2), 179 node(controller3), 180 node(controller4), 181 node(controller5) 182 ) 183 184 // THEN the dismissed views can be reused 185 Assert.assertEquals(rootController.view, controller3.view.parent) 186 Assert.assertEquals(rootController.view, controller4.view.parent) 187 Assert.assertEquals(rootController.view, controller5.view.parent) 188 verifyDetachingChildLogged(controller3, oldParent = null) 189 verifyDetachingChildLogged(controller4, oldParent = null) 190 verifyDetachingChildLogged(controller5, oldParent = null) 191 } 192 193 @Test 194 fun testUnmanagedViews() { 195 // GIVEN a preexisting tree of controllers 196 applySpecAndCheck( 197 node(controller1), 198 node(controller2, node(controller3), node(controller4)), 199 node(controller5) 200 ) 201 202 // GIVEN some additional unmanaged views attached to the tree 203 val unmanagedView1 = View(mContext) 204 val unmanagedView2 = View(mContext) 205 rootController.view.addView(unmanagedView1, 1) 206 controller2.view.addView(unmanagedView2, 0) 207 208 // WHEN a new spec is applied with additional nodes 209 // THEN the final tree matches the spec 210 applySpecAndCheck( 211 node(controller1), 212 node(controller2, node(controller3), node(controller4), node(controller6)), 213 node(controller5), 214 node(controller7) 215 ) 216 217 // THEN the unmanaged views have been pushed to the end of their parents 218 Assert.assertEquals(unmanagedView1, rootController.view.getChildAt(4)) 219 Assert.assertEquals(unmanagedView2, controller2.view.getChildAt(3)) 220 } 221 222 private fun applySpecAndCheck(spec: NodeSpec) { 223 differ.applySpec(spec) 224 checkMatchesSpec(spec) 225 } 226 227 private fun applySpecAndCheck(vararg children: SpecBuilder) { 228 applySpecAndCheck(node(rootController, *children).build()) 229 } 230 231 private fun checkMatchesSpec(spec: NodeSpec) { 232 val parent = spec.controller 233 val children = spec.children 234 for (i in children.indices) { 235 val childSpec = children[i] 236 val view = parent.getChildAt(i) 237 Assert.assertEquals( 238 "Child $i of parent ${parent.nodeLabel} " + 239 "should be ${childSpec.controller.nodeLabel} " + 240 "but instead " + 241 view?.let(differ::getViewLabel), 242 view, 243 childSpec.controller.view 244 ) 245 if (childSpec.children.isNotEmpty()) { 246 checkMatchesSpec(childSpec) 247 } 248 } 249 } 250 251 private fun verifySkipDetachingChildLogged(child: NodeController, parent: NodeController) { 252 verify(logger) 253 .logSkipDetachingChild( 254 key = matches(child.nodeLabel), 255 parentKey = matches(parent.nodeLabel), 256 anyBoolean(), 257 anyBoolean() 258 ) 259 } 260 261 private fun verifyDetachingChildLogged(child: NodeController, oldParent: NodeController?) { 262 verify(logger) 263 .logDetachingChild( 264 key = matches(child.nodeLabel), 265 isTransfer = anyBoolean(), 266 isParentRemoved = anyBoolean(), 267 oldParent = oldParent?.let { matches(it.nodeLabel) } ?: isNull(), 268 newParent = isNull() 269 ) 270 } 271 272 private class FakeController(context: Context, label: String) : NodeController { 273 var supportsKeepInParent: Boolean = false 274 275 override val view: FrameLayout = FrameLayout(context) 276 override val nodeLabel: String = label 277 override fun getChildCount(): Int = view.childCount 278 279 override fun getChildAt(index: Int): View? { 280 return view.getChildAt(index) 281 } 282 283 override fun addChildAt(child: NodeController, index: Int) { 284 view.addView(child.view, index) 285 } 286 287 override fun moveChildTo(child: NodeController, index: Int) { 288 view.removeView(child.view) 289 view.addView(child.view, index) 290 } 291 292 override fun removeChild(child: NodeController, isTransfer: Boolean) { 293 view.removeView(child.view) 294 } 295 296 override fun onViewAdded() {} 297 override fun onViewMoved() {} 298 override fun onViewRemoved() {} 299 override fun offerToKeepInParentForAnimation(): Boolean { 300 return supportsKeepInParent 301 } 302 303 override fun removeFromParentIfKeptForAnimation(): Boolean { 304 if (supportsKeepInParent) { 305 (view.parent as? ViewGroup)?.removeView(view) 306 return true 307 } 308 309 return false 310 } 311 312 override fun resetKeepInParentForAnimation() { 313 supportsKeepInParent = false 314 } 315 } 316 317 private class SpecBuilder( 318 private val mController: NodeController, 319 private val children: Array<out SpecBuilder> 320 ) { 321 322 @JvmOverloads 323 fun build(parent: NodeSpec? = null): NodeSpec { 324 val spec = NodeSpecImpl(parent, mController) 325 for (childBuilder in children) { 326 spec.children.add(childBuilder.build(spec)) 327 } 328 return spec 329 } 330 } 331 332 companion object { 333 private fun node(controller: NodeController, vararg children: SpecBuilder): SpecBuilder { 334 return SpecBuilder(controller, children) 335 } 336 } 337 } 338