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