1 /*
2  * Copyright (C) 2022 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 package com.android.systemui.shade
17 
18 import android.animation.Animator
19 import android.app.AlarmManager
20 import android.app.PendingIntent
21 import android.app.StatusBarManager
22 import android.content.Context
23 import android.content.res.Resources
24 import android.content.res.XmlResourceParser
25 import android.graphics.Rect
26 import android.testing.AndroidTestingRunner
27 import android.view.Display
28 import android.view.DisplayCutout
29 import android.view.View
30 import android.view.ViewPropertyAnimator
31 import android.view.WindowInsets
32 import android.widget.LinearLayout
33 import android.widget.TextView
34 import androidx.constraintlayout.motion.widget.MotionLayout
35 import androidx.constraintlayout.widget.ConstraintSet
36 import androidx.test.filters.SmallTest
37 import com.android.app.animation.Interpolators
38 import com.android.systemui.R
39 import com.android.systemui.SysuiTestCase
40 import com.android.systemui.animation.ShadeInterpolation
41 import com.android.systemui.battery.BatteryMeterView
42 import com.android.systemui.battery.BatteryMeterViewController
43 import com.android.systemui.demomode.DemoMode
44 import com.android.systemui.demomode.DemoModeController
45 import com.android.systemui.dump.DumpManager
46 import com.android.systemui.plugins.ActivityStarter
47 import com.android.systemui.qs.ChipVisibilityListener
48 import com.android.systemui.qs.HeaderPrivacyIconsController
49 import com.android.systemui.shade.ShadeHeaderController.Companion.DEFAULT_CLOCK_INTENT
50 import com.android.systemui.shade.ShadeHeaderController.Companion.LARGE_SCREEN_HEADER_CONSTRAINT
51 import com.android.systemui.shade.ShadeHeaderController.Companion.QQS_HEADER_CONSTRAINT
52 import com.android.systemui.shade.ShadeHeaderController.Companion.QS_HEADER_CONSTRAINT
53 import com.android.systemui.shade.carrier.ShadeCarrierGroup
54 import com.android.systemui.shade.carrier.ShadeCarrierGroupController
55 import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
56 import com.android.systemui.statusbar.phone.StatusBarIconController
57 import com.android.systemui.statusbar.phone.StatusIconContainer
58 import com.android.systemui.statusbar.phone.StatusOverlayHoverListenerFactory
59 import com.android.systemui.statusbar.policy.Clock
60 import com.android.systemui.statusbar.policy.FakeConfigurationController
61 import com.android.systemui.statusbar.policy.NextAlarmController
62 import com.android.systemui.statusbar.policy.VariableDateView
63 import com.android.systemui.statusbar.policy.VariableDateViewController
64 import com.android.systemui.util.mockito.any
65 import com.android.systemui.util.mockito.argumentCaptor
66 import com.android.systemui.util.mockito.capture
67 import com.android.systemui.util.mockito.eq
68 import com.android.systemui.util.mockito.mock
69 import com.google.common.truth.Truth.assertThat
70 import org.junit.Before
71 import org.junit.Rule
72 import org.junit.Test
73 import org.junit.runner.RunWith
74 import org.mockito.Answers
75 import org.mockito.ArgumentCaptor
76 import org.mockito.ArgumentMatchers.anyFloat
77 import org.mockito.ArgumentMatchers.anyInt
78 import org.mockito.Captor
79 import org.mockito.Mock
80 import org.mockito.Mockito
81 import org.mockito.Mockito.mock
82 import org.mockito.Mockito.reset
83 import org.mockito.Mockito.times
84 import org.mockito.Mockito.verify
85 import org.mockito.Mockito.`when` as whenever
86 import org.mockito.junit.MockitoJUnit
87 
88 private val EMPTY_CHANGES = ConstraintsChanges()
89 
90 @SmallTest
91 @RunWith(AndroidTestingRunner::class)
92 class ShadeHeaderControllerTest : SysuiTestCase() {
93 
94     @Mock(answer = Answers.RETURNS_MOCKS) private lateinit var view: MotionLayout
95     @Mock private lateinit var statusIcons: StatusIconContainer
96     @Mock private lateinit var statusBarIconController: StatusBarIconController
97     @Mock private lateinit var iconManagerFactory: StatusBarIconController.TintedIconManager.Factory
98     @Mock private lateinit var iconManager: StatusBarIconController.TintedIconManager
99     @Mock private lateinit var mShadeCarrierGroupController: ShadeCarrierGroupController
100     @Mock
101     private lateinit var mShadeCarrierGroupControllerBuilder: ShadeCarrierGroupController.Builder
102     @Mock private lateinit var clock: Clock
103     @Mock private lateinit var date: VariableDateView
104     @Mock private lateinit var carrierGroup: ShadeCarrierGroup
105     @Mock private lateinit var batteryMeterView: BatteryMeterView
106     @Mock private lateinit var batteryMeterViewController: BatteryMeterViewController
107     @Mock private lateinit var privacyIconsController: HeaderPrivacyIconsController
108     @Mock private lateinit var insetsProvider: StatusBarContentInsetsProvider
109     @Mock private lateinit var variableDateViewControllerFactory: VariableDateViewController.Factory
110     @Mock private lateinit var variableDateViewController: VariableDateViewController
111     @Mock private lateinit var dumpManager: DumpManager
112     @Mock
113     private lateinit var combinedShadeHeadersConstraintManager:
114         CombinedShadeHeadersConstraintManager
115 
116     @Mock private lateinit var mockedContext: Context
117     private lateinit var viewContext: Context
118 
119     @Mock private lateinit var qqsConstraints: ConstraintSet
120     @Mock private lateinit var qsConstraints: ConstraintSet
121     @Mock private lateinit var largeScreenConstraints: ConstraintSet
122 
123     @Mock private lateinit var demoModeController: DemoModeController
124     @Mock private lateinit var qsBatteryModeController: QsBatteryModeController
125     @Mock private lateinit var nextAlarmController: NextAlarmController
126     @Mock private lateinit var activityStarter: ActivityStarter
127     @Mock private lateinit var mStatusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory
128 
129     @JvmField @Rule val mockitoRule = MockitoJUnit.rule()
130     var viewVisibility = View.GONE
131     var viewAlpha = 1f
132 
133     private val systemIcons = LinearLayout(context)
134     private lateinit var shadeHeaderController: ShadeHeaderController
135     private lateinit var carrierIconSlots: List<String>
136     private val configurationController = FakeConfigurationController()
137     @Captor private lateinit var demoModeControllerCapture: ArgumentCaptor<DemoMode>
138 
139     @Before
140     fun setup() {
141         whenever<Clock>(view.requireViewById(R.id.clock)).thenReturn(clock)
142         whenever(clock.context).thenReturn(mockedContext)
143 
144         whenever<TextView>(view.requireViewById(R.id.date)).thenReturn(date)
145         whenever(date.context).thenReturn(mockedContext)
146 
147         whenever<ShadeCarrierGroup>(view.requireViewById(R.id.carrier_group)).thenReturn(carrierGroup)
148 
149         whenever<BatteryMeterView>(view.requireViewById(R.id.batteryRemainingIcon))
150             .thenReturn(batteryMeterView)
151 
152         whenever<StatusIconContainer>(view.requireViewById(R.id.statusIcons)).thenReturn(statusIcons)
153         whenever<View>(view.requireViewById(R.id.shade_header_system_icons)).thenReturn(systemIcons)
154 
155         viewContext = Mockito.spy(context)
156         whenever(view.context).thenReturn(viewContext)
157         whenever(view.resources).thenReturn(context.resources)
158         whenever(statusIcons.context).thenReturn(context)
159         whenever(mShadeCarrierGroupControllerBuilder.setShadeCarrierGroup(any()))
160             .thenReturn(mShadeCarrierGroupControllerBuilder)
161         whenever(mShadeCarrierGroupControllerBuilder.build())
162             .thenReturn(mShadeCarrierGroupController)
163         whenever(view.setVisibility(anyInt())).then {
164             viewVisibility = it.arguments[0] as Int
165             null
166         }
167         whenever(view.visibility).thenAnswer { _ -> viewVisibility }
168 
169         whenever(view.setAlpha(anyFloat())).then {
170             viewAlpha = it.arguments[0] as Float
171             null
172         }
173         whenever(view.alpha).thenAnswer { _ -> viewAlpha }
174 
175         whenever(variableDateViewControllerFactory.create(any()))
176             .thenReturn(variableDateViewController)
177         whenever(iconManagerFactory.create(any(), any())).thenReturn(iconManager)
178 
179         setUpDefaultInsets()
180         setUpMotionLayout(view)
181 
182         shadeHeaderController =
183             ShadeHeaderController(
184                 view,
185                 statusBarIconController,
186                 iconManagerFactory,
187                 privacyIconsController,
188                 insetsProvider,
189                 configurationController,
190                 variableDateViewControllerFactory,
191                 batteryMeterViewController,
192                 dumpManager,
193                 mShadeCarrierGroupControllerBuilder,
194                 combinedShadeHeadersConstraintManager,
195                 demoModeController,
196                 qsBatteryModeController,
197                 nextAlarmController,
198                 activityStarter,
199                 mStatusOverlayHoverListenerFactory
200             )
201         whenever(view.isAttachedToWindow).thenReturn(true)
202         shadeHeaderController.init()
203         carrierIconSlots =
204             listOf(context.getString(com.android.internal.R.string.status_bar_mobile))
205     }
206 
207     @Test
208     fun updateListeners_registersWhenVisible() {
209         makeShadeVisible()
210         verify(mShadeCarrierGroupController).setListening(true)
211         verify(statusBarIconController).addIconGroup(any())
212     }
213 
214     @Test
215     fun statusIconsAddedWhenAttached() {
216         verify(statusBarIconController).addIconGroup(any())
217     }
218 
219     @Test
220     fun statusIconsRemovedWhenDettached() {
221         shadeHeaderController.simulateViewDetached()
222         verify(statusBarIconController).removeIconGroup(any())
223     }
224 
225     @Test
226     fun shadeExpandedFraction_updatesAlpha() {
227         makeShadeVisible()
228         shadeHeaderController.shadeExpandedFraction = 0.5f
229         verify(view).setAlpha(ShadeInterpolation.getContentAlpha(0.5f))
230     }
231 
232     @Test
233     fun singleCarrier_enablesCarrierIconsInStatusIcons() {
234         whenever(mShadeCarrierGroupController.isSingleCarrier).thenReturn(true)
235 
236         makeShadeVisible()
237 
238         verify(statusIcons).removeIgnoredSlots(carrierIconSlots)
239     }
240 
241     @Test
242     fun dualCarrier_disablesCarrierIconsInStatusIcons() {
243         whenever(mShadeCarrierGroupController.isSingleCarrier).thenReturn(false)
244 
245         makeShadeVisible()
246         shadeHeaderController.qsExpandedFraction = 1.0f
247 
248         verify(statusIcons).addIgnoredSlots(carrierIconSlots)
249     }
250 
251     @Test
252     fun dualCarrier_enablesCarrierIconsInStatusIcons_qsExpanded() {
253         whenever(mShadeCarrierGroupController.isSingleCarrier).thenReturn(false)
254 
255         makeShadeVisible()
256         shadeHeaderController.qsExpandedFraction = 0.0f
257 
258         verify(statusIcons, times(2)).removeIgnoredSlots(carrierIconSlots)
259     }
260 
261     @Test
262     fun disableQS_notDisabled_visible() {
263         makeShadeVisible()
264         shadeHeaderController.disable(0, 0, false)
265 
266         assertThat(viewVisibility).isEqualTo(View.VISIBLE)
267     }
268 
269     @Test
270     fun disableQS_disabled_gone() {
271         makeShadeVisible()
272         shadeHeaderController.disable(0, StatusBarManager.DISABLE2_QUICK_SETTINGS, false)
273 
274         assertThat(viewVisibility).isEqualTo(View.GONE)
275     }
276 
277     private fun makeShadeVisible() {
278         shadeHeaderController.largeScreenActive = true
279         shadeHeaderController.qsVisible = true
280     }
281 
282     @Test
283     fun updateConfig_changesFontStyle() {
284         configurationController.notifyDensityOrFontScaleChanged()
285 
286         verify(clock).setTextAppearance(R.style.TextAppearance_QS_Status)
287         verify(date).setTextAppearance(R.style.TextAppearance_QS_Status)
288         verify(carrierGroup).updateTextAppearance(R.style.TextAppearance_QS_Status_Carriers)
289     }
290 
291     @Test
292     fun animateOutOnStartCustomizing() {
293         val animator = mock(ViewPropertyAnimator::class.java, Answers.RETURNS_SELF)
294         val duration = 1000L
295         whenever(view.animate()).thenReturn(animator)
296 
297         shadeHeaderController.startCustomizingAnimation(show = true, duration)
298 
299         verify(animator).setDuration(duration)
300         verify(animator).alpha(0f)
301         verify(animator).setInterpolator(Interpolators.ALPHA_OUT)
302         verify(animator).start()
303     }
304 
305     @Test
306     fun animateInOnEndCustomizing() {
307         val animator = mock(ViewPropertyAnimator::class.java, Answers.RETURNS_SELF)
308         val duration = 1000L
309         whenever(view.animate()).thenReturn(animator)
310 
311         shadeHeaderController.startCustomizingAnimation(show = false, duration)
312 
313         verify(animator).setDuration(duration)
314         verify(animator).alpha(1f)
315         verify(animator).setInterpolator(Interpolators.ALPHA_IN)
316         verify(animator).start()
317     }
318 
319     @Test
320     fun customizerAnimatorChangesViewVisibility() {
321         makeShadeVisible()
322 
323         val animator = mock(ViewPropertyAnimator::class.java, Answers.RETURNS_SELF)
324         val duration = 1000L
325         whenever(view.animate()).thenReturn(animator)
326         val listenerCaptor = argumentCaptor<Animator.AnimatorListener>()
327 
328         shadeHeaderController.startCustomizingAnimation(show = true, duration)
329         verify(animator).setListener(capture(listenerCaptor))
330         // Start and end the animation
331         listenerCaptor.value.onAnimationStart(mock())
332         listenerCaptor.value.onAnimationEnd(mock())
333         assertThat(viewVisibility).isEqualTo(View.INVISIBLE)
334 
335         reset(animator)
336         shadeHeaderController.startCustomizingAnimation(show = false, duration)
337         verify(animator).setListener(capture(listenerCaptor))
338         // Start and end the animation
339         listenerCaptor.value.onAnimationStart(mock())
340         listenerCaptor.value.onAnimationEnd(mock())
341         assertThat(viewVisibility).isEqualTo(View.VISIBLE)
342     }
343 
344     @Test
345     fun animatorListenersClearedAtEnd() {
346         val animator = mock(ViewPropertyAnimator::class.java, Answers.RETURNS_SELF)
347         whenever(view.animate()).thenReturn(animator)
348 
349         shadeHeaderController.startCustomizingAnimation(show = true, 0L)
350         val listenerCaptor = argumentCaptor<Animator.AnimatorListener>()
351         verify(animator).setListener(capture(listenerCaptor))
352 
353         listenerCaptor.value.onAnimationEnd(mock())
354         verify(animator).setListener(null)
355     }
356 
357     @Test
358     fun demoMode_attachDemoMode() {
359         val cb = argumentCaptor<DemoMode>()
360         verify(demoModeController).addCallback(capture(cb))
361         cb.value.onDemoModeStarted()
362         verify(clock).onDemoModeStarted()
363     }
364 
365     @Test
366     fun demoMode_detachDemoMode() {
367         shadeHeaderController.simulateViewDetached()
368         val cb = argumentCaptor<DemoMode>()
369         verify(demoModeController).removeCallback(capture(cb))
370         cb.value.onDemoModeFinished()
371         verify(clock).onDemoModeFinished()
372     }
373 
374     @Test
375     fun testControllersCreatedAndInitialized() {
376         verify(variableDateViewController).init()
377 
378         verify(batteryMeterViewController).init()
379         verify(batteryMeterViewController).ignoreTunerUpdates()
380 
381         val inOrder = Mockito.inOrder(mShadeCarrierGroupControllerBuilder)
382         inOrder.verify(mShadeCarrierGroupControllerBuilder).setShadeCarrierGroup(carrierGroup)
383         inOrder.verify(mShadeCarrierGroupControllerBuilder).build()
384     }
385 
386     @Test
387     fun batteryModeControllerCalledWhenQsExpandedFractionChanges() {
388         whenever(qsBatteryModeController.getBatteryMode(Mockito.same(null), eq(0f)))
389             .thenReturn(BatteryMeterView.MODE_ON)
390         whenever(qsBatteryModeController.getBatteryMode(Mockito.same(null), eq(1f)))
391             .thenReturn(BatteryMeterView.MODE_ESTIMATE)
392         shadeHeaderController.qsVisible = true
393 
394         val times = 10
395         repeat(times) { shadeHeaderController.qsExpandedFraction = it / (times - 1).toFloat() }
396 
397         verify(batteryMeterView).setPercentShowMode(BatteryMeterView.MODE_ON)
398         verify(batteryMeterView).setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
399     }
400 
401     @Test
402     fun testClockPivotLtr() {
403         val width = 200
404         whenever(clock.width).thenReturn(width)
405         whenever(clock.isLayoutRtl).thenReturn(false)
406 
407         val captor = ArgumentCaptor.forClass(View.OnLayoutChangeListener::class.java)
408         verify(clock, times(2)).addOnLayoutChangeListener(capture(captor))
409 
410         captor.value.onLayoutChange(clock, 0, 1, 2, 3, 4, 5, 6, 7)
411         verify(clock).pivotX = 0f
412     }
413 
414     @Test
415     fun testClockPivotRtl() {
416         val width = 200
417         whenever(clock.width).thenReturn(width)
418         whenever(clock.isLayoutRtl).thenReturn(true)
419 
420         val captor = ArgumentCaptor.forClass(View.OnLayoutChangeListener::class.java)
421         verify(clock, times(2)).addOnLayoutChangeListener(capture(captor))
422 
423         captor.value.onLayoutChange(clock, 0, 1, 2, 3, 4, 5, 6, 7)
424         verify(clock).pivotX = width.toFloat()
425     }
426 
427     @Test
428     fun testShadeExpanded_true() {
429         // When shade is expanded, view should be visible regardless of largeScreenActive
430         shadeHeaderController.largeScreenActive = false
431         shadeHeaderController.qsVisible = true
432         assertThat(viewVisibility).isEqualTo(View.VISIBLE)
433 
434         shadeHeaderController.largeScreenActive = true
435         assertThat(viewVisibility).isEqualTo(View.VISIBLE)
436     }
437 
438     @Test
439     fun testShadeExpanded_false() {
440         // When shade is not expanded, view should be invisible regardless of largeScreenActive
441         shadeHeaderController.largeScreenActive = false
442         shadeHeaderController.qsVisible = false
443         assertThat(viewVisibility).isEqualTo(View.INVISIBLE)
444 
445         shadeHeaderController.largeScreenActive = true
446         assertThat(viewVisibility).isEqualTo(View.INVISIBLE)
447     }
448 
449     @Test
450     fun testLargeScreenActive_false() {
451         shadeHeaderController.largeScreenActive = true // Make sure there's a change
452         Mockito.clearInvocations(view)
453 
454         shadeHeaderController.largeScreenActive = false
455 
456         verify(view).setTransition(ShadeHeaderController.HEADER_TRANSITION_ID)
457     }
458 
459     @Test
460     fun testLargeScreenActive_collapseActionRun_onSystemIconsClick() {
461         shadeHeaderController.largeScreenActive = true
462         var wasRun = false
463         shadeHeaderController.shadeCollapseAction = Runnable { wasRun = true }
464 
465         systemIcons.performClick()
466 
467         assertThat(wasRun).isTrue()
468     }
469 
470     @Test
471     fun testShadeExpandedFraction() {
472         // View needs to be visible for this to actually take effect
473         shadeHeaderController.qsVisible = true
474 
475         Mockito.clearInvocations(view)
476         shadeHeaderController.shadeExpandedFraction = 0.3f
477         verify(view).alpha = ShadeInterpolation.getContentAlpha(0.3f)
478 
479         Mockito.clearInvocations(view)
480         shadeHeaderController.shadeExpandedFraction = 1f
481         verify(view).alpha = ShadeInterpolation.getContentAlpha(1f)
482 
483         Mockito.clearInvocations(view)
484         shadeHeaderController.shadeExpandedFraction = 0f
485         verify(view).alpha = ShadeInterpolation.getContentAlpha(0f)
486     }
487 
488     @Test
489     fun testQsExpandedFraction_headerTransition() {
490         shadeHeaderController.qsVisible = true
491         shadeHeaderController.largeScreenActive = false
492 
493         Mockito.clearInvocations(view)
494         shadeHeaderController.qsExpandedFraction = 0.3f
495         verify(view).progress = 0.3f
496     }
497 
498     @Test
499     fun testQsExpandedFraction_largeScreen() {
500         shadeHeaderController.qsVisible = true
501         shadeHeaderController.largeScreenActive = true
502 
503         Mockito.clearInvocations(view)
504         shadeHeaderController.qsExpandedFraction = 0.3f
505         verify(view, Mockito.never()).progress = anyFloat()
506     }
507 
508     @Test
509     fun testScrollY_headerTransition() {
510         shadeHeaderController.largeScreenActive = false
511 
512         Mockito.clearInvocations(view)
513         shadeHeaderController.qsScrollY = 20
514         verify(view).scrollY = 20
515     }
516 
517     @Test
518     fun testScrollY_largeScreen() {
519         shadeHeaderController.largeScreenActive = true
520 
521         Mockito.clearInvocations(view)
522         shadeHeaderController.qsScrollY = 20
523         verify(view, Mockito.never()).scrollY = anyInt()
524     }
525 
526     @Test
527     fun testPrivacyChipVisibilityChanged_visible_changesCorrectConstraints() {
528         val chipVisibleChanges = createMockConstraintChanges()
529         val chipNotVisibleChanges = createMockConstraintChanges()
530 
531         whenever(combinedShadeHeadersConstraintManager.privacyChipVisibilityConstraints(true))
532             .thenReturn(chipVisibleChanges)
533         whenever(combinedShadeHeadersConstraintManager.privacyChipVisibilityConstraints(false))
534             .thenReturn(chipNotVisibleChanges)
535 
536         val captor = ArgumentCaptor.forClass(ChipVisibilityListener::class.java)
537         verify(privacyIconsController).chipVisibilityListener = capture(captor)
538 
539         captor.value.onChipVisibilityRefreshed(true)
540 
541         verify(chipVisibleChanges.qqsConstraintsChanges)!!.invoke(qqsConstraints)
542         verify(chipVisibleChanges.qsConstraintsChanges)!!.invoke(qsConstraints)
543         verify(chipVisibleChanges.largeScreenConstraintsChanges)!!.invoke(largeScreenConstraints)
544 
545         verify(chipNotVisibleChanges.qqsConstraintsChanges, Mockito.never())!!.invoke(any())
546         verify(chipNotVisibleChanges.qsConstraintsChanges, Mockito.never())!!.invoke(any())
547         verify(chipNotVisibleChanges.largeScreenConstraintsChanges, Mockito.never())!!.invoke(any())
548     }
549 
550     @Test
551     fun testPrivacyChipVisibilityChanged_notVisible_changesCorrectConstraints() {
552         val chipVisibleChanges = createMockConstraintChanges()
553         val chipNotVisibleChanges = createMockConstraintChanges()
554 
555         whenever(combinedShadeHeadersConstraintManager.privacyChipVisibilityConstraints(true))
556             .thenReturn(chipVisibleChanges)
557         whenever(combinedShadeHeadersConstraintManager.privacyChipVisibilityConstraints(false))
558             .thenReturn(chipNotVisibleChanges)
559 
560         val captor = ArgumentCaptor.forClass(ChipVisibilityListener::class.java)
561         verify(privacyIconsController).chipVisibilityListener = capture(captor)
562 
563         captor.value.onChipVisibilityRefreshed(false)
564 
565         verify(chipVisibleChanges.qqsConstraintsChanges, Mockito.never())!!.invoke(qqsConstraints)
566         verify(chipVisibleChanges.qsConstraintsChanges, Mockito.never())!!.invoke(qsConstraints)
567         verify(chipVisibleChanges.largeScreenConstraintsChanges, Mockito.never())!!.invoke(
568             largeScreenConstraints
569         )
570 
571         verify(chipNotVisibleChanges.qqsConstraintsChanges)!!.invoke(any())
572         verify(chipNotVisibleChanges.qsConstraintsChanges)!!.invoke(any())
573         verify(chipNotVisibleChanges.largeScreenConstraintsChanges)!!.invoke(any())
574     }
575 
576     @Test
577     fun testInsetsGuides_ltr() {
578         whenever(view.isLayoutRtl).thenReturn(false)
579         val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
580         verify(view).setOnApplyWindowInsetsListener(capture(captor))
581         val mockConstraintsChanges = createMockConstraintChanges()
582 
583         val (insetLeft, insetRight) = 30 to 40
584         val (paddingStart, paddingEnd) = 10 to 20
585         whenever(view.paddingStart).thenReturn(paddingStart)
586         whenever(view.paddingEnd).thenReturn(paddingEnd)
587 
588         mockInsetsProvider(insetLeft to insetRight, false)
589 
590         whenever(
591                 combinedShadeHeadersConstraintManager.edgesGuidelinesConstraints(
592                     anyInt(),
593                     anyInt(),
594                     anyInt(),
595                     anyInt()
596                 )
597             )
598             .thenReturn(mockConstraintsChanges)
599 
600         captor.value.onApplyWindowInsets(view, createWindowInsets())
601 
602         verify(combinedShadeHeadersConstraintManager)
603             .edgesGuidelinesConstraints(insetLeft, paddingStart, insetRight, paddingEnd)
604 
605         verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
606         verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
607         verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
608     }
609 
610     @Test
611     fun testInsetsGuides_rtl() {
612         whenever(view.isLayoutRtl).thenReturn(true)
613         val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
614         verify(view).setOnApplyWindowInsetsListener(capture(captor))
615         val mockConstraintsChanges = createMockConstraintChanges()
616 
617         val (insetLeft, insetRight) = 30 to 40
618         val (paddingStart, paddingEnd) = 10 to 20
619         whenever(view.paddingStart).thenReturn(paddingStart)
620         whenever(view.paddingEnd).thenReturn(paddingEnd)
621 
622         mockInsetsProvider(insetLeft to insetRight, false)
623 
624         whenever(
625                 combinedShadeHeadersConstraintManager.edgesGuidelinesConstraints(
626                     anyInt(),
627                     anyInt(),
628                     anyInt(),
629                     anyInt()
630                 )
631             )
632             .thenReturn(mockConstraintsChanges)
633 
634         captor.value.onApplyWindowInsets(view, createWindowInsets())
635 
636         verify(combinedShadeHeadersConstraintManager)
637             .edgesGuidelinesConstraints(insetRight, paddingStart, insetLeft, paddingEnd)
638 
639         verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
640         verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
641         verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
642     }
643 
644     @Test
645     fun testNullCutout() {
646         val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
647         verify(view).setOnApplyWindowInsetsListener(capture(captor))
648         val mockConstraintsChanges = createMockConstraintChanges()
649 
650         whenever(combinedShadeHeadersConstraintManager.emptyCutoutConstraints())
651             .thenReturn(mockConstraintsChanges)
652 
653         captor.value.onApplyWindowInsets(view, createWindowInsets(null))
654 
655         verify(combinedShadeHeadersConstraintManager).emptyCutoutConstraints()
656         verify(combinedShadeHeadersConstraintManager, Mockito.never())
657             .centerCutoutConstraints(Mockito.anyBoolean(), anyInt())
658 
659         verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
660         verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
661         verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
662     }
663 
664     @Test
665     fun testEmptyCutout() {
666         val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
667         verify(view).setOnApplyWindowInsetsListener(capture(captor))
668         val mockConstraintsChanges = createMockConstraintChanges()
669 
670         whenever(combinedShadeHeadersConstraintManager.emptyCutoutConstraints())
671             .thenReturn(mockConstraintsChanges)
672 
673         captor.value.onApplyWindowInsets(view, createWindowInsets())
674 
675         verify(combinedShadeHeadersConstraintManager).emptyCutoutConstraints()
676         verify(combinedShadeHeadersConstraintManager, Mockito.never())
677             .centerCutoutConstraints(Mockito.anyBoolean(), anyInt())
678 
679         verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
680         verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
681         verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
682     }
683 
684     @Test
685     fun testCornerCutout_emptyRect() {
686         val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
687         verify(view).setOnApplyWindowInsetsListener(capture(captor))
688         val mockConstraintsChanges = createMockConstraintChanges()
689 
690         mockInsetsProvider(0 to 0, true)
691 
692         whenever(combinedShadeHeadersConstraintManager.emptyCutoutConstraints())
693             .thenReturn(mockConstraintsChanges)
694 
695         captor.value.onApplyWindowInsets(view, createWindowInsets())
696 
697         verify(combinedShadeHeadersConstraintManager).emptyCutoutConstraints()
698         verify(combinedShadeHeadersConstraintManager, Mockito.never())
699             .centerCutoutConstraints(Mockito.anyBoolean(), anyInt())
700 
701         verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
702         verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
703         verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
704     }
705 
706     @Test
707     fun testCornerCutout_nonEmptyRect() {
708         val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
709         verify(view).setOnApplyWindowInsetsListener(capture(captor))
710         val mockConstraintsChanges = createMockConstraintChanges()
711 
712         mockInsetsProvider(0 to 0, true)
713 
714         whenever(combinedShadeHeadersConstraintManager.emptyCutoutConstraints())
715             .thenReturn(mockConstraintsChanges)
716 
717         captor.value.onApplyWindowInsets(view, createWindowInsets(Rect(1, 2, 3, 4)))
718 
719         verify(combinedShadeHeadersConstraintManager).emptyCutoutConstraints()
720         verify(combinedShadeHeadersConstraintManager, Mockito.never())
721             .centerCutoutConstraints(Mockito.anyBoolean(), anyInt())
722 
723         verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
724         verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
725         verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
726     }
727 
728     @Test
729     fun testTopCutout_ltr() {
730         val width = 100
731         val paddingLeft = 10
732         val paddingRight = 20
733         val cutoutWidth = 30
734 
735         whenever(view.isLayoutRtl).thenReturn(false)
736         whenever(view.width).thenReturn(width)
737         whenever(view.paddingLeft).thenReturn(paddingLeft)
738         whenever(view.paddingRight).thenReturn(paddingRight)
739 
740         val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
741         verify(view).setOnApplyWindowInsetsListener(capture(captor))
742         val mockConstraintsChanges = createMockConstraintChanges()
743 
744         mockInsetsProvider(0 to 0, false)
745 
746         whenever(
747                 combinedShadeHeadersConstraintManager.centerCutoutConstraints(
748                     Mockito.anyBoolean(),
749                     anyInt()
750                 )
751             )
752             .thenReturn(mockConstraintsChanges)
753 
754         captor.value.onApplyWindowInsets(view, createWindowInsets(Rect(0, 0, cutoutWidth, 1)))
755 
756         verify(combinedShadeHeadersConstraintManager, Mockito.never()).emptyCutoutConstraints()
757         val offset = (width - paddingLeft - paddingRight - cutoutWidth) / 2
758         verify(combinedShadeHeadersConstraintManager).centerCutoutConstraints(false, offset)
759 
760         verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
761         verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
762         verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
763     }
764 
765     @Test
766     fun testTopCutout_rtl() {
767         val width = 100
768         val paddingLeft = 10
769         val paddingRight = 20
770         val cutoutWidth = 30
771 
772         whenever(view.isLayoutRtl).thenReturn(true)
773         whenever(view.width).thenReturn(width)
774         whenever(view.paddingLeft).thenReturn(paddingLeft)
775         whenever(view.paddingRight).thenReturn(paddingRight)
776 
777         val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
778         verify(view).setOnApplyWindowInsetsListener(capture(captor))
779         val mockConstraintsChanges = createMockConstraintChanges()
780 
781         mockInsetsProvider(0 to 0, false)
782 
783         whenever(
784                 combinedShadeHeadersConstraintManager.centerCutoutConstraints(
785                     Mockito.anyBoolean(),
786                     anyInt()
787                 )
788             )
789             .thenReturn(mockConstraintsChanges)
790 
791         captor.value.onApplyWindowInsets(view, createWindowInsets(Rect(0, 0, cutoutWidth, 1)))
792 
793         verify(combinedShadeHeadersConstraintManager, Mockito.never()).emptyCutoutConstraints()
794         val offset = (width - paddingLeft - paddingRight - cutoutWidth) / 2
795         verify(combinedShadeHeadersConstraintManager).centerCutoutConstraints(true, offset)
796 
797         verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
798         verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
799         verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
800     }
801 
802     @Test
803     fun alarmIconNotIgnored() {
804         verify(statusIcons, Mockito.never())
805             .addIgnoredSlot(context.getString(com.android.internal.R.string.status_bar_alarm_clock))
806     }
807 
808     @Test
809     fun privacyChipParentVisibleFromStart() {
810         verify(privacyIconsController).onParentVisible()
811     }
812 
813     @Test
814     fun privacyChipParentVisibleAlways() {
815         shadeHeaderController.largeScreenActive = true
816         shadeHeaderController.largeScreenActive = false
817         shadeHeaderController.largeScreenActive = true
818 
819         verify(privacyIconsController, Mockito.never()).onParentInvisible()
820     }
821 
822     @Test
823     fun clockPivotYInCenter() {
824         val captor = ArgumentCaptor.forClass(View.OnLayoutChangeListener::class.java)
825         verify(clock, times(2)).addOnLayoutChangeListener(capture(captor))
826         var height = 100
827         val width = 50
828 
829         clock.executeLayoutChange(0, 0, width, height, captor.value)
830         verify(clock).pivotY = height.toFloat() / 2
831 
832         height = 150
833         clock.executeLayoutChange(0, 0, width, height, captor.value)
834         verify(clock).pivotY = height.toFloat() / 2
835     }
836 
837     @Test
838     fun onDensityOrFontScaleChanged_reloadConstraints() {
839         // After density or font scale change, constraints need to be reloaded to reflect new
840         // dimensions.
841         Mockito.reset(qqsConstraints)
842         Mockito.reset(qsConstraints)
843         Mockito.reset(largeScreenConstraints)
844 
845         configurationController.notifyDensityOrFontScaleChanged()
846 
847         val captor = ArgumentCaptor.forClass(XmlResourceParser::class.java)
848         verify(qqsConstraints).load(eq(viewContext), capture(captor))
849         assertThat(captor.value.getResId()).isEqualTo(R.xml.qqs_header)
850         verify(qsConstraints).load(eq(viewContext), capture(captor))
851         assertThat(captor.value.getResId()).isEqualTo(R.xml.qs_header)
852         verify(largeScreenConstraints).load(eq(viewContext), capture(captor))
853         assertThat(captor.value.getResId()).isEqualTo(R.xml.large_screen_shade_header)
854     }
855 
856     @Test
857     fun carrierStartPaddingIsSetOnClockLayout() {
858         val clockWidth = 200
859         val maxClockScale = context.resources.getFloat(R.dimen.qqs_expand_clock_scale)
860         val expectedStartPadding = (clockWidth * maxClockScale).toInt()
861         whenever(clock.width).thenReturn(clockWidth)
862 
863         val captor = ArgumentCaptor.forClass(View.OnLayoutChangeListener::class.java)
864         verify(clock, times(2)).addOnLayoutChangeListener(capture(captor))
865         captor.allValues.forEach { clock.executeLayoutChange(0, 0, clockWidth, 0, it) }
866 
867         verify(carrierGroup).setPaddingRelative(expectedStartPadding, 0, 0, 0)
868     }
869 
870     @Test
871     fun launchClock_launchesDefaultIntentWhenNoAlarmSet() {
872         shadeHeaderController.launchClockActivity()
873 
874         verify(activityStarter).postStartActivityDismissingKeyguard(DEFAULT_CLOCK_INTENT, 0)
875     }
876 
877     @Test
878     fun launchClock_launchesNextAlarmWhenExists() {
879         val pendingIntent = mock<PendingIntent>()
880         val aci = AlarmManager.AlarmClockInfo(12345, pendingIntent)
881         val captor =
882             ArgumentCaptor.forClass(NextAlarmController.NextAlarmChangeCallback::class.java)
883 
884         verify(nextAlarmController).addCallback(capture(captor))
885         captor.value.onNextAlarmChanged(aci)
886 
887         shadeHeaderController.launchClockActivity()
888 
889         verify(activityStarter).postStartActivityDismissingKeyguard(pendingIntent)
890     }
891 
892     private fun View.executeLayoutChange(
893         left: Int,
894         top: Int,
895         right: Int,
896         bottom: Int,
897         listener: View.OnLayoutChangeListener
898     ) {
899         val oldLeft = this.left
900         val oldTop = this.top
901         val oldRight = this.right
902         val oldBottom = this.bottom
903         whenever(this.left).thenReturn(left)
904         whenever(this.top).thenReturn(top)
905         whenever(this.right).thenReturn(right)
906         whenever(this.bottom).thenReturn(bottom)
907         whenever(this.height).thenReturn(bottom - top)
908         whenever(this.width).thenReturn(right - left)
909         listener.onLayoutChange(
910             this,
911             oldLeft,
912             oldTop,
913             oldRight,
914             oldBottom,
915             left,
916             top,
917             right,
918             bottom
919         )
920     }
921 
922     private fun createWindowInsets(topCutout: Rect? = Rect()): WindowInsets {
923         val windowInsets: WindowInsets = mock()
924         val displayCutout: DisplayCutout = mock()
925         whenever(windowInsets.displayCutout)
926             .thenReturn(if (topCutout != null) displayCutout else null)
927         whenever(displayCutout.boundingRectTop).thenReturn(topCutout)
928 
929         return windowInsets
930     }
931 
932     private fun mockInsetsProvider(
933         insets: Pair<Int, Int> = 0 to 0,
934         cornerCutout: Boolean = false,
935     ) {
936         whenever(insetsProvider.getStatusBarContentInsetsForCurrentRotation())
937             .thenReturn(insets.toAndroidPair())
938         whenever(insetsProvider.currentRotationHasCornerCutout()).thenReturn(cornerCutout)
939     }
940 
941     private fun createMockConstraintChanges(): ConstraintsChanges {
942         return ConstraintsChanges(mock(), mock(), mock())
943     }
944 
945     private fun XmlResourceParser.getResId(): Int {
946         return Resources.getAttributeSetSourceResId(this)
947     }
948 
949     private fun setUpMotionLayout(motionLayout: MotionLayout) {
950         whenever(motionLayout.getConstraintSet(QQS_HEADER_CONSTRAINT)).thenReturn(qqsConstraints)
951         whenever(motionLayout.getConstraintSet(QS_HEADER_CONSTRAINT)).thenReturn(qsConstraints)
952         whenever(motionLayout.getConstraintSet(LARGE_SCREEN_HEADER_CONSTRAINT))
953             .thenReturn(largeScreenConstraints)
954     }
955 
956     private fun setUpDefaultInsets() {
957         whenever(
958                 combinedShadeHeadersConstraintManager.edgesGuidelinesConstraints(
959                     anyInt(),
960                     anyInt(),
961                     anyInt(),
962                     anyInt()
963                 )
964             )
965             .thenReturn(EMPTY_CHANGES)
966         whenever(combinedShadeHeadersConstraintManager.emptyCutoutConstraints())
967             .thenReturn(EMPTY_CHANGES)
968         whenever(
969                 combinedShadeHeadersConstraintManager.centerCutoutConstraints(
970                     Mockito.anyBoolean(),
971                     anyInt()
972                 )
973             )
974             .thenReturn(EMPTY_CHANGES)
975         whenever(
976                 combinedShadeHeadersConstraintManager.privacyChipVisibilityConstraints(
977                     Mockito.anyBoolean()
978                 )
979             )
980             .thenReturn(EMPTY_CHANGES)
981         whenever(insetsProvider.getStatusBarContentInsetsForCurrentRotation())
982             .thenReturn(Pair(0, 0).toAndroidPair())
983         whenever(insetsProvider.currentRotationHasCornerCutout()).thenReturn(false)
984         setupCurrentInsets(null)
985     }
986 
987     private fun setupCurrentInsets(cutout: DisplayCutout?) {
988         val mockedDisplay =
989             mock<Display>().also { display -> whenever(display.cutout).thenReturn(cutout) }
990         whenever(viewContext.display).thenReturn(mockedDisplay)
991     }
992 
993     private fun <T, U> Pair<T, U>.toAndroidPair(): android.util.Pair<T, U> {
994         return android.util.Pair(first, second)
995     }
996 }
997