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.platform.test.annotations.EnableFlags
21 import android.testing.AndroidTestingRunner
22 import android.testing.TestableLooper
23 import android.view.View
24 import android.view.ViewGroup
25 import android.view.WindowInsets
26 import android.view.WindowManagerPolicyConstants
27 import androidx.annotation.IdRes
28 import androidx.constraintlayout.widget.ConstraintLayout
29 import androidx.constraintlayout.widget.ConstraintSet
30 import androidx.test.filters.SmallTest
31 import com.android.systemui.Flags.FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX
32 import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT
33 import com.android.systemui.SysuiTestCase
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.res.R
42 import com.android.systemui.shade.domain.interactor.ShadeInteractor
43 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
44 import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController
45 import com.android.systemui.util.concurrency.FakeExecutor
46 import com.android.systemui.util.mockito.capture
47 import com.android.systemui.util.mockito.mock
48 import com.android.systemui.util.mockito.whenever
49 import com.android.systemui.util.time.FakeSystemClock
50 import com.google.common.truth.Truth.assertThat
51 import java.util.function.Consumer
52 import kotlinx.coroutines.flow.MutableStateFlow
53 import org.junit.Before
54 import org.junit.Test
55 import org.junit.runner.RunWith
56 import org.mockito.ArgumentCaptor
57 import org.mockito.Captor
58 import org.mockito.Mockito
59 import org.mockito.Mockito.RETURNS_DEEP_STUBS
60 import org.mockito.Mockito.any
61 import org.mockito.Mockito.anyInt
62 import org.mockito.Mockito.doNothing
63 import org.mockito.Mockito.eq
64 import org.mockito.Mockito.mock
65 import org.mockito.Mockito.never
66 import org.mockito.Mockito.reset
67 import org.mockito.Mockito.verify
68 import org.mockito.MockitoAnnotations
69 
70 /**
71  * Uses Flags.KEYGUARD_STATUS_VIEW_MIGRATE_NSSL set to false. If all goes well, this set of tests
72  * will be deleted.
73  */
74 @RunWith(AndroidTestingRunner::class)
75 @TestableLooper.RunWithLooper(setAsMainLooper = true)
76 @SmallTest
77 class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() {
78 
79     private val view = mock<NotificationsQuickSettingsContainer>()
80     private val navigationModeController = mock<NavigationModeController>()
81     private val overviewProxyService = mock<OverviewProxyService>()
82     private val shadeHeaderController = mock<ShadeHeaderController>()
83     private val shadeInteractor = mock<ShadeInteractor>()
84     private val fragmentService = mock<FragmentService>()
85     private val fragmentHostManager = mock<FragmentHostManager>()
86     private val notificationStackScrollLayoutController =
87         mock<NotificationStackScrollLayoutController>()
88     private val largeScreenHeaderHelper = mock<LargeScreenHeaderHelper>()
89 
90     @Captor lateinit var navigationModeCaptor: ArgumentCaptor<ModeChangedListener>
91     @Captor lateinit var taskbarVisibilityCaptor: ArgumentCaptor<OverviewProxyListener>
92     @Captor lateinit var windowInsetsCallbackCaptor: ArgumentCaptor<Consumer<WindowInsets>>
93     @Captor lateinit var constraintSetCaptor: ArgumentCaptor<ConstraintSet>
94     @Captor lateinit var attachStateListenerCaptor: ArgumentCaptor<View.OnAttachStateChangeListener>
95 
96     lateinit var underTest: NotificationsQSContainerController
97 
98     private lateinit var navigationModeCallback: ModeChangedListener
99     private lateinit var taskbarVisibilityCallback: OverviewProxyListener
100     private lateinit var windowInsetsCallback: Consumer<WindowInsets>
101     private lateinit var fakeSystemClock: FakeSystemClock
102     private lateinit var delayableExecutor: FakeExecutor
103 
104     @Before
setupnull105     fun setup() {
106         MockitoAnnotations.initMocks(this)
107         fakeSystemClock = FakeSystemClock()
108         delayableExecutor = FakeExecutor(fakeSystemClock)
109         mContext.ensureTestableResources()
110         whenever(view.context).thenReturn(mContext)
111         whenever(view.resources).thenReturn(mContext.resources)
112 
113         whenever(fragmentService.getFragmentHostManager(any())).thenReturn(fragmentHostManager)
114         whenever(shadeInteractor.isQsExpanded).thenReturn(MutableStateFlow(false))
115 
116         underTest =
117             NotificationsQSContainerController(
118                 view,
119                 navigationModeController,
120                 overviewProxyService,
121                 shadeHeaderController,
122                 shadeInteractor,
123                 fragmentService,
124                 delayableExecutor,
125                 notificationStackScrollLayoutController,
126                 ResourcesSplitShadeStateController(),
127                 largeScreenHeaderHelperLazy = { largeScreenHeaderHelper }
128             )
129 
130         overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, SCRIM_MARGIN)
131         overrideResource(R.dimen.notification_panel_margin_bottom, NOTIFICATIONS_MARGIN)
132         overrideResource(R.bool.config_use_split_notification_shade, false)
133         overrideResource(R.dimen.qs_footer_actions_bottom_padding, FOOTER_ACTIONS_PADDING)
134         overrideResource(R.dimen.qs_footer_action_inset, FOOTER_ACTIONS_INSET)
135         whenever(navigationModeController.addListener(navigationModeCaptor.capture()))
136             .thenReturn(GESTURES_NAVIGATION)
137         doNothing().`when`(overviewProxyService).addCallback(taskbarVisibilityCaptor.capture())
138         doNothing().`when`(view).setInsetsChangedListener(windowInsetsCallbackCaptor.capture())
139         doNothing().`when`(view).applyConstraints(constraintSetCaptor.capture())
140         doNothing().`when`(view).addOnAttachStateChangeListener(attachStateListenerCaptor.capture())
141         underTest.init()
142         attachStateListenerCaptor.value.onViewAttachedToWindow(view)
143 
144         navigationModeCallback = navigationModeCaptor.value
145         taskbarVisibilityCallback = taskbarVisibilityCaptor.value
146         windowInsetsCallback = windowInsetsCallbackCaptor.value
147 
148         Mockito.clearInvocations(view)
149     }
150 
151     @Test
testSmallScreen_updateResources_splitShadeHeightIsSetnull152     fun testSmallScreen_updateResources_splitShadeHeightIsSet() {
153         overrideResource(R.bool.config_use_large_screen_shade_header, false)
154         overrideResource(R.dimen.qs_header_height, 10)
155         overrideResource(R.dimen.large_screen_shade_header_height, 20)
156 
157         // ensure the estimated height (would be 3 here) wouldn't impact this test case
158         overrideResource(R.dimen.large_screen_shade_header_min_height, 1)
159         overrideResource(R.dimen.new_qs_header_non_clickable_element_height, 1)
160 
161         underTest.updateResources()
162 
163         val captor = ArgumentCaptor.forClass(ConstraintSet::class.java)
164         verify(view).applyConstraints(capture(captor))
165         assertThat(captor.value.getHeight(R.id.split_shade_status_bar)).isEqualTo(10)
166     }
167 
168     @Test
169     @DisableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX)
testLargeScreen_updateResources_refactorFlagOff_splitShadeHeightIsSetBasedOnResourcenull170     fun testLargeScreen_updateResources_refactorFlagOff_splitShadeHeightIsSetBasedOnResource() {
171         val headerResourceHeight = 20
172         val headerHelperHeight = 30
173         whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight())
174             .thenReturn(headerHelperHeight)
175         overrideResource(R.bool.config_use_large_screen_shade_header, true)
176         overrideResource(R.dimen.qs_header_height, 10)
177         overrideResource(R.dimen.large_screen_shade_header_height, headerResourceHeight)
178 
179         // ensure the estimated height (would be 3 here) wouldn't impact this test case
180         overrideResource(R.dimen.large_screen_shade_header_min_height, 1)
181         overrideResource(R.dimen.new_qs_header_non_clickable_element_height, 1)
182 
183         underTest.updateResources()
184 
185         val captor = ArgumentCaptor.forClass(ConstraintSet::class.java)
186         verify(view).applyConstraints(capture(captor))
187         assertThat(captor.value.getHeight(R.id.split_shade_status_bar))
188             .isEqualTo(headerResourceHeight)
189     }
190 
191     @Test
192     @EnableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX)
testLargeScreen_updateResources_refactorFlagOn_splitShadeHeightIsSetBasedOnHelpernull193     fun testLargeScreen_updateResources_refactorFlagOn_splitShadeHeightIsSetBasedOnHelper() {
194         val headerResourceHeight = 20
195         val headerHelperHeight = 30
196         whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight())
197             .thenReturn(headerHelperHeight)
198         overrideResource(R.bool.config_use_large_screen_shade_header, true)
199         overrideResource(R.dimen.qs_header_height, 10)
200         overrideResource(R.dimen.large_screen_shade_header_height, headerResourceHeight)
201 
202         // ensure the estimated height (would be 3 here) wouldn't impact this test case
203         overrideResource(R.dimen.large_screen_shade_header_min_height, 1)
204         overrideResource(R.dimen.new_qs_header_non_clickable_element_height, 1)
205 
206         underTest.updateResources()
207 
208         val captor = ArgumentCaptor.forClass(ConstraintSet::class.java)
209         verify(view).applyConstraints(capture(captor))
210         assertThat(captor.value.getHeight(R.id.split_shade_status_bar))
211             .isEqualTo(headerHelperHeight)
212     }
213 
214     @Test
testSmallScreen_estimatedHeightIsLargerThanDimenValue_shadeHeightIsSetToEstimatedHeightnull215     fun testSmallScreen_estimatedHeightIsLargerThanDimenValue_shadeHeightIsSetToEstimatedHeight() {
216         overrideResource(R.bool.config_use_large_screen_shade_header, false)
217         overrideResource(R.dimen.qs_header_height, 10)
218         overrideResource(R.dimen.large_screen_shade_header_height, 20)
219 
220         // make the estimated height (would be 15 here) larger than qs_header_height
221         overrideResource(R.dimen.large_screen_shade_header_min_height, 5)
222         overrideResource(R.dimen.new_qs_header_non_clickable_element_height, 5)
223 
224         underTest.updateResources()
225 
226         val captor = ArgumentCaptor.forClass(ConstraintSet::class.java)
227         verify(view).applyConstraints(capture(captor))
228         assertThat(captor.value.getHeight(R.id.split_shade_status_bar)).isEqualTo(15)
229     }
230 
231     @Test
testTaskbarVisibleInSplitShadenull232     fun testTaskbarVisibleInSplitShade() {
233         enableSplitShade()
234 
235         given(
236             taskbarVisible = true,
237             navigationMode = GESTURES_NAVIGATION,
238             insets = windowInsets().withStableBottom()
239         )
240         then(
241             expectedContainerPadding = 0, // taskbar should disappear when shade is expanded
242             expectedNotificationsMargin = NOTIFICATIONS_MARGIN,
243             expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET
244         )
245 
246         given(
247             taskbarVisible = true,
248             navigationMode = BUTTONS_NAVIGATION,
249             insets = windowInsets().withStableBottom()
250         )
251         then(
252             expectedContainerPadding = STABLE_INSET_BOTTOM,
253             expectedNotificationsMargin = NOTIFICATIONS_MARGIN,
254             expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET
255         )
256     }
257 
258     @Test
testTaskbarNotVisibleInSplitShadenull259     fun testTaskbarNotVisibleInSplitShade() {
260         // when taskbar is not visible, it means we're on the home screen
261         enableSplitShade()
262 
263         given(
264             taskbarVisible = false,
265             navigationMode = GESTURES_NAVIGATION,
266             insets = windowInsets().withStableBottom()
267         )
268         then(
269             expectedContainerPadding = 0,
270             expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET
271         )
272 
273         given(
274             taskbarVisible = false,
275             navigationMode = BUTTONS_NAVIGATION,
276             insets = windowInsets().withStableBottom()
277         )
278         then(
279             expectedContainerPadding = 0, // qs goes full height as it's not obscuring nav buttons
280             expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN,
281             expectedQsPadding = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET
282         )
283     }
284 
285     @Test
testTaskbarNotVisibleInSplitShadeWithCutoutnull286     fun testTaskbarNotVisibleInSplitShadeWithCutout() {
287         enableSplitShade()
288 
289         given(
290             taskbarVisible = false,
291             navigationMode = GESTURES_NAVIGATION,
292             insets = windowInsets().withCutout()
293         )
294         then(
295             expectedContainerPadding = CUTOUT_HEIGHT,
296             expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET
297         )
298 
299         given(
300             taskbarVisible = false,
301             navigationMode = BUTTONS_NAVIGATION,
302             insets = windowInsets().withCutout().withStableBottom()
303         )
304         then(
305             expectedContainerPadding = 0,
306             expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN,
307             expectedQsPadding = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET
308         )
309     }
310 
311     @Test
testTaskbarVisibleInSinglePaneShadenull312     fun testTaskbarVisibleInSinglePaneShade() {
313         disableSplitShade()
314 
315         given(
316             taskbarVisible = true,
317             navigationMode = GESTURES_NAVIGATION,
318             insets = windowInsets().withStableBottom()
319         )
320         then(expectedContainerPadding = 0, expectedQsPadding = STABLE_INSET_BOTTOM)
321 
322         given(
323             taskbarVisible = true,
324             navigationMode = BUTTONS_NAVIGATION,
325             insets = windowInsets().withStableBottom()
326         )
327         then(
328             expectedContainerPadding = STABLE_INSET_BOTTOM,
329             expectedQsPadding = STABLE_INSET_BOTTOM
330         )
331     }
332 
333     @Test
testTaskbarNotVisibleInSinglePaneShadenull334     fun testTaskbarNotVisibleInSinglePaneShade() {
335         disableSplitShade()
336 
337         given(taskbarVisible = false, navigationMode = GESTURES_NAVIGATION, insets = emptyInsets())
338         then(expectedContainerPadding = 0)
339 
340         given(
341             taskbarVisible = false,
342             navigationMode = GESTURES_NAVIGATION,
343             insets = windowInsets().withCutout().withStableBottom()
344         )
345         then(expectedContainerPadding = CUTOUT_HEIGHT, expectedQsPadding = STABLE_INSET_BOTTOM)
346 
347         given(
348             taskbarVisible = false,
349             navigationMode = BUTTONS_NAVIGATION,
350             insets = windowInsets().withStableBottom()
351         )
352         then(
353             expectedContainerPadding = 0,
354             expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN,
355             expectedQsPadding = STABLE_INSET_BOTTOM
356         )
357     }
358 
359     @Test
testDetailShowingInSinglePaneShadenull360     fun testDetailShowingInSinglePaneShade() {
361         disableSplitShade()
362         underTest.setDetailShowing(true)
363 
364         // always sets spacings to 0
365         given(
366             taskbarVisible = false,
367             navigationMode = GESTURES_NAVIGATION,
368             insets = windowInsets().withStableBottom()
369         )
370         then(expectedContainerPadding = 0, expectedNotificationsMargin = 0)
371 
372         given(taskbarVisible = false, navigationMode = BUTTONS_NAVIGATION, insets = emptyInsets())
373         then(expectedContainerPadding = 0, expectedNotificationsMargin = 0)
374     }
375 
376     @Test
testDetailShowingInSplitShadenull377     fun testDetailShowingInSplitShade() {
378         enableSplitShade()
379         underTest.setDetailShowing(true)
380 
381         given(
382             taskbarVisible = false,
383             navigationMode = GESTURES_NAVIGATION,
384             insets = windowInsets().withStableBottom()
385         )
386         then(expectedContainerPadding = 0)
387 
388         // should not influence spacing
389         given(taskbarVisible = false, navigationMode = BUTTONS_NAVIGATION, insets = emptyInsets())
390         then(expectedContainerPadding = 0)
391     }
392 
393     @Test
testNotificationsMarginBottomIsUpdatednull394     fun testNotificationsMarginBottomIsUpdated() {
395         Mockito.clearInvocations(view)
396         enableSplitShade()
397         verify(view).setNotificationsMarginBottom(NOTIFICATIONS_MARGIN)
398 
399         overrideResource(R.dimen.notification_panel_margin_bottom, 100)
400         disableSplitShade()
401         verify(view).setNotificationsMarginBottom(100)
402     }
403 
404     @Test
405     @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
testSplitShadeLayout_isAlignedToGuidelinenull406     fun testSplitShadeLayout_isAlignedToGuideline() {
407         enableSplitShade()
408         underTest.updateResources()
409         assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd).isEqualTo(R.id.qs_edge_guideline)
410         assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).startToStart)
411             .isEqualTo(R.id.qs_edge_guideline)
412     }
413 
414     @Test
415     @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
testSinglePaneLayout_childrenHaveEqualMarginsnull416     fun testSinglePaneLayout_childrenHaveEqualMargins() {
417         disableSplitShade()
418         underTest.updateResources()
419         val qsStartMargin = getConstraintSetLayout(R.id.qs_frame).startMargin
420         val qsEndMargin = getConstraintSetLayout(R.id.qs_frame).endMargin
421         val notifStartMargin = getConstraintSetLayout(R.id.notification_stack_scroller).startMargin
422         val notifEndMargin = getConstraintSetLayout(R.id.notification_stack_scroller).endMargin
423         assertThat(
424                 qsStartMargin == qsEndMargin &&
425                     notifStartMargin == notifEndMargin &&
426                     qsStartMargin == notifStartMargin
427             )
428             .isTrue()
429     }
430 
431     @Test
432     @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
testSplitShadeLayout_childrenHaveInsideMarginsOfZeronull433     fun testSplitShadeLayout_childrenHaveInsideMarginsOfZero() {
434         enableSplitShade()
435         underTest.updateResources()
436         assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin).isEqualTo(0)
437         assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).startMargin)
438             .isEqualTo(0)
439     }
440 
441     @Test
testSplitShadeLayout_qsFrameHasHorizontalMarginsOfZeronull442     fun testSplitShadeLayout_qsFrameHasHorizontalMarginsOfZero() {
443         enableSplitShade()
444         underTest.updateResources()
445         assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin).isEqualTo(0)
446         assertThat(getConstraintSetLayout(R.id.qs_frame).startMargin).isEqualTo(0)
447     }
448 
449     @Test
450     @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX)
testLargeScreenLayout_refactorFlagOff_qsAndNotifsTopMarginIsOfHeaderHeightResourcenull451     fun testLargeScreenLayout_refactorFlagOff_qsAndNotifsTopMarginIsOfHeaderHeightResource() {
452         setLargeScreen()
453         val largeScreenHeaderResourceHeight = 100
454         val largeScreenHeaderHelperHeight = 200
455         whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight())
456             .thenReturn(largeScreenHeaderHelperHeight)
457         overrideResource(R.dimen.large_screen_shade_header_height, largeScreenHeaderResourceHeight)
458 
459         // ensure the estimated height (would be 30 here) wouldn't impact this test case
460         overrideResource(R.dimen.large_screen_shade_header_min_height, 10)
461         overrideResource(R.dimen.new_qs_header_non_clickable_element_height, 10)
462 
463         underTest.updateResources()
464 
465         assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin)
466             .isEqualTo(largeScreenHeaderResourceHeight)
467         assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).topMargin)
468             .isEqualTo(largeScreenHeaderResourceHeight)
469     }
470 
471     @Test
472     @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
473     @EnableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX)
testLargeScreenLayout_refactorFlagOn_qsAndNotifsTopMarginIsOfHeaderHeightHelpernull474     fun testLargeScreenLayout_refactorFlagOn_qsAndNotifsTopMarginIsOfHeaderHeightHelper() {
475         setLargeScreen()
476         val largeScreenHeaderResourceHeight = 100
477         val largeScreenHeaderHelperHeight = 200
478         whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight())
479             .thenReturn(largeScreenHeaderHelperHeight)
480         overrideResource(R.dimen.large_screen_shade_header_height, largeScreenHeaderResourceHeight)
481 
482         // ensure the estimated height (would be 30 here) wouldn't impact this test case
483         overrideResource(R.dimen.large_screen_shade_header_min_height, 10)
484         overrideResource(R.dimen.new_qs_header_non_clickable_element_height, 10)
485 
486         underTest.updateResources()
487 
488         assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin)
489             .isEqualTo(largeScreenHeaderHelperHeight)
490         assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).topMargin)
491             .isEqualTo(largeScreenHeaderHelperHeight)
492     }
493 
494     @Test
495     @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
testSmallScreenLayout_qsAndNotifsTopMarginIsZeronull496     fun testSmallScreenLayout_qsAndNotifsTopMarginIsZero() {
497         setSmallScreen()
498         underTest.updateResources()
499         assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin).isEqualTo(0)
500         assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).topMargin).isEqualTo(0)
501     }
502 
503     @Test
testSinglePaneShadeLayout_qsFrameHasHorizontalMarginsSetToCorrectValuenull504     fun testSinglePaneShadeLayout_qsFrameHasHorizontalMarginsSetToCorrectValue() {
505         disableSplitShade()
506         underTest.updateResources()
507         val notificationPanelMarginHorizontal =
508             mContext.resources.getDimensionPixelSize(R.dimen.notification_panel_margin_horizontal)
509         assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin)
510             .isEqualTo(notificationPanelMarginHorizontal)
511         assertThat(getConstraintSetLayout(R.id.qs_frame).startMargin)
512             .isEqualTo(notificationPanelMarginHorizontal)
513     }
514 
515     @Test
516     @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
testSinglePaneShadeLayout_isAlignedToParentnull517     fun testSinglePaneShadeLayout_isAlignedToParent() {
518         disableSplitShade()
519         underTest.updateResources()
520         assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd)
521             .isEqualTo(ConstraintSet.PARENT_ID)
522         assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).startToStart)
523             .isEqualTo(ConstraintSet.PARENT_ID)
524     }
525 
526     @Test
testAllChildrenOfNotificationContainer_haveIdsnull527     fun testAllChildrenOfNotificationContainer_haveIds() {
528         // set dimen to 0 to avoid triggering updating bottom spacing
529         overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, 0)
530         val container = NotificationsQuickSettingsContainer(mContext, null)
531         container.removeAllViews()
532         container.addView(newViewWithId(1))
533         container.addView(newViewWithId(View.NO_ID))
534         val controller =
535             NotificationsQSContainerController(
536                 container,
537                 navigationModeController,
538                 overviewProxyService,
539                 shadeHeaderController,
540                 shadeInteractor,
541                 fragmentService,
542                 delayableExecutor,
543                 notificationStackScrollLayoutController,
544                 ResourcesSplitShadeStateController(),
545                 largeScreenHeaderHelperLazy = { largeScreenHeaderHelper }
546             )
547         controller.updateConstraints()
548 
549         assertThat(container.getChildAt(0).id).isEqualTo(1)
550         assertThat(container.getChildAt(1).id).isNotEqualTo(View.NO_ID)
551     }
552 
553     @Test
testWindowInsetDebouncenull554     fun testWindowInsetDebounce() {
555         disableSplitShade()
556 
557         given(
558             taskbarVisible = false,
559             navigationMode = GESTURES_NAVIGATION,
560             insets = emptyInsets(),
561             applyImmediately = false
562         )
563         fakeSystemClock.advanceTime(INSET_DEBOUNCE_MILLIS / 2)
564         windowInsetsCallback.accept(windowInsets().withStableBottom())
565 
566         delayableExecutor.advanceClockToLast()
567         delayableExecutor.runAllReady()
568 
569         verify(view, never()).setQSContainerPaddingBottom(0)
570         verify(view).setQSContainerPaddingBottom(STABLE_INSET_BOTTOM)
571     }
572 
573     @Test
testStartCustomizingWithDurationnull574     fun testStartCustomizingWithDuration() {
575         underTest.setCustomizerShowing(true, 100L)
576         verify(shadeHeaderController).startCustomizingAnimation(true, 100L)
577     }
578 
579     @Test
testEndCustomizingWithDurationnull580     fun testEndCustomizingWithDuration() {
581         underTest.setCustomizerShowing(true, 0L) // Only tracks changes
582         reset(shadeHeaderController)
583 
584         underTest.setCustomizerShowing(false, 100L)
585         verify(shadeHeaderController).startCustomizingAnimation(false, 100L)
586     }
587 
588     @Test
testTagListenerAddednull589     fun testTagListenerAdded() {
590         verify(fragmentHostManager).addTagListener(eq(QS.TAG), eq(view))
591     }
592 
593     @Test
testTagListenerRemovednull594     fun testTagListenerRemoved() {
595         attachStateListenerCaptor.value.onViewDetachedFromWindow(view)
596         verify(fragmentHostManager).removeTagListener(eq(QS.TAG), eq(view))
597     }
598 
disableSplitShadenull599     private fun disableSplitShade() {
600         setSplitShadeEnabled(false)
601     }
602 
enableSplitShadenull603     private fun enableSplitShade() {
604         setSplitShadeEnabled(true)
605     }
606 
setSplitShadeEnablednull607     private fun setSplitShadeEnabled(enabled: Boolean) {
608         overrideResource(R.bool.config_use_split_notification_shade, enabled)
609         underTest.updateResources()
610     }
611 
setSmallScreennull612     private fun setSmallScreen() {
613         setLargeScreenEnabled(false)
614     }
615 
setLargeScreennull616     private fun setLargeScreen() {
617         setLargeScreenEnabled(true)
618     }
619 
setLargeScreenEnablednull620     private fun setLargeScreenEnabled(enabled: Boolean) {
621         overrideResource(R.bool.config_use_large_screen_shade_header, enabled)
622     }
623 
givennull624     private fun given(
625         taskbarVisible: Boolean,
626         navigationMode: Int,
627         insets: WindowInsets,
628         applyImmediately: Boolean = true
629     ) {
630         Mockito.clearInvocations(view)
631         taskbarVisibilityCallback.onTaskbarStatusUpdated(taskbarVisible, false)
632         navigationModeCallback.onNavigationModeChanged(navigationMode)
633         windowInsetsCallback.accept(insets)
634         if (applyImmediately) {
635             delayableExecutor.advanceClockToLast()
636             delayableExecutor.runAllReady()
637         }
638     }
639 
thennull640     fun then(
641         expectedContainerPadding: Int,
642         expectedNotificationsMargin: Int = NOTIFICATIONS_MARGIN,
643         expectedQsPadding: Int = 0
644     ) {
645         verify(view).setPadding(anyInt(), anyInt(), anyInt(), eq(expectedContainerPadding))
646         verify(view).setNotificationsMarginBottom(expectedNotificationsMargin)
647         verify(view).setQSContainerPaddingBottom(expectedQsPadding)
648         Mockito.clearInvocations(view)
649     }
650 
windowInsetsnull651     private fun windowInsets() = mock(WindowInsets::class.java, RETURNS_DEEP_STUBS)
652 
653     private fun emptyInsets() = mock(WindowInsets::class.java)
654 
655     private fun WindowInsets.withCutout(): WindowInsets {
656         whenever(checkNotNull(displayCutout).safeInsetBottom).thenReturn(CUTOUT_HEIGHT)
657         return this
658     }
659 
withStableBottomnull660     private fun WindowInsets.withStableBottom(): WindowInsets {
661         whenever(stableInsetBottom).thenReturn(STABLE_INSET_BOTTOM)
662         return this
663     }
664 
getConstraintSetLayoutnull665     private fun getConstraintSetLayout(@IdRes id: Int): ConstraintSet.Layout {
666         return constraintSetCaptor.value.getConstraint(id).layout
667     }
668 
newViewWithIdnull669     private fun newViewWithId(id: Int): View {
670         val view = View(mContext)
671         view.id = id
672         val layoutParams =
673             ConstraintLayout.LayoutParams(
674                 ViewGroup.LayoutParams.WRAP_CONTENT,
675                 ViewGroup.LayoutParams.WRAP_CONTENT
676             )
677         // required as cloning ConstraintSet fails if view doesn't have layout params
678         view.layoutParams = layoutParams
679         return view
680     }
681 
682     companion object {
683         const val STABLE_INSET_BOTTOM = 100
684         const val CUTOUT_HEIGHT = 50
685         const val GESTURES_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL
686         const val BUTTONS_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON
687         const val NOTIFICATIONS_MARGIN = 50
688         const val SCRIM_MARGIN = 10
689         const val FOOTER_ACTIONS_INSET = 2
690         const val FOOTER_ACTIONS_PADDING = 2
691         const val FOOTER_ACTIONS_OFFSET = FOOTER_ACTIONS_INSET + FOOTER_ACTIONS_PADDING
692         const val QS_PADDING_OFFSET = SCRIM_MARGIN + FOOTER_ACTIONS_OFFSET
693     }
694 }
695