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