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