1 /* 2 * Copyright (C) 2023 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.shade 18 19 import android.platform.test.annotations.DisableFlags 20 import android.platform.test.annotations.EnableFlags 21 import android.testing.AndroidTestingRunner 22 import android.testing.TestableLooper 23 import android.view.View 24 import android.view.ViewGroup 25 import android.view.WindowInsets 26 import android.view.WindowManagerPolicyConstants 27 import androidx.annotation.IdRes 28 import androidx.constraintlayout.widget.ConstraintLayout 29 import androidx.constraintlayout.widget.ConstraintSet 30 import androidx.test.filters.SmallTest 31 import com.android.systemui.Flags.FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX 32 import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT 33 import com.android.systemui.SysuiTestCase 34 import com.android.systemui.fragments.FragmentHostManager 35 import com.android.systemui.fragments.FragmentService 36 import com.android.systemui.navigationbar.NavigationModeController 37 import com.android.systemui.navigationbar.NavigationModeController.ModeChangedListener 38 import com.android.systemui.plugins.qs.QS 39 import com.android.systemui.recents.OverviewProxyService 40 import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener 41 import com.android.systemui.res.R 42 import com.android.systemui.shade.domain.interactor.ShadeInteractor 43 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController 44 import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController 45 import com.android.systemui.util.concurrency.FakeExecutor 46 import com.android.systemui.util.mockito.capture 47 import com.android.systemui.util.mockito.mock 48 import com.android.systemui.util.mockito.whenever 49 import com.android.systemui.util.time.FakeSystemClock 50 import com.google.common.truth.Truth.assertThat 51 import java.util.function.Consumer 52 import kotlinx.coroutines.flow.MutableStateFlow 53 import org.junit.Before 54 import org.junit.Test 55 import org.junit.runner.RunWith 56 import org.mockito.ArgumentCaptor 57 import org.mockito.Captor 58 import org.mockito.Mockito 59 import org.mockito.Mockito.RETURNS_DEEP_STUBS 60 import org.mockito.Mockito.any 61 import org.mockito.Mockito.anyInt 62 import org.mockito.Mockito.doNothing 63 import org.mockito.Mockito.eq 64 import org.mockito.Mockito.mock 65 import org.mockito.Mockito.never 66 import org.mockito.Mockito.reset 67 import org.mockito.Mockito.verify 68 import org.mockito.MockitoAnnotations 69 70 /** 71 * Uses Flags.KEYGUARD_STATUS_VIEW_MIGRATE_NSSL set to false. If all goes well, this set of tests 72 * will be deleted. 73 */ 74 @RunWith(AndroidTestingRunner::class) 75 @TestableLooper.RunWithLooper(setAsMainLooper = true) 76 @SmallTest 77 class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { 78 79 private val view = mock<NotificationsQuickSettingsContainer>() 80 private val navigationModeController = mock<NavigationModeController>() 81 private val overviewProxyService = mock<OverviewProxyService>() 82 private val shadeHeaderController = mock<ShadeHeaderController>() 83 private val shadeInteractor = mock<ShadeInteractor>() 84 private val fragmentService = mock<FragmentService>() 85 private val fragmentHostManager = mock<FragmentHostManager>() 86 private val notificationStackScrollLayoutController = 87 mock<NotificationStackScrollLayoutController>() 88 private val largeScreenHeaderHelper = mock<LargeScreenHeaderHelper>() 89 90 @Captor lateinit var navigationModeCaptor: ArgumentCaptor<ModeChangedListener> 91 @Captor lateinit var taskbarVisibilityCaptor: ArgumentCaptor<OverviewProxyListener> 92 @Captor lateinit var windowInsetsCallbackCaptor: ArgumentCaptor<Consumer<WindowInsets>> 93 @Captor lateinit var constraintSetCaptor: ArgumentCaptor<ConstraintSet> 94 @Captor lateinit var attachStateListenerCaptor: ArgumentCaptor<View.OnAttachStateChangeListener> 95 96 lateinit var underTest: NotificationsQSContainerController 97 98 private lateinit var navigationModeCallback: ModeChangedListener 99 private lateinit var taskbarVisibilityCallback: OverviewProxyListener 100 private lateinit var windowInsetsCallback: Consumer<WindowInsets> 101 private lateinit var fakeSystemClock: FakeSystemClock 102 private lateinit var delayableExecutor: FakeExecutor 103 104 @Before setupnull105 fun setup() { 106 MockitoAnnotations.initMocks(this) 107 fakeSystemClock = FakeSystemClock() 108 delayableExecutor = FakeExecutor(fakeSystemClock) 109 mContext.ensureTestableResources() 110 whenever(view.context).thenReturn(mContext) 111 whenever(view.resources).thenReturn(mContext.resources) 112 113 whenever(fragmentService.getFragmentHostManager(any())).thenReturn(fragmentHostManager) 114 whenever(shadeInteractor.isQsExpanded).thenReturn(MutableStateFlow(false)) 115 116 underTest = 117 NotificationsQSContainerController( 118 view, 119 navigationModeController, 120 overviewProxyService, 121 shadeHeaderController, 122 shadeInteractor, 123 fragmentService, 124 delayableExecutor, 125 notificationStackScrollLayoutController, 126 ResourcesSplitShadeStateController(), 127 largeScreenHeaderHelperLazy = { largeScreenHeaderHelper } 128 ) 129 130 overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, SCRIM_MARGIN) 131 overrideResource(R.dimen.notification_panel_margin_bottom, NOTIFICATIONS_MARGIN) 132 overrideResource(R.bool.config_use_split_notification_shade, false) 133 overrideResource(R.dimen.qs_footer_actions_bottom_padding, FOOTER_ACTIONS_PADDING) 134 overrideResource(R.dimen.qs_footer_action_inset, FOOTER_ACTIONS_INSET) 135 whenever(navigationModeController.addListener(navigationModeCaptor.capture())) 136 .thenReturn(GESTURES_NAVIGATION) 137 doNothing().`when`(overviewProxyService).addCallback(taskbarVisibilityCaptor.capture()) 138 doNothing().`when`(view).setInsetsChangedListener(windowInsetsCallbackCaptor.capture()) 139 doNothing().`when`(view).applyConstraints(constraintSetCaptor.capture()) 140 doNothing().`when`(view).addOnAttachStateChangeListener(attachStateListenerCaptor.capture()) 141 underTest.init() 142 attachStateListenerCaptor.value.onViewAttachedToWindow(view) 143 144 navigationModeCallback = navigationModeCaptor.value 145 taskbarVisibilityCallback = taskbarVisibilityCaptor.value 146 windowInsetsCallback = windowInsetsCallbackCaptor.value 147 148 Mockito.clearInvocations(view) 149 } 150 151 @Test testSmallScreen_updateResources_splitShadeHeightIsSetnull152 fun testSmallScreen_updateResources_splitShadeHeightIsSet() { 153 overrideResource(R.bool.config_use_large_screen_shade_header, false) 154 overrideResource(R.dimen.qs_header_height, 10) 155 overrideResource(R.dimen.large_screen_shade_header_height, 20) 156 157 // ensure the estimated height (would be 3 here) wouldn't impact this test case 158 overrideResource(R.dimen.large_screen_shade_header_min_height, 1) 159 overrideResource(R.dimen.new_qs_header_non_clickable_element_height, 1) 160 161 underTest.updateResources() 162 163 val captor = ArgumentCaptor.forClass(ConstraintSet::class.java) 164 verify(view).applyConstraints(capture(captor)) 165 assertThat(captor.value.getHeight(R.id.split_shade_status_bar)).isEqualTo(10) 166 } 167 168 @Test 169 @DisableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX) testLargeScreen_updateResources_refactorFlagOff_splitShadeHeightIsSetBasedOnResourcenull170 fun testLargeScreen_updateResources_refactorFlagOff_splitShadeHeightIsSetBasedOnResource() { 171 val headerResourceHeight = 20 172 val headerHelperHeight = 30 173 whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()) 174 .thenReturn(headerHelperHeight) 175 overrideResource(R.bool.config_use_large_screen_shade_header, true) 176 overrideResource(R.dimen.qs_header_height, 10) 177 overrideResource(R.dimen.large_screen_shade_header_height, headerResourceHeight) 178 179 // ensure the estimated height (would be 3 here) wouldn't impact this test case 180 overrideResource(R.dimen.large_screen_shade_header_min_height, 1) 181 overrideResource(R.dimen.new_qs_header_non_clickable_element_height, 1) 182 183 underTest.updateResources() 184 185 val captor = ArgumentCaptor.forClass(ConstraintSet::class.java) 186 verify(view).applyConstraints(capture(captor)) 187 assertThat(captor.value.getHeight(R.id.split_shade_status_bar)) 188 .isEqualTo(headerResourceHeight) 189 } 190 191 @Test 192 @EnableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX) testLargeScreen_updateResources_refactorFlagOn_splitShadeHeightIsSetBasedOnHelpernull193 fun testLargeScreen_updateResources_refactorFlagOn_splitShadeHeightIsSetBasedOnHelper() { 194 val headerResourceHeight = 20 195 val headerHelperHeight = 30 196 whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()) 197 .thenReturn(headerHelperHeight) 198 overrideResource(R.bool.config_use_large_screen_shade_header, true) 199 overrideResource(R.dimen.qs_header_height, 10) 200 overrideResource(R.dimen.large_screen_shade_header_height, headerResourceHeight) 201 202 // ensure the estimated height (would be 3 here) wouldn't impact this test case 203 overrideResource(R.dimen.large_screen_shade_header_min_height, 1) 204 overrideResource(R.dimen.new_qs_header_non_clickable_element_height, 1) 205 206 underTest.updateResources() 207 208 val captor = ArgumentCaptor.forClass(ConstraintSet::class.java) 209 verify(view).applyConstraints(capture(captor)) 210 assertThat(captor.value.getHeight(R.id.split_shade_status_bar)) 211 .isEqualTo(headerHelperHeight) 212 } 213 214 @Test testSmallScreen_estimatedHeightIsLargerThanDimenValue_shadeHeightIsSetToEstimatedHeightnull215 fun testSmallScreen_estimatedHeightIsLargerThanDimenValue_shadeHeightIsSetToEstimatedHeight() { 216 overrideResource(R.bool.config_use_large_screen_shade_header, false) 217 overrideResource(R.dimen.qs_header_height, 10) 218 overrideResource(R.dimen.large_screen_shade_header_height, 20) 219 220 // make the estimated height (would be 15 here) larger than qs_header_height 221 overrideResource(R.dimen.large_screen_shade_header_min_height, 5) 222 overrideResource(R.dimen.new_qs_header_non_clickable_element_height, 5) 223 224 underTest.updateResources() 225 226 val captor = ArgumentCaptor.forClass(ConstraintSet::class.java) 227 verify(view).applyConstraints(capture(captor)) 228 assertThat(captor.value.getHeight(R.id.split_shade_status_bar)).isEqualTo(15) 229 } 230 231 @Test testTaskbarVisibleInSplitShadenull232 fun testTaskbarVisibleInSplitShade() { 233 enableSplitShade() 234 235 given( 236 taskbarVisible = true, 237 navigationMode = GESTURES_NAVIGATION, 238 insets = windowInsets().withStableBottom() 239 ) 240 then( 241 expectedContainerPadding = 0, // taskbar should disappear when shade is expanded 242 expectedNotificationsMargin = NOTIFICATIONS_MARGIN, 243 expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET 244 ) 245 246 given( 247 taskbarVisible = true, 248 navigationMode = BUTTONS_NAVIGATION, 249 insets = windowInsets().withStableBottom() 250 ) 251 then( 252 expectedContainerPadding = STABLE_INSET_BOTTOM, 253 expectedNotificationsMargin = NOTIFICATIONS_MARGIN, 254 expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET 255 ) 256 } 257 258 @Test testTaskbarNotVisibleInSplitShadenull259 fun testTaskbarNotVisibleInSplitShade() { 260 // when taskbar is not visible, it means we're on the home screen 261 enableSplitShade() 262 263 given( 264 taskbarVisible = false, 265 navigationMode = GESTURES_NAVIGATION, 266 insets = windowInsets().withStableBottom() 267 ) 268 then( 269 expectedContainerPadding = 0, 270 expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET 271 ) 272 273 given( 274 taskbarVisible = false, 275 navigationMode = BUTTONS_NAVIGATION, 276 insets = windowInsets().withStableBottom() 277 ) 278 then( 279 expectedContainerPadding = 0, // qs goes full height as it's not obscuring nav buttons 280 expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN, 281 expectedQsPadding = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET 282 ) 283 } 284 285 @Test testTaskbarNotVisibleInSplitShadeWithCutoutnull286 fun testTaskbarNotVisibleInSplitShadeWithCutout() { 287 enableSplitShade() 288 289 given( 290 taskbarVisible = false, 291 navigationMode = GESTURES_NAVIGATION, 292 insets = windowInsets().withCutout() 293 ) 294 then( 295 expectedContainerPadding = CUTOUT_HEIGHT, 296 expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET 297 ) 298 299 given( 300 taskbarVisible = false, 301 navigationMode = BUTTONS_NAVIGATION, 302 insets = windowInsets().withCutout().withStableBottom() 303 ) 304 then( 305 expectedContainerPadding = 0, 306 expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN, 307 expectedQsPadding = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET 308 ) 309 } 310 311 @Test testTaskbarVisibleInSinglePaneShadenull312 fun testTaskbarVisibleInSinglePaneShade() { 313 disableSplitShade() 314 315 given( 316 taskbarVisible = true, 317 navigationMode = GESTURES_NAVIGATION, 318 insets = windowInsets().withStableBottom() 319 ) 320 then(expectedContainerPadding = 0, expectedQsPadding = STABLE_INSET_BOTTOM) 321 322 given( 323 taskbarVisible = true, 324 navigationMode = BUTTONS_NAVIGATION, 325 insets = windowInsets().withStableBottom() 326 ) 327 then( 328 expectedContainerPadding = STABLE_INSET_BOTTOM, 329 expectedQsPadding = STABLE_INSET_BOTTOM 330 ) 331 } 332 333 @Test testTaskbarNotVisibleInSinglePaneShadenull334 fun testTaskbarNotVisibleInSinglePaneShade() { 335 disableSplitShade() 336 337 given(taskbarVisible = false, navigationMode = GESTURES_NAVIGATION, insets = emptyInsets()) 338 then(expectedContainerPadding = 0) 339 340 given( 341 taskbarVisible = false, 342 navigationMode = GESTURES_NAVIGATION, 343 insets = windowInsets().withCutout().withStableBottom() 344 ) 345 then(expectedContainerPadding = CUTOUT_HEIGHT, expectedQsPadding = STABLE_INSET_BOTTOM) 346 347 given( 348 taskbarVisible = false, 349 navigationMode = BUTTONS_NAVIGATION, 350 insets = windowInsets().withStableBottom() 351 ) 352 then( 353 expectedContainerPadding = 0, 354 expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN, 355 expectedQsPadding = STABLE_INSET_BOTTOM 356 ) 357 } 358 359 @Test testDetailShowingInSinglePaneShadenull360 fun testDetailShowingInSinglePaneShade() { 361 disableSplitShade() 362 underTest.setDetailShowing(true) 363 364 // always sets spacings to 0 365 given( 366 taskbarVisible = false, 367 navigationMode = GESTURES_NAVIGATION, 368 insets = windowInsets().withStableBottom() 369 ) 370 then(expectedContainerPadding = 0, expectedNotificationsMargin = 0) 371 372 given(taskbarVisible = false, navigationMode = BUTTONS_NAVIGATION, insets = emptyInsets()) 373 then(expectedContainerPadding = 0, expectedNotificationsMargin = 0) 374 } 375 376 @Test testDetailShowingInSplitShadenull377 fun testDetailShowingInSplitShade() { 378 enableSplitShade() 379 underTest.setDetailShowing(true) 380 381 given( 382 taskbarVisible = false, 383 navigationMode = GESTURES_NAVIGATION, 384 insets = windowInsets().withStableBottom() 385 ) 386 then(expectedContainerPadding = 0) 387 388 // should not influence spacing 389 given(taskbarVisible = false, navigationMode = BUTTONS_NAVIGATION, insets = emptyInsets()) 390 then(expectedContainerPadding = 0) 391 } 392 393 @Test testNotificationsMarginBottomIsUpdatednull394 fun testNotificationsMarginBottomIsUpdated() { 395 Mockito.clearInvocations(view) 396 enableSplitShade() 397 verify(view).setNotificationsMarginBottom(NOTIFICATIONS_MARGIN) 398 399 overrideResource(R.dimen.notification_panel_margin_bottom, 100) 400 disableSplitShade() 401 verify(view).setNotificationsMarginBottom(100) 402 } 403 404 @Test 405 @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) testSplitShadeLayout_isAlignedToGuidelinenull406 fun testSplitShadeLayout_isAlignedToGuideline() { 407 enableSplitShade() 408 underTest.updateResources() 409 assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd).isEqualTo(R.id.qs_edge_guideline) 410 assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).startToStart) 411 .isEqualTo(R.id.qs_edge_guideline) 412 } 413 414 @Test 415 @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) testSinglePaneLayout_childrenHaveEqualMarginsnull416 fun testSinglePaneLayout_childrenHaveEqualMargins() { 417 disableSplitShade() 418 underTest.updateResources() 419 val qsStartMargin = getConstraintSetLayout(R.id.qs_frame).startMargin 420 val qsEndMargin = getConstraintSetLayout(R.id.qs_frame).endMargin 421 val notifStartMargin = getConstraintSetLayout(R.id.notification_stack_scroller).startMargin 422 val notifEndMargin = getConstraintSetLayout(R.id.notification_stack_scroller).endMargin 423 assertThat( 424 qsStartMargin == qsEndMargin && 425 notifStartMargin == notifEndMargin && 426 qsStartMargin == notifStartMargin 427 ) 428 .isTrue() 429 } 430 431 @Test 432 @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) testSplitShadeLayout_childrenHaveInsideMarginsOfZeronull433 fun testSplitShadeLayout_childrenHaveInsideMarginsOfZero() { 434 enableSplitShade() 435 underTest.updateResources() 436 assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin).isEqualTo(0) 437 assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).startMargin) 438 .isEqualTo(0) 439 } 440 441 @Test testSplitShadeLayout_qsFrameHasHorizontalMarginsOfZeronull442 fun testSplitShadeLayout_qsFrameHasHorizontalMarginsOfZero() { 443 enableSplitShade() 444 underTest.updateResources() 445 assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin).isEqualTo(0) 446 assertThat(getConstraintSetLayout(R.id.qs_frame).startMargin).isEqualTo(0) 447 } 448 449 @Test 450 @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX) testLargeScreenLayout_refactorFlagOff_qsAndNotifsTopMarginIsOfHeaderHeightResourcenull451 fun testLargeScreenLayout_refactorFlagOff_qsAndNotifsTopMarginIsOfHeaderHeightResource() { 452 setLargeScreen() 453 val largeScreenHeaderResourceHeight = 100 454 val largeScreenHeaderHelperHeight = 200 455 whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()) 456 .thenReturn(largeScreenHeaderHelperHeight) 457 overrideResource(R.dimen.large_screen_shade_header_height, largeScreenHeaderResourceHeight) 458 459 // ensure the estimated height (would be 30 here) wouldn't impact this test case 460 overrideResource(R.dimen.large_screen_shade_header_min_height, 10) 461 overrideResource(R.dimen.new_qs_header_non_clickable_element_height, 10) 462 463 underTest.updateResources() 464 465 assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin) 466 .isEqualTo(largeScreenHeaderResourceHeight) 467 assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).topMargin) 468 .isEqualTo(largeScreenHeaderResourceHeight) 469 } 470 471 @Test 472 @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) 473 @EnableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX) testLargeScreenLayout_refactorFlagOn_qsAndNotifsTopMarginIsOfHeaderHeightHelpernull474 fun testLargeScreenLayout_refactorFlagOn_qsAndNotifsTopMarginIsOfHeaderHeightHelper() { 475 setLargeScreen() 476 val largeScreenHeaderResourceHeight = 100 477 val largeScreenHeaderHelperHeight = 200 478 whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()) 479 .thenReturn(largeScreenHeaderHelperHeight) 480 overrideResource(R.dimen.large_screen_shade_header_height, largeScreenHeaderResourceHeight) 481 482 // ensure the estimated height (would be 30 here) wouldn't impact this test case 483 overrideResource(R.dimen.large_screen_shade_header_min_height, 10) 484 overrideResource(R.dimen.new_qs_header_non_clickable_element_height, 10) 485 486 underTest.updateResources() 487 488 assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin) 489 .isEqualTo(largeScreenHeaderHelperHeight) 490 assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).topMargin) 491 .isEqualTo(largeScreenHeaderHelperHeight) 492 } 493 494 @Test 495 @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) testSmallScreenLayout_qsAndNotifsTopMarginIsZeronull496 fun testSmallScreenLayout_qsAndNotifsTopMarginIsZero() { 497 setSmallScreen() 498 underTest.updateResources() 499 assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin).isEqualTo(0) 500 assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).topMargin).isEqualTo(0) 501 } 502 503 @Test testSinglePaneShadeLayout_qsFrameHasHorizontalMarginsSetToCorrectValuenull504 fun testSinglePaneShadeLayout_qsFrameHasHorizontalMarginsSetToCorrectValue() { 505 disableSplitShade() 506 underTest.updateResources() 507 val notificationPanelMarginHorizontal = 508 mContext.resources.getDimensionPixelSize(R.dimen.notification_panel_margin_horizontal) 509 assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin) 510 .isEqualTo(notificationPanelMarginHorizontal) 511 assertThat(getConstraintSetLayout(R.id.qs_frame).startMargin) 512 .isEqualTo(notificationPanelMarginHorizontal) 513 } 514 515 @Test 516 @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) testSinglePaneShadeLayout_isAlignedToParentnull517 fun testSinglePaneShadeLayout_isAlignedToParent() { 518 disableSplitShade() 519 underTest.updateResources() 520 assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd) 521 .isEqualTo(ConstraintSet.PARENT_ID) 522 assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).startToStart) 523 .isEqualTo(ConstraintSet.PARENT_ID) 524 } 525 526 @Test testAllChildrenOfNotificationContainer_haveIdsnull527 fun testAllChildrenOfNotificationContainer_haveIds() { 528 // set dimen to 0 to avoid triggering updating bottom spacing 529 overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, 0) 530 val container = NotificationsQuickSettingsContainer(mContext, null) 531 container.removeAllViews() 532 container.addView(newViewWithId(1)) 533 container.addView(newViewWithId(View.NO_ID)) 534 val controller = 535 NotificationsQSContainerController( 536 container, 537 navigationModeController, 538 overviewProxyService, 539 shadeHeaderController, 540 shadeInteractor, 541 fragmentService, 542 delayableExecutor, 543 notificationStackScrollLayoutController, 544 ResourcesSplitShadeStateController(), 545 largeScreenHeaderHelperLazy = { largeScreenHeaderHelper } 546 ) 547 controller.updateConstraints() 548 549 assertThat(container.getChildAt(0).id).isEqualTo(1) 550 assertThat(container.getChildAt(1).id).isNotEqualTo(View.NO_ID) 551 } 552 553 @Test testWindowInsetDebouncenull554 fun testWindowInsetDebounce() { 555 disableSplitShade() 556 557 given( 558 taskbarVisible = false, 559 navigationMode = GESTURES_NAVIGATION, 560 insets = emptyInsets(), 561 applyImmediately = false 562 ) 563 fakeSystemClock.advanceTime(INSET_DEBOUNCE_MILLIS / 2) 564 windowInsetsCallback.accept(windowInsets().withStableBottom()) 565 566 delayableExecutor.advanceClockToLast() 567 delayableExecutor.runAllReady() 568 569 verify(view, never()).setQSContainerPaddingBottom(0) 570 verify(view).setQSContainerPaddingBottom(STABLE_INSET_BOTTOM) 571 } 572 573 @Test testStartCustomizingWithDurationnull574 fun testStartCustomizingWithDuration() { 575 underTest.setCustomizerShowing(true, 100L) 576 verify(shadeHeaderController).startCustomizingAnimation(true, 100L) 577 } 578 579 @Test testEndCustomizingWithDurationnull580 fun testEndCustomizingWithDuration() { 581 underTest.setCustomizerShowing(true, 0L) // Only tracks changes 582 reset(shadeHeaderController) 583 584 underTest.setCustomizerShowing(false, 100L) 585 verify(shadeHeaderController).startCustomizingAnimation(false, 100L) 586 } 587 588 @Test testTagListenerAddednull589 fun testTagListenerAdded() { 590 verify(fragmentHostManager).addTagListener(eq(QS.TAG), eq(view)) 591 } 592 593 @Test testTagListenerRemovednull594 fun testTagListenerRemoved() { 595 attachStateListenerCaptor.value.onViewDetachedFromWindow(view) 596 verify(fragmentHostManager).removeTagListener(eq(QS.TAG), eq(view)) 597 } 598 disableSplitShadenull599 private fun disableSplitShade() { 600 setSplitShadeEnabled(false) 601 } 602 enableSplitShadenull603 private fun enableSplitShade() { 604 setSplitShadeEnabled(true) 605 } 606 setSplitShadeEnablednull607 private fun setSplitShadeEnabled(enabled: Boolean) { 608 overrideResource(R.bool.config_use_split_notification_shade, enabled) 609 underTest.updateResources() 610 } 611 setSmallScreennull612 private fun setSmallScreen() { 613 setLargeScreenEnabled(false) 614 } 615 setLargeScreennull616 private fun setLargeScreen() { 617 setLargeScreenEnabled(true) 618 } 619 setLargeScreenEnablednull620 private fun setLargeScreenEnabled(enabled: Boolean) { 621 overrideResource(R.bool.config_use_large_screen_shade_header, enabled) 622 } 623 givennull624 private fun given( 625 taskbarVisible: Boolean, 626 navigationMode: Int, 627 insets: WindowInsets, 628 applyImmediately: Boolean = true 629 ) { 630 Mockito.clearInvocations(view) 631 taskbarVisibilityCallback.onTaskbarStatusUpdated(taskbarVisible, false) 632 navigationModeCallback.onNavigationModeChanged(navigationMode) 633 windowInsetsCallback.accept(insets) 634 if (applyImmediately) { 635 delayableExecutor.advanceClockToLast() 636 delayableExecutor.runAllReady() 637 } 638 } 639 thennull640 fun then( 641 expectedContainerPadding: Int, 642 expectedNotificationsMargin: Int = NOTIFICATIONS_MARGIN, 643 expectedQsPadding: Int = 0 644 ) { 645 verify(view).setPadding(anyInt(), anyInt(), anyInt(), eq(expectedContainerPadding)) 646 verify(view).setNotificationsMarginBottom(expectedNotificationsMargin) 647 verify(view).setQSContainerPaddingBottom(expectedQsPadding) 648 Mockito.clearInvocations(view) 649 } 650 windowInsetsnull651 private fun windowInsets() = mock(WindowInsets::class.java, RETURNS_DEEP_STUBS) 652 653 private fun emptyInsets() = mock(WindowInsets::class.java) 654 655 private fun WindowInsets.withCutout(): WindowInsets { 656 whenever(checkNotNull(displayCutout).safeInsetBottom).thenReturn(CUTOUT_HEIGHT) 657 return this 658 } 659 withStableBottomnull660 private fun WindowInsets.withStableBottom(): WindowInsets { 661 whenever(stableInsetBottom).thenReturn(STABLE_INSET_BOTTOM) 662 return this 663 } 664 getConstraintSetLayoutnull665 private fun getConstraintSetLayout(@IdRes id: Int): ConstraintSet.Layout { 666 return constraintSetCaptor.value.getConstraint(id).layout 667 } 668 newViewWithIdnull669 private fun newViewWithId(id: Int): View { 670 val view = View(mContext) 671 view.id = id 672 val layoutParams = 673 ConstraintLayout.LayoutParams( 674 ViewGroup.LayoutParams.WRAP_CONTENT, 675 ViewGroup.LayoutParams.WRAP_CONTENT 676 ) 677 // required as cloning ConstraintSet fails if view doesn't have layout params 678 view.layoutParams = layoutParams 679 return view 680 } 681 682 companion object { 683 const val STABLE_INSET_BOTTOM = 100 684 const val CUTOUT_HEIGHT = 50 685 const val GESTURES_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL 686 const val BUTTONS_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON 687 const val NOTIFICATIONS_MARGIN = 50 688 const val SCRIM_MARGIN = 10 689 const val FOOTER_ACTIONS_INSET = 2 690 const val FOOTER_ACTIONS_PADDING = 2 691 const val FOOTER_ACTIONS_OFFSET = FOOTER_ACTIONS_INSET + FOOTER_ACTIONS_PADDING 692 const val QS_PADDING_OFFSET = SCRIM_MARGIN + FOOTER_ACTIONS_OFFSET 693 } 694 } 695