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