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