1 /* <lambda>null2 * Copyright (C) 2022 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.stack 18 19 import android.annotation.DimenRes 20 import android.platform.test.annotations.EnableFlags 21 import android.service.notification.StatusBarNotification 22 import android.view.View.VISIBLE 23 import androidx.test.ext.junit.runners.AndroidJUnit4 24 import androidx.test.filters.SmallTest 25 import com.android.systemui.SysuiTestCase 26 import com.android.systemui.kosmos.testScope 27 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager 28 import com.android.systemui.res.R 29 import com.android.systemui.statusbar.LockscreenShadeTransitionController 30 import com.android.systemui.statusbar.StatusBarState 31 import com.android.systemui.statusbar.SysuiStatusBarStateController 32 import com.android.systemui.statusbar.notification.collection.EntryAdapter 33 import com.android.systemui.statusbar.notification.collection.NotificationEntry 34 import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor 35 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi 36 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow 37 import com.android.systemui.statusbar.notification.row.ExpandableView 38 import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController 39 import com.android.systemui.testKosmos 40 import com.android.systemui.util.mockito.any 41 import com.android.systemui.util.mockito.eq 42 import com.android.systemui.util.mockito.nullable 43 import com.google.common.truth.Truth.assertThat 44 import org.junit.Before 45 import org.junit.Test 46 import org.junit.runner.RunWith 47 import org.mockito.Mock 48 import org.mockito.Mockito.mock 49 import org.mockito.Mockito.`when` as whenever 50 import org.mockito.MockitoAnnotations 51 52 @SmallTest 53 @RunWith(AndroidJUnit4::class) 54 class NotificationStackSizeCalculatorTest : SysuiTestCase() { 55 56 @Mock private lateinit var sysuiStatusBarStateController: SysuiStatusBarStateController 57 @Mock 58 private lateinit var lockscreenShadeTransitionController: LockscreenShadeTransitionController 59 @Mock private lateinit var mediaDataManager: MediaDataManager 60 @Mock private lateinit var stackLayout: NotificationStackScrollLayout 61 @Mock private lateinit var seenNotificationsInteractor: SeenNotificationsInteractor 62 63 private val testableResources = mContext.orCreateTestableResources 64 private val kosmos = testKosmos() 65 private val testScope = kosmos.testScope 66 67 private lateinit var sizeCalculator: NotificationStackSizeCalculator 68 69 private val gapHeight = px(R.dimen.notification_section_divider_height) 70 private val dividerHeight = px(R.dimen.notification_divider_height) 71 private val shelfHeight = px(R.dimen.notification_shelf_height) 72 private val rowHeight = px(R.dimen.notification_max_height) 73 74 @Before 75 fun setUp() { 76 MockitoAnnotations.initMocks(this) 77 78 sizeCalculator = 79 NotificationStackSizeCalculator( 80 statusBarStateController = sysuiStatusBarStateController, 81 lockscreenShadeTransitionController = lockscreenShadeTransitionController, 82 mediaDataManager = mediaDataManager, 83 testableResources.resources, 84 ResourcesSplitShadeStateController(), 85 seenNotificationsInteractor = seenNotificationsInteractor, 86 scope = testScope, 87 ) 88 } 89 90 @Test 91 fun computeMaxKeyguardNotifications_zeroSpace_returnZero() { 92 val rows = listOf(createMockRow(height = rowHeight)) 93 94 val maxNotifications = 95 computeMaxKeyguardNotifications( 96 rows, 97 spaceForNotifications = 0f, 98 spaceForShelf = 0f, 99 shelfHeight = 0f, 100 ) 101 102 assertThat(maxNotifications).isEqualTo(0) 103 } 104 105 @Test 106 fun computeMaxKeyguardNotifications_infiniteSpace_returnsAll() { 107 val numberOfRows = 30 108 val rows = createLockscreenRows(numberOfRows) 109 110 val maxNotifications = 111 computeMaxKeyguardNotifications( 112 rows, 113 spaceForNotifications = Float.MAX_VALUE, 114 spaceForShelf = Float.MAX_VALUE, 115 shelfHeight, 116 ) 117 118 assertThat(maxNotifications).isEqualTo(numberOfRows) 119 } 120 121 @Test 122 fun computeMaxKeyguardNotifications_spaceForOneAndShelf_returnsOne() { 123 setGapHeight(gapHeight) 124 val shelfHeight = rowHeight / 2 // Shelf absence won't leave room for another row. 125 val spaceForNotifications = rowHeight + dividerHeight 126 val spaceForShelf = gapHeight + dividerHeight + shelfHeight 127 val rows = listOf(createMockRow(rowHeight), createMockRow(rowHeight)) 128 129 val maxNotifications = 130 computeMaxKeyguardNotifications(rows, spaceForNotifications, spaceForShelf, shelfHeight) 131 132 assertThat(maxNotifications).isEqualTo(1) 133 } 134 135 @Test 136 fun computeMaxKeyguardNotifications_onLockscreenSpaceForMinHeightButNotIntrinsicHeight_returnsOne() { 137 setGapHeight(0f) 138 // No divider height since we're testing one element where index = 0 139 140 whenever(sysuiStatusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) 141 whenever(lockscreenShadeTransitionController.fractionToShade).thenReturn(0f) 142 143 val row = createMockRow(10f, isSticky = true) 144 whenever(row.getMinHeight(any())).thenReturn(5) 145 146 val maxNotifications = 147 computeMaxKeyguardNotifications( 148 listOf(row), 149 /* spaceForNotifications= */ 5f, 150 /* spaceForShelf= */ 0f, 151 /* shelfHeight= */ 0f, 152 ) 153 154 assertThat(maxNotifications).isEqualTo(1) 155 } 156 157 @Test 158 @EnableFlags(PromotedNotificationUi.FLAG_NAME) 159 fun maxKeyguardNotificationsForPromotedOngoing_onLockscreenSpaceForMinHeightButNotIntrinsicHeight_returnsOne() { 160 setGapHeight(0f) 161 // No divider height since we're testing one element where index = 0 162 163 whenever(sysuiStatusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) 164 whenever(lockscreenShadeTransitionController.fractionToShade).thenReturn(0f) 165 166 val row = createMockRow(10f, isPromotedOngoing = true) 167 whenever(row.getMinHeight(any())).thenReturn(5) 168 169 val maxNotifications = 170 computeMaxKeyguardNotifications( 171 listOf(row), 172 /* spaceForNotifications= */ 5f, 173 /* spaceForShelf= */ 0f, 174 /* shelfHeight= */ 0f, 175 ) 176 177 assertThat(maxNotifications).isEqualTo(1) 178 } 179 180 @Test 181 fun computeMaxKeyguardNotifications_spaceForTwo_returnsTwo() { 182 setGapHeight(gapHeight) 183 val shelfHeight = shelfHeight + dividerHeight 184 val spaceForNotifications = 185 listOf(rowHeight + dividerHeight, gapHeight + rowHeight + dividerHeight).sum() 186 val spaceForShelf = gapHeight + dividerHeight + shelfHeight 187 val rows = 188 listOf(createMockRow(rowHeight), createMockRow(rowHeight), createMockRow(rowHeight)) 189 190 val maxNotifications = 191 computeMaxKeyguardNotifications( 192 rows, 193 spaceForNotifications + 1, 194 spaceForShelf, 195 shelfHeight, 196 ) 197 198 assertThat(maxNotifications).isEqualTo(2) 199 } 200 201 @Test 202 fun computeHeight_gapBeforeShelf_returnsSpaceUsed() { 203 // Each row in separate section. 204 setGapHeight(gapHeight) 205 206 val notifSpace = listOf(rowHeight, dividerHeight + gapHeight + rowHeight).sum() 207 208 val shelfSpace = dividerHeight + gapHeight + shelfHeight 209 val spaceUsed = notifSpace + shelfSpace 210 val rows = 211 listOf(createMockRow(rowHeight), createMockRow(rowHeight), createMockRow(rowHeight)) 212 213 val maxNotifications = 214 computeMaxKeyguardNotifications(rows, notifSpace, shelfSpace, shelfHeight) 215 assertThat(maxNotifications).isEqualTo(2) 216 217 val height = sizeCalculator.computeHeight(stackLayout, maxNotifications, this.shelfHeight) 218 assertThat(height).isEqualTo(spaceUsed) 219 } 220 221 @Test 222 fun computeHeight_noGapBeforeShelf_returnsSpaceUsed() { 223 // Both rows are in the same section. 224 setGapHeight(0f) 225 226 val spaceForNotifications = rowHeight 227 val spaceForShelf = dividerHeight + shelfHeight 228 val spaceUsed = spaceForNotifications + spaceForShelf 229 val rows = listOf(createMockRow(rowHeight), createMockRow(rowHeight)) 230 231 // test that we only use space required 232 val maxNotifications = 233 computeMaxKeyguardNotifications( 234 rows, 235 spaceForNotifications + 1, 236 spaceForShelf, 237 shelfHeight, 238 ) 239 assertThat(maxNotifications).isEqualTo(1) 240 241 val height = sizeCalculator.computeHeight(stackLayout, maxNotifications, this.shelfHeight) 242 assertThat(height).isEqualTo(spaceUsed) 243 } 244 245 @Test 246 fun onLockscreen_onKeyguard_AndNotGoingToShade_returnsTrue() { 247 whenever(sysuiStatusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) 248 whenever(lockscreenShadeTransitionController.fractionToShade).thenReturn(0f) 249 assertThat(sizeCalculator.onLockscreen()).isTrue() 250 } 251 252 @Test 253 fun onLockscreen_goingToShade_returnsFalse() { 254 whenever(sysuiStatusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) 255 whenever(lockscreenShadeTransitionController.fractionToShade).thenReturn(0.5f) 256 assertThat(sizeCalculator.onLockscreen()).isFalse() 257 } 258 259 @Test 260 fun onLockscreen_notOnLockscreen_returnsFalse() { 261 whenever(sysuiStatusBarStateController.state).thenReturn(StatusBarState.SHADE) 262 whenever(lockscreenShadeTransitionController.fractionToShade).thenReturn(1f) 263 assertThat(sizeCalculator.onLockscreen()).isFalse() 264 } 265 266 @Test 267 fun getSpaceNeeded_onLockscreenEnoughSpaceStickyHun_intrinsicHeight() { 268 setGapHeight(0f) 269 // No divider height since we're testing one element where index = 0 270 271 val row = createMockRow(10f, isSticky = true) 272 whenever(row.getMinHeight(any())).thenReturn(5) 273 274 val space = 275 sizeCalculator.getSpaceNeeded( 276 row, 277 visibleIndex = 0, 278 previousView = null, 279 stack = stackLayout, 280 onLockscreen = true, 281 ) 282 assertThat(space.whenEnoughSpace).isEqualTo(10f) 283 } 284 285 @Test 286 @EnableFlags(PromotedNotificationUi.FLAG_NAME) 287 fun getSpaceNeeded_onLockscreenEnoughSpacePromotedOngoing_intrinsicHeight() { 288 setGapHeight(0f) 289 // No divider height since we're testing one element where index = 0 290 291 val row = createMockRow(10f, isPromotedOngoing = true) 292 whenever(row.getMinHeight(any())).thenReturn(5) 293 294 val space = 295 sizeCalculator.getSpaceNeeded( 296 row, 297 visibleIndex = 0, 298 previousView = null, 299 stack = stackLayout, 300 onLockscreen = true, 301 ) 302 assertThat(space.whenEnoughSpace).isEqualTo(10f) 303 } 304 305 @Test 306 fun getSpaceNeeded_onLockscreenEnoughSpaceNotStickyHun_minHeight() { 307 setGapHeight(0f) 308 // No divider height since we're testing one element where index = 0 309 310 val row = createMockRow(rowHeight) 311 whenever(row.heightWithoutLockscreenConstraints).thenReturn(10) 312 whenever(row.getMinHeight(any())).thenReturn(5) 313 314 val space = 315 sizeCalculator.getSpaceNeeded( 316 row, 317 visibleIndex = 0, 318 previousView = null, 319 stack = stackLayout, 320 onLockscreen = true, 321 ) 322 assertThat(space.whenEnoughSpace).isEqualTo(5) 323 } 324 325 @Test 326 fun getSpaceNeeded_onLockscreenSavingSpaceStickyHun_minHeight() { 327 setGapHeight(0f) 328 // No divider height since we're testing one element where index = 0 329 330 val expandableView = createMockRow(10f, isSticky = true) 331 whenever(expandableView.getMinHeight(any())).thenReturn(5) 332 333 val space = 334 sizeCalculator.getSpaceNeeded( 335 expandableView, 336 visibleIndex = 0, 337 previousView = null, 338 stack = stackLayout, 339 onLockscreen = true, 340 ) 341 assertThat(space.whenSavingSpace).isEqualTo(5) 342 } 343 344 @Test 345 @EnableFlags(PromotedNotificationUi.FLAG_NAME) 346 fun getSpaceNeeded_onLockscreenSavingSpacePromotedOngoing_minHeight() { 347 setGapHeight(0f) 348 // No divider height since we're testing one element where index = 0 349 350 val expandableView = createMockRow(10f, isPromotedOngoing = true) 351 whenever(expandableView.getMinHeight(any())).thenReturn(5) 352 353 val space = 354 sizeCalculator.getSpaceNeeded( 355 expandableView, 356 visibleIndex = 0, 357 previousView = null, 358 stack = stackLayout, 359 onLockscreen = true, 360 ) 361 assertThat(space.whenSavingSpace).isEqualTo(5) 362 } 363 364 @Test 365 fun getSpaceNeeded_onLockscreenSavingSpaceNotStickyHun_minHeight() { 366 setGapHeight(0f) 367 // No divider height since we're testing one element where index = 0 368 369 val expandableView = createMockRow(rowHeight) 370 whenever(expandableView.getMinHeight(any())).thenReturn(5) 371 whenever(expandableView.intrinsicHeight).thenReturn(10) 372 373 val space = 374 sizeCalculator.getSpaceNeeded( 375 expandableView, 376 visibleIndex = 0, 377 previousView = null, 378 stack = stackLayout, 379 onLockscreen = true, 380 ) 381 assertThat(space.whenSavingSpace).isEqualTo(5) 382 } 383 384 @Test 385 fun getSpaceNeeded_notOnLockscreen_intrinsicHeight() { 386 setGapHeight(0f) 387 // No divider height since we're testing one element where index = 0 388 389 val expandableView = createMockRow(rowHeight) 390 whenever(expandableView.getMinHeight(any())).thenReturn(1) 391 392 val space = 393 sizeCalculator.getSpaceNeeded( 394 expandableView, 395 visibleIndex = 0, 396 previousView = null, 397 stack = stackLayout, 398 onLockscreen = false, 399 ) 400 assertThat(space.whenEnoughSpace).isEqualTo(rowHeight) 401 assertThat(space.whenSavingSpace).isEqualTo(rowHeight) 402 } 403 404 private fun computeMaxKeyguardNotifications( 405 rows: List<ExpandableView>, 406 spaceForNotifications: Float, 407 spaceForShelf: Float, 408 shelfHeight: Float = this.shelfHeight, 409 ): Int { 410 setupChildren(rows) 411 return sizeCalculator.computeMaxKeyguardNotifications( 412 stackLayout, 413 spaceForNotifications, 414 spaceForShelf, 415 shelfHeight, 416 ) 417 } 418 419 private fun setupChildren(children: List<ExpandableView>) { 420 whenever(stackLayout.getChildAt(any())).thenAnswer { invocation -> 421 val inx = invocation.getArgument<Int>(0) 422 return@thenAnswer children[inx] 423 } 424 whenever(stackLayout.childCount).thenReturn(children.size) 425 } 426 427 private fun createLockscreenRows(number: Int): List<ExpandableNotificationRow> = 428 (1..number).map { createMockRow() }.toList() 429 430 private fun createMockRow( 431 height: Float = rowHeight, 432 isSticky: Boolean = false, 433 isRemoved: Boolean = false, 434 visibility: Int = VISIBLE, 435 isPromotedOngoing: Boolean = false, 436 ): ExpandableNotificationRow { 437 val row = mock(ExpandableNotificationRow::class.java) 438 val entry = mock(NotificationEntry::class.java) 439 whenever(entry.isStickyAndNotDemoted).thenReturn(isSticky) 440 val entryAdapter = mock(EntryAdapter::class.java) 441 whenever(entryAdapter.canPeek()).thenReturn(isSticky) 442 whenever(row.entryAdapter).thenReturn(entryAdapter) 443 val sbn = mock(StatusBarNotification::class.java) 444 whenever(entry.sbn).thenReturn(sbn) 445 whenever(row.entry).thenReturn(entry) 446 whenever(row.isRemoved).thenReturn(isRemoved) 447 whenever(row.visibility).thenReturn(visibility) 448 whenever(row.getMinHeight(any())).thenReturn(height.toInt()) 449 whenever(row.intrinsicHeight).thenReturn(height.toInt()) 450 whenever(row.heightWithoutLockscreenConstraints).thenReturn(height.toInt()) 451 whenever(row.isPromotedOngoing).thenReturn(isPromotedOngoing) 452 return row 453 } 454 455 private fun setGapHeight(height: Float) { 456 whenever(stackLayout.calculateGapHeight(nullable(), nullable(), any())).thenReturn(height) 457 whenever(stackLayout.calculateGapHeight(nullable(), nullable(), /* visibleIndex= */ eq(0))) 458 .thenReturn(0f) 459 } 460 461 private fun px(@DimenRes id: Int): Float = 462 testableResources.resources.getDimensionPixelSize(id).toFloat() 463 } 464