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