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