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