1 /*
2  * Copyright (C) 2021 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 androidx.test.filters.SmallTest
20 import com.android.systemui.SysuiTestCase
21 import com.android.systemui.dump.logcatLogBuffer
22 import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager
23 import com.android.systemui.statusbar.notification.collection.GroupEntry
24 import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
25 import com.android.systemui.statusbar.notification.collection.ListEntry
26 import com.android.systemui.statusbar.notification.collection.NotificationEntry
27 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
28 import com.android.systemui.statusbar.notification.collection.getAttachState
29 import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection
30 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
31 import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider
32 import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING
33 import com.android.systemui.statusbar.notification.stack.BUCKET_PEOPLE
34 import com.android.systemui.statusbar.notification.stack.BUCKET_SILENT
35 import com.android.systemui.statusbar.notification.stack.PriorityBucket
36 import com.android.systemui.util.mockito.any
37 import com.android.systemui.util.mockito.mock
38 import org.junit.Before
39 import org.junit.Test
40 import org.mockito.Mockito
41 import org.mockito.Mockito.`when` as whenever
42 
43 @SmallTest
44 class NodeSpecBuilderTest : SysuiTestCase() {
45 
46     private val mediaContainerController: MediaContainerController = mock()
47     private val sectionsFeatureManager: NotificationSectionsFeatureManager = mock()
48     private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider = mock()
49     private val viewBarn: NotifViewBarn = mock()
50     private val logger = NodeSpecBuilderLogger(mock(), logcatLogBuffer())
51 
52     private var rootController: NodeController = buildFakeController("rootController")
53     private var headerController0: NodeController = buildFakeController("header0")
54     private var headerController1: NodeController = buildFakeController("header1")
55     private var headerController2: NodeController = buildFakeController("header2")
56 
57     private val section0Bucket = BUCKET_PEOPLE
58     private val section1Bucket = BUCKET_ALERTING
59     private val section2Bucket = BUCKET_SILENT
60 
61     private val section0 = buildSection(0, section0Bucket, headerController0)
62     private val section0NoHeader = buildSection(0, section0Bucket, null)
63     private val section1 = buildSection(1, section1Bucket, headerController1)
64     private val section1NoHeader = buildSection(1, section1Bucket, null)
65     private val section2 = buildSection(2, section2Bucket, headerController2)
66     private val section3 = buildSection(3, section2Bucket, headerController2)
67 
68     private val fakeViewBarn = FakeViewBarn()
69 
70     private lateinit var specBuilder: NodeSpecBuilder
71 
72     @Before
73     fun setUp() {
74         whenever(mediaContainerController.mediaContainerView).thenReturn(mock())
75         whenever(viewBarn.requireNodeController(any())).thenAnswer {
76             fakeViewBarn.getViewByEntry(it.getArgument(0))
77         }
78 
79         specBuilder = NodeSpecBuilder(mediaContainerController, sectionsFeatureManager,
80                 sectionHeaderVisibilityProvider, viewBarn, logger)
81     }
82 
83     @Test
84     fun testMultipleSectionsWithSameController() {
85         whenever(sectionHeaderVisibilityProvider.sectionHeadersVisible).thenReturn(true)
86         checkOutput(
87                 listOf(
88                         notif(0, section0),
89                         notif(1, section2),
90                         notif(2, section3)
91                 ),
92                 tree(
93                         node(headerController0),
94                         notifNode(0),
95                         node(headerController2),
96                         notifNode(1),
97                         notifNode(2)
98                 )
99         )
100     }
101 
102     @Test(expected = RuntimeException::class)
103     fun testMultipleSectionsWithSameControllerNonConsecutive() {
104         whenever(sectionHeaderVisibilityProvider.sectionHeadersVisible).thenReturn(true)
105         checkOutput(
106                 listOf(
107                         notif(0, section0),
108                         notif(1, section1),
109                         notif(2, section3),
110                         notif(3, section1)
111                 ),
112                 tree()
113         )
114     }
115 
116     @Test
117     fun testSimpleMapping() {
118         whenever(sectionHeaderVisibilityProvider.sectionHeadersVisible).thenReturn(true)
119         checkOutput(
120             // GIVEN a simple flat list of notifications all in the same headerless section
121             listOf(
122                 notif(0, section0NoHeader),
123                 notif(1, section0NoHeader),
124                 notif(2, section0NoHeader),
125                 notif(3, section0NoHeader)
126             ),
127 
128             // THEN we output a similarly simple flag list of nodes
129             tree(
130                 notifNode(0),
131                 notifNode(1),
132                 notifNode(2),
133                 notifNode(3)
134             )
135         )
136     }
137 
138     @Test
139     fun testSimpleMappingWithMedia() {
140         whenever(sectionHeaderVisibilityProvider.sectionHeadersVisible).thenReturn(true)
141         // WHEN media controls are enabled
142         whenever(sectionsFeatureManager.isMediaControlsEnabled()).thenReturn(true)
143 
144         checkOutput(
145                 // GIVEN a simple flat list of notifications all in the same headerless section
146                 listOf(
147                         notif(0, section0NoHeader),
148                         notif(1, section0NoHeader),
149                         notif(2, section0NoHeader),
150                         notif(3, section0NoHeader)
151                 ),
152 
153                 // THEN we output a similarly simple flag list of nodes, with media at the top
154                 tree(
155                         node(mediaContainerController),
156                         notifNode(0),
157                         notifNode(1),
158                         notifNode(2),
159                         notifNode(3)
160                 )
161         )
162     }
163 
164     @Test
165     fun testHeaderInjection() {
166         // WHEN section headers are supposed to be visible
167         whenever(sectionHeaderVisibilityProvider.sectionHeadersVisible).thenReturn(true)
168         checkOutput(
169                 // GIVEN a flat list of notifications, spread across three sections
170                 listOf(
171                         notif(0, section0),
172                         notif(1, section0),
173                         notif(2, section1),
174                         notif(3, section2)
175                 ),
176 
177                 // THEN each section has its header injected
178                 tree(
179                         node(headerController0),
180                         notifNode(0),
181                         notifNode(1),
182                         node(headerController1),
183                         notifNode(2),
184                         node(headerController2),
185                         notifNode(3)
186                 )
187         )
188     }
189 
190     @Test
191     fun testHeaderSuppression() {
192         // WHEN section headers are supposed to be hidden
193         whenever(sectionHeaderVisibilityProvider.sectionHeadersVisible).thenReturn(false)
194         checkOutput(
195                 // GIVEN a flat list of notifications, spread across three sections
196                 listOf(
197                         notif(0, section0),
198                         notif(1, section0),
199                         notif(2, section1),
200                         notif(3, section2)
201                 ),
202 
203                 // THEN each section has its header injected
204                 tree(
205                         notifNode(0),
206                         notifNode(1),
207                         notifNode(2),
208                         notifNode(3)
209                 )
210         )
211     }
212 
213     @Test
214     fun testGroups() {
215         whenever(sectionHeaderVisibilityProvider.sectionHeadersVisible).thenReturn(true)
216         checkOutput(
217                 // GIVEN a mixed list of top-level notifications and groups
218                 listOf(
219                     notif(0, section0),
220                     group(1, section1,
221                             notif(2),
222                             notif(3),
223                             notif(4)
224                     ),
225                     notif(5, section2),
226                     group(6, section2,
227                             notif(7),
228                             notif(8),
229                             notif(9)
230                     )
231                 ),
232 
233                 // THEN we properly construct all the nodes
234                 tree(
235                         node(headerController0),
236                         notifNode(0),
237                         node(headerController1),
238                         notifNode(1,
239                                 notifNode(2),
240                                 notifNode(3),
241                                 notifNode(4)
242                         ),
243                         node(headerController2),
244                         notifNode(5),
245                         notifNode(6,
246                                 notifNode(7),
247                                 notifNode(8),
248                                 notifNode(9)
249                         )
250                 )
251         )
252     }
253 
254     @Test
255     fun testSecondSectionWithNoHeader() {
256         whenever(sectionHeaderVisibilityProvider.sectionHeadersVisible).thenReturn(true)
257         checkOutput(
258                 // GIVEN a middle section with no associated header view
259                 listOf(
260                         notif(0, section0),
261                         notif(1, section1NoHeader),
262                         group(2, section1NoHeader,
263                                 notif(3),
264                                 notif(4)
265                         ),
266                         notif(5, section2)
267                 ),
268 
269                 // THEN the header view is left out of the tree (but the notifs are still present)
270                 tree(
271                         node(headerController0),
272                         notifNode(0),
273                         notifNode(1),
274                         notifNode(2,
275                                 notifNode(3),
276                                 notifNode(4)
277                         ),
278                         node(headerController2),
279                         notifNode(5)
280                 )
281         )
282     }
283 
284     @Test(expected = RuntimeException::class)
285     fun testRepeatedSectionsThrow() {
286         whenever(sectionHeaderVisibilityProvider.sectionHeadersVisible).thenReturn(true)
287         checkOutput(
288                 // GIVEN a malformed list where sections are not contiguous
289                 listOf(
290                         notif(0, section0),
291                         notif(1, section1),
292                         notif(2, section0)
293                 ),
294 
295                 // THEN an exception is thrown
296                 tree()
297         )
298     }
299 
300     private fun checkOutput(list: List<ListEntry>, desiredTree: NodeSpecImpl) {
301         checkTree(desiredTree, specBuilder.buildNodeSpec(rootController, list))
302     }
303 
304     private fun checkTree(desiredTree: NodeSpec, actualTree: NodeSpec) {
305         try {
306             checkNode(desiredTree, actualTree)
307         } catch (e: AssertionError) {
308             throw AssertionError("Trees don't match: ${e.message}\nActual tree:\n" +
309                     treeSpecToStr(actualTree))
310         }
311     }
312 
313     private fun checkNode(desiredTree: NodeSpec, actualTree: NodeSpec) {
314         if (actualTree.controller != desiredTree.controller) {
315             throw AssertionError("Node {${actualTree.controller.nodeLabel}} should " +
316                     "be ${desiredTree.controller.nodeLabel}")
317         }
318         for (i in 0 until desiredTree.children.size) {
319             if (i >= actualTree.children.size) {
320                 throw AssertionError("Node {${actualTree.controller.nodeLabel}}" +
321                         " is missing child ${desiredTree.children[i].controller.nodeLabel}")
322             }
323             checkNode(desiredTree.children[i], actualTree.children[i])
324         }
325     }
326 
327     private fun notif(id: Int, section: NotifSection? = null): NotificationEntry {
328         val entry = NotificationEntryBuilder()
329                 .setId(id)
330                 .build()
331         if (section != null) {
332             getAttachState(entry).section = section
333         }
334         fakeViewBarn.buildNotifView(id, entry)
335         return entry
336     }
337 
338     private fun group(
339         id: Int,
340         section: NotifSection,
341         vararg children: NotificationEntry
342     ): GroupEntry {
343         val group = GroupEntryBuilder()
344                 .setKey("group_$id")
345                 .setSummary(
346                         NotificationEntryBuilder()
347                                 .setId(id)
348                                 .build())
349                 .setChildren(children.asList())
350                 .build()
351         getAttachState(group).section = section
352         fakeViewBarn.buildNotifView(id, group.summary!!)
353 
354         for (child in children) {
355             getAttachState(child).section = section
356         }
357         return group
358     }
359 
360     private fun tree(vararg children: NodeSpecImpl): NodeSpecImpl {
361         return node(rootController, *children)
362     }
363 
364     private fun node(view: NodeController, vararg children: NodeSpecImpl): NodeSpecImpl {
365         val node = NodeSpecImpl(null, view)
366         node.children.addAll(children)
367         return node
368     }
369 
370     private fun notifNode(id: Int, vararg children: NodeSpecImpl): NodeSpecImpl {
371         return node(fakeViewBarn.getViewById(id), *children)
372     }
373 }
374 
375 private class FakeViewBarn {
376     private val entries = mutableMapOf<Int, NotificationEntry>()
377     private val views = mutableMapOf<NotificationEntry, NodeController>()
378 
379     fun buildNotifView(id: Int, entry: NotificationEntry) {
380         if (entries.contains(id)) {
381             throw RuntimeException("ID $id is already in use")
382         }
383         entries[id] = entry
384         views[entry] = buildFakeController("Entry $id")
385     }
386 
387     fun getViewById(id: Int): NodeController {
388         return views[entries[id] ?: throw RuntimeException("No view with ID $id")]!!
389     }
390 
391     fun getViewByEntry(entry: NotificationEntry): NodeController {
392         return views[entry] ?: throw RuntimeException("No view defined for key ${entry.key}")
393     }
394 }
395 
396 private fun buildFakeController(name: String): NodeController {
397     val controller = Mockito.mock(NodeController::class.java)
398     whenever(controller.nodeLabel).thenReturn(name)
399     return controller
400 }
401 
402 private fun buildSection(
403     index: Int,
404     @PriorityBucket bucket: Int,
405     nodeController: NodeController?
406 ): NotifSection {
407     return NotifSection(object : NotifSectioner("Section $index (bucket=$bucket)", bucket) {
408 
409         override fun isInSection(entry: ListEntry?): Boolean {
410             throw NotImplementedError("This should never be called")
411         }
412 
413         override fun getHeaderNodeController(): NodeController? {
414             return nodeController
415         }
416     }, index)
417 }
418