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