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.EnableFlags 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 @RunWith(AndroidTestingRunner::class) 69 @TestableLooper.RunWithLooper(setAsMainLooper = true) 70 @SmallTest 71 @EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) 72 class NotificationsQSContainerControllerTest : SysuiTestCase() { 73 74 private val view = mock<NotificationsQuickSettingsContainer>() 75 private val navigationModeController = mock<NavigationModeController>() 76 private val overviewProxyService = mock<OverviewProxyService>() 77 private val shadeHeaderController = mock<ShadeHeaderController>() 78 private val shadeInteractor = mock<ShadeInteractor>() 79 private val fragmentService = mock<FragmentService>() 80 private val fragmentHostManager = mock<FragmentHostManager>() 81 private val notificationStackScrollLayoutController = 82 mock<NotificationStackScrollLayoutController>() 83 private val largeScreenHeaderHelper = mock<LargeScreenHeaderHelper>() 84 85 @Captor lateinit var navigationModeCaptor: ArgumentCaptor<ModeChangedListener> 86 @Captor lateinit var taskbarVisibilityCaptor: ArgumentCaptor<OverviewProxyListener> 87 @Captor lateinit var windowInsetsCallbackCaptor: ArgumentCaptor<Consumer<WindowInsets>> 88 @Captor lateinit var constraintSetCaptor: ArgumentCaptor<ConstraintSet> 89 @Captor lateinit var attachStateListenerCaptor: ArgumentCaptor<View.OnAttachStateChangeListener> 90 91 lateinit var underTest: NotificationsQSContainerController 92 93 private lateinit var navigationModeCallback: ModeChangedListener 94 private lateinit var taskbarVisibilityCallback: OverviewProxyListener 95 private lateinit var windowInsetsCallback: Consumer<WindowInsets> 96 private lateinit var fakeSystemClock: FakeSystemClock 97 private lateinit var delayableExecutor: FakeExecutor 98 99 @Before setupnull100 fun setup() { 101 MockitoAnnotations.initMocks(this) 102 fakeSystemClock = FakeSystemClock() 103 delayableExecutor = FakeExecutor(fakeSystemClock) 104 mContext.ensureTestableResources() 105 whenever(view.context).thenReturn(mContext) 106 whenever(view.resources).thenReturn(mContext.resources) 107 108 whenever(fragmentService.getFragmentHostManager(any())).thenReturn(fragmentHostManager) 109 110 whenever(shadeInteractor.isQsExpanded).thenReturn(MutableStateFlow(false)) 111 112 underTest = 113 NotificationsQSContainerController( 114 view, 115 navigationModeController, 116 overviewProxyService, 117 shadeHeaderController, 118 shadeInteractor, 119 fragmentService, 120 delayableExecutor, 121 notificationStackScrollLayoutController, 122 ResourcesSplitShadeStateController(), 123 largeScreenHeaderHelperLazy = { largeScreenHeaderHelper } 124 ) 125 126 overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, SCRIM_MARGIN) 127 overrideResource(R.dimen.notification_panel_margin_bottom, NOTIFICATIONS_MARGIN) 128 overrideResource(R.bool.config_use_split_notification_shade, false) 129 overrideResource(R.dimen.qs_footer_actions_bottom_padding, FOOTER_ACTIONS_PADDING) 130 overrideResource(R.dimen.qs_footer_action_inset, FOOTER_ACTIONS_INSET) 131 whenever(navigationModeController.addListener(navigationModeCaptor.capture())) 132 .thenReturn(GESTURES_NAVIGATION) 133 doNothing().`when`(overviewProxyService).addCallback(taskbarVisibilityCaptor.capture()) 134 doNothing().`when`(view).setInsetsChangedListener(windowInsetsCallbackCaptor.capture()) 135 doNothing().`when`(view).applyConstraints(constraintSetCaptor.capture()) 136 doNothing().`when`(view).addOnAttachStateChangeListener(attachStateListenerCaptor.capture()) 137 underTest.init() 138 attachStateListenerCaptor.value.onViewAttachedToWindow(view) 139 140 navigationModeCallback = navigationModeCaptor.value 141 taskbarVisibilityCallback = taskbarVisibilityCaptor.value 142 windowInsetsCallback = windowInsetsCallbackCaptor.value 143 144 Mockito.clearInvocations(view) 145 } 146 147 @Test testSmallScreen_updateResources_splitShadeHeightIsSetnull148 fun testSmallScreen_updateResources_splitShadeHeightIsSet() { 149 overrideResource(R.bool.config_use_large_screen_shade_header, false) 150 overrideResource(R.dimen.qs_header_height, 10) 151 overrideResource(R.dimen.large_screen_shade_header_height, 20) 152 153 // ensure the estimated height (would be 3 here) wouldn't impact this test case 154 overrideResource(R.dimen.large_screen_shade_header_min_height, 1) 155 overrideResource(R.dimen.new_qs_header_non_clickable_element_height, 1) 156 157 underTest.updateResources() 158 159 val captor = ArgumentCaptor.forClass(ConstraintSet::class.java) 160 verify(view).applyConstraints(capture(captor)) 161 assertThat(captor.value.getHeight(R.id.split_shade_status_bar)).isEqualTo(10) 162 } 163 164 @Test testLargeScreen_updateResources_splitShadeHeightIsSet_basedOnHelpernull165 fun testLargeScreen_updateResources_splitShadeHeightIsSet_basedOnHelper() { 166 val helperHeight = 30 167 val resourceHeight = 20 168 whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(helperHeight) 169 overrideResource(R.bool.config_use_large_screen_shade_header, true) 170 overrideResource(R.dimen.qs_header_height, 10) 171 overrideResource(R.dimen.large_screen_shade_header_height, resourceHeight) 172 173 // ensure the estimated height (would be 3 here) wouldn't impact this test case 174 overrideResource(R.dimen.large_screen_shade_header_min_height, 1) 175 overrideResource(R.dimen.new_qs_header_non_clickable_element_height, 1) 176 177 underTest.updateResources() 178 179 val captor = ArgumentCaptor.forClass(ConstraintSet::class.java) 180 verify(view).applyConstraints(capture(captor)) 181 assertThat(captor.value.getHeight(R.id.split_shade_status_bar)).isEqualTo(helperHeight) 182 } 183 184 @Test testSmallScreen_estimatedHeightIsLargerThanDimenValue_shadeHeightIsSetToEstimatedHeightnull185 fun testSmallScreen_estimatedHeightIsLargerThanDimenValue_shadeHeightIsSetToEstimatedHeight() { 186 overrideResource(R.bool.config_use_large_screen_shade_header, false) 187 overrideResource(R.dimen.qs_header_height, 10) 188 overrideResource(R.dimen.large_screen_shade_header_height, 20) 189 190 // make the estimated height (would be 15 here) larger than qs_header_height 191 overrideResource(R.dimen.large_screen_shade_header_min_height, 5) 192 overrideResource(R.dimen.new_qs_header_non_clickable_element_height, 5) 193 194 underTest.updateResources() 195 196 val captor = ArgumentCaptor.forClass(ConstraintSet::class.java) 197 verify(view).applyConstraints(capture(captor)) 198 assertThat(captor.value.getHeight(R.id.split_shade_status_bar)).isEqualTo(15) 199 } 200 201 @Test testTaskbarVisibleInSplitShadenull202 fun testTaskbarVisibleInSplitShade() { 203 enableSplitShade() 204 205 given( 206 taskbarVisible = true, 207 navigationMode = GESTURES_NAVIGATION, 208 insets = windowInsets().withStableBottom() 209 ) 210 then( 211 expectedContainerPadding = 0, // taskbar should disappear when shade is expanded 212 expectedNotificationsMargin = NOTIFICATIONS_MARGIN, 213 expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET 214 ) 215 216 given( 217 taskbarVisible = true, 218 navigationMode = BUTTONS_NAVIGATION, 219 insets = windowInsets().withStableBottom() 220 ) 221 then( 222 expectedContainerPadding = STABLE_INSET_BOTTOM, 223 expectedNotificationsMargin = NOTIFICATIONS_MARGIN, 224 expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET 225 ) 226 } 227 228 @Test testTaskbarNotVisibleInSplitShadenull229 fun testTaskbarNotVisibleInSplitShade() { 230 // when taskbar is not visible, it means we're on the home screen 231 enableSplitShade() 232 233 given( 234 taskbarVisible = false, 235 navigationMode = GESTURES_NAVIGATION, 236 insets = windowInsets().withStableBottom() 237 ) 238 then( 239 expectedContainerPadding = 0, 240 expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET 241 ) 242 243 given( 244 taskbarVisible = false, 245 navigationMode = BUTTONS_NAVIGATION, 246 insets = windowInsets().withStableBottom() 247 ) 248 then( 249 expectedContainerPadding = 0, // qs goes full height as it's not obscuring nav buttons 250 expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN, 251 expectedQsPadding = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET 252 ) 253 } 254 255 @Test testTaskbarNotVisibleInSplitShadeWithCutoutnull256 fun testTaskbarNotVisibleInSplitShadeWithCutout() { 257 enableSplitShade() 258 259 given( 260 taskbarVisible = false, 261 navigationMode = GESTURES_NAVIGATION, 262 insets = windowInsets().withCutout() 263 ) 264 then( 265 expectedContainerPadding = CUTOUT_HEIGHT, 266 expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET 267 ) 268 269 given( 270 taskbarVisible = false, 271 navigationMode = BUTTONS_NAVIGATION, 272 insets = windowInsets().withCutout().withStableBottom() 273 ) 274 then( 275 expectedContainerPadding = 0, 276 expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN, 277 expectedQsPadding = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET 278 ) 279 } 280 281 @Test testTaskbarVisibleInSinglePaneShadenull282 fun testTaskbarVisibleInSinglePaneShade() { 283 disableSplitShade() 284 285 given( 286 taskbarVisible = true, 287 navigationMode = GESTURES_NAVIGATION, 288 insets = windowInsets().withStableBottom() 289 ) 290 then(expectedContainerPadding = 0, expectedQsPadding = STABLE_INSET_BOTTOM) 291 292 given( 293 taskbarVisible = true, 294 navigationMode = BUTTONS_NAVIGATION, 295 insets = windowInsets().withStableBottom() 296 ) 297 then( 298 expectedContainerPadding = STABLE_INSET_BOTTOM, 299 expectedQsPadding = STABLE_INSET_BOTTOM 300 ) 301 } 302 303 @Test testTaskbarNotVisibleInSinglePaneShadenull304 fun testTaskbarNotVisibleInSinglePaneShade() { 305 disableSplitShade() 306 307 given(taskbarVisible = false, navigationMode = GESTURES_NAVIGATION, insets = emptyInsets()) 308 then(expectedContainerPadding = 0) 309 310 given( 311 taskbarVisible = false, 312 navigationMode = GESTURES_NAVIGATION, 313 insets = windowInsets().withCutout().withStableBottom() 314 ) 315 then(expectedContainerPadding = CUTOUT_HEIGHT, expectedQsPadding = STABLE_INSET_BOTTOM) 316 317 given( 318 taskbarVisible = false, 319 navigationMode = BUTTONS_NAVIGATION, 320 insets = windowInsets().withStableBottom() 321 ) 322 then( 323 expectedContainerPadding = 0, 324 expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN, 325 expectedQsPadding = STABLE_INSET_BOTTOM 326 ) 327 } 328 329 @Test testDetailShowingInSinglePaneShadenull330 fun testDetailShowingInSinglePaneShade() { 331 disableSplitShade() 332 underTest.setDetailShowing(true) 333 334 // always sets spacings to 0 335 given( 336 taskbarVisible = false, 337 navigationMode = GESTURES_NAVIGATION, 338 insets = windowInsets().withStableBottom() 339 ) 340 then(expectedContainerPadding = 0, expectedNotificationsMargin = 0) 341 342 given(taskbarVisible = false, navigationMode = BUTTONS_NAVIGATION, insets = emptyInsets()) 343 then(expectedContainerPadding = 0, expectedNotificationsMargin = 0) 344 } 345 346 @Test testDetailShowingInSplitShadenull347 fun testDetailShowingInSplitShade() { 348 enableSplitShade() 349 underTest.setDetailShowing(true) 350 351 given( 352 taskbarVisible = false, 353 navigationMode = GESTURES_NAVIGATION, 354 insets = windowInsets().withStableBottom() 355 ) 356 then(expectedContainerPadding = 0) 357 358 // should not influence spacing 359 given(taskbarVisible = false, navigationMode = BUTTONS_NAVIGATION, insets = emptyInsets()) 360 then(expectedContainerPadding = 0) 361 } 362 363 @Test testNotificationsMarginBottomIsUpdatednull364 fun testNotificationsMarginBottomIsUpdated() { 365 Mockito.clearInvocations(view) 366 enableSplitShade() 367 verify(view).setNotificationsMarginBottom(NOTIFICATIONS_MARGIN) 368 369 overrideResource(R.dimen.notification_panel_margin_bottom, 100) 370 disableSplitShade() 371 verify(view).setNotificationsMarginBottom(100) 372 } 373 374 @Test testSplitShadeLayout_isAlignedToGuidelinenull375 fun testSplitShadeLayout_isAlignedToGuideline() { 376 enableSplitShade() 377 underTest.updateResources() 378 assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd).isEqualTo(R.id.qs_edge_guideline) 379 } 380 381 @Test testSinglePaneLayout_childrenHaveEqualMarginsnull382 fun testSinglePaneLayout_childrenHaveEqualMargins() { 383 disableSplitShade() 384 underTest.updateResources() 385 val qsStartMargin = getConstraintSetLayout(R.id.qs_frame).startMargin 386 val qsEndMargin = getConstraintSetLayout(R.id.qs_frame).endMargin 387 assertThat(qsStartMargin == qsEndMargin).isTrue() 388 } 389 390 @Test testSplitShadeLayout_childrenHaveInsideMarginsOfZeronull391 fun testSplitShadeLayout_childrenHaveInsideMarginsOfZero() { 392 enableSplitShade() 393 underTest.updateResources() 394 assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin).isEqualTo(0) 395 } 396 397 @Test testSplitShadeLayout_qsFrameHasHorizontalMarginsOfZeronull398 fun testSplitShadeLayout_qsFrameHasHorizontalMarginsOfZero() { 399 enableSplitShade() 400 underTest.updateResources() 401 assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin).isEqualTo(0) 402 assertThat(getConstraintSetLayout(R.id.qs_frame).startMargin).isEqualTo(0) 403 } 404 405 @Test testLargeScreenLayout_qsAndNotifsTopMarginIsOfHeaderHelperHeightnull406 fun testLargeScreenLayout_qsAndNotifsTopMarginIsOfHeaderHelperHeight() { 407 setLargeScreen() 408 val largeScreenHeaderHelperHeight = 200 409 val largeScreenHeaderResourceHeight = 100 410 whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()) 411 .thenReturn(largeScreenHeaderHelperHeight) 412 overrideResource(R.dimen.large_screen_shade_header_height, largeScreenHeaderResourceHeight) 413 414 // ensure the estimated height (would be 30 here) wouldn't impact this test case 415 overrideResource(R.dimen.large_screen_shade_header_min_height, 10) 416 overrideResource(R.dimen.new_qs_header_non_clickable_element_height, 10) 417 418 underTest.updateResources() 419 420 assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin) 421 .isEqualTo(largeScreenHeaderHelperHeight) 422 } 423 424 @Test testSmallScreenLayout_qsAndNotifsTopMarginIsZeronull425 fun testSmallScreenLayout_qsAndNotifsTopMarginIsZero() { 426 setSmallScreen() 427 underTest.updateResources() 428 assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin).isEqualTo(0) 429 } 430 431 @Test testSinglePaneShadeLayout_qsFrameHasHorizontalMarginsSetToCorrectValuenull432 fun testSinglePaneShadeLayout_qsFrameHasHorizontalMarginsSetToCorrectValue() { 433 disableSplitShade() 434 underTest.updateResources() 435 val notificationPanelMarginHorizontal = 436 mContext.resources.getDimensionPixelSize(R.dimen.notification_panel_margin_horizontal) 437 assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin) 438 .isEqualTo(notificationPanelMarginHorizontal) 439 assertThat(getConstraintSetLayout(R.id.qs_frame).startMargin) 440 .isEqualTo(notificationPanelMarginHorizontal) 441 } 442 443 @Test testSinglePaneShadeLayout_isAlignedToParentnull444 fun testSinglePaneShadeLayout_isAlignedToParent() { 445 disableSplitShade() 446 underTest.updateResources() 447 assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd) 448 .isEqualTo(ConstraintSet.PARENT_ID) 449 } 450 451 @Test testAllChildrenOfNotificationContainer_haveIdsnull452 fun testAllChildrenOfNotificationContainer_haveIds() { 453 // set dimen to 0 to avoid triggering updating bottom spacing 454 overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, 0) 455 val container = NotificationsQuickSettingsContainer(mContext, null) 456 container.removeAllViews() 457 container.addView(newViewWithId(1)) 458 container.addView(newViewWithId(View.NO_ID)) 459 val controller = 460 NotificationsQSContainerController( 461 container, 462 navigationModeController, 463 overviewProxyService, 464 shadeHeaderController, 465 shadeInteractor, 466 fragmentService, 467 delayableExecutor, 468 notificationStackScrollLayoutController, 469 ResourcesSplitShadeStateController(), 470 largeScreenHeaderHelperLazy = { largeScreenHeaderHelper } 471 ) 472 controller.updateConstraints() 473 474 assertThat(container.getChildAt(0).id).isEqualTo(1) 475 assertThat(container.getChildAt(1).id).isNotEqualTo(View.NO_ID) 476 } 477 478 @Test testWindowInsetDebouncenull479 fun testWindowInsetDebounce() { 480 disableSplitShade() 481 482 given( 483 taskbarVisible = false, 484 navigationMode = GESTURES_NAVIGATION, 485 insets = emptyInsets(), 486 applyImmediately = false 487 ) 488 fakeSystemClock.advanceTime(INSET_DEBOUNCE_MILLIS / 2) 489 windowInsetsCallback.accept(windowInsets().withStableBottom()) 490 491 delayableExecutor.advanceClockToLast() 492 delayableExecutor.runAllReady() 493 494 verify(view, never()).setQSContainerPaddingBottom(0) 495 verify(view).setQSContainerPaddingBottom(STABLE_INSET_BOTTOM) 496 } 497 498 @Test testStartCustomizingWithDurationnull499 fun testStartCustomizingWithDuration() { 500 underTest.setCustomizerShowing(true, 100L) 501 verify(shadeHeaderController).startCustomizingAnimation(true, 100L) 502 } 503 504 @Test testEndCustomizingWithDurationnull505 fun testEndCustomizingWithDuration() { 506 underTest.setCustomizerShowing(true, 0L) // Only tracks changes 507 reset(shadeHeaderController) 508 509 underTest.setCustomizerShowing(false, 100L) 510 verify(shadeHeaderController).startCustomizingAnimation(false, 100L) 511 } 512 513 @Test testTagListenerAddednull514 fun testTagListenerAdded() { 515 verify(fragmentHostManager).addTagListener(eq(QS.TAG), eq(view)) 516 } 517 518 @Test testTagListenerRemovednull519 fun testTagListenerRemoved() { 520 attachStateListenerCaptor.value.onViewDetachedFromWindow(view) 521 verify(fragmentHostManager).removeTagListener(eq(QS.TAG), eq(view)) 522 } 523 disableSplitShadenull524 private fun disableSplitShade() { 525 setSplitShadeEnabled(false) 526 } 527 enableSplitShadenull528 private fun enableSplitShade() { 529 setSplitShadeEnabled(true) 530 } 531 setSplitShadeEnablednull532 private fun setSplitShadeEnabled(enabled: Boolean) { 533 overrideResource(R.bool.config_use_split_notification_shade, enabled) 534 underTest.updateResources() 535 } 536 setSmallScreennull537 private fun setSmallScreen() { 538 setLargeScreenEnabled(false) 539 } 540 setLargeScreennull541 private fun setLargeScreen() { 542 setLargeScreenEnabled(true) 543 } 544 setLargeScreenEnablednull545 private fun setLargeScreenEnabled(enabled: Boolean) { 546 overrideResource(R.bool.config_use_large_screen_shade_header, enabled) 547 } 548 givennull549 private fun given( 550 taskbarVisible: Boolean, 551 navigationMode: Int, 552 insets: WindowInsets, 553 applyImmediately: Boolean = true 554 ) { 555 Mockito.clearInvocations(view) 556 taskbarVisibilityCallback.onTaskbarStatusUpdated(taskbarVisible, false) 557 navigationModeCallback.onNavigationModeChanged(navigationMode) 558 windowInsetsCallback.accept(insets) 559 if (applyImmediately) { 560 delayableExecutor.advanceClockToLast() 561 delayableExecutor.runAllReady() 562 } 563 } 564 thennull565 fun then( 566 expectedContainerPadding: Int, 567 expectedNotificationsMargin: Int = NOTIFICATIONS_MARGIN, 568 expectedQsPadding: Int = 0 569 ) { 570 verify(view).setPadding(anyInt(), anyInt(), anyInt(), eq(expectedContainerPadding)) 571 verify(view).setNotificationsMarginBottom(expectedNotificationsMargin) 572 verify(view).setQSContainerPaddingBottom(expectedQsPadding) 573 Mockito.clearInvocations(view) 574 } 575 windowInsetsnull576 private fun windowInsets() = mock(WindowInsets::class.java, RETURNS_DEEP_STUBS) 577 578 private fun emptyInsets() = mock(WindowInsets::class.java) 579 580 private fun WindowInsets.withCutout(): WindowInsets { 581 whenever(checkNotNull(displayCutout).safeInsetBottom).thenReturn(CUTOUT_HEIGHT) 582 return this 583 } 584 withStableBottomnull585 private fun WindowInsets.withStableBottom(): WindowInsets { 586 whenever(stableInsetBottom).thenReturn(STABLE_INSET_BOTTOM) 587 return this 588 } 589 getConstraintSetLayoutnull590 private fun getConstraintSetLayout(@IdRes id: Int): ConstraintSet.Layout { 591 return constraintSetCaptor.value.getConstraint(id).layout 592 } 593 newViewWithIdnull594 private fun newViewWithId(id: Int): View { 595 val view = View(mContext) 596 view.id = id 597 val layoutParams = 598 ConstraintLayout.LayoutParams( 599 ViewGroup.LayoutParams.WRAP_CONTENT, 600 ViewGroup.LayoutParams.WRAP_CONTENT 601 ) 602 // required as cloning ConstraintSet fails if view doesn't have layout params 603 view.layoutParams = layoutParams 604 return view 605 } 606 607 companion object { 608 const val STABLE_INSET_BOTTOM = 100 609 const val CUTOUT_HEIGHT = 50 610 const val GESTURES_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL 611 const val BUTTONS_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON 612 const val NOTIFICATIONS_MARGIN = 50 613 const val SCRIM_MARGIN = 10 614 const val FOOTER_ACTIONS_INSET = 2 615 const val FOOTER_ACTIONS_PADDING = 2 616 const val FOOTER_ACTIONS_OFFSET = FOOTER_ACTIONS_INSET + FOOTER_ACTIONS_PADDING 617 const val QS_PADDING_OFFSET = SCRIM_MARGIN + FOOTER_ACTIONS_OFFSET 618 } 619 } 620