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