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