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