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