1 /*
<lambda>null2  * Copyright (C) 2020 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.statusbar
18 
19 import android.os.IBinder
20 import android.testing.TestableLooper.RunWithLooper
21 import android.view.Choreographer
22 import android.view.View
23 import android.view.ViewRootImpl
24 import androidx.test.ext.junit.runners.AndroidJUnit4
25 import androidx.test.filters.SmallTest
26 import com.android.systemui.SysuiTestCase
27 import com.android.systemui.animation.ShadeInterpolation
28 import com.android.systemui.dump.DumpManager
29 import com.android.systemui.plugins.statusbar.StatusBarStateController
30 import com.android.systemui.res.R
31 import com.android.systemui.shade.ShadeExpansionChangeEvent
32 import com.android.systemui.statusbar.phone.BiometricUnlockController
33 import com.android.systemui.statusbar.phone.DozeParameters
34 import com.android.systemui.statusbar.phone.ScrimController
35 import com.android.systemui.statusbar.policy.FakeConfigurationController
36 import com.android.systemui.statusbar.policy.KeyguardStateController
37 import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController
38 import com.android.systemui.util.WallpaperController
39 import com.android.systemui.util.mockito.eq
40 import com.google.common.truth.Truth.assertThat
41 import java.util.function.Consumer
42 import org.junit.Before
43 import org.junit.Rule
44 import org.junit.Test
45 import org.junit.runner.RunWith
46 import org.mockito.ArgumentCaptor
47 import org.mockito.ArgumentMatchers.anyInt
48 import org.mockito.ArgumentMatchers.floatThat
49 import org.mockito.Captor
50 import org.mockito.Mock
51 import org.mockito.Mockito
52 import org.mockito.Mockito.any
53 import org.mockito.Mockito.anyFloat
54 import org.mockito.Mockito.anyString
55 import org.mockito.Mockito.clearInvocations
56 import org.mockito.Mockito.never
57 import org.mockito.Mockito.reset
58 import org.mockito.Mockito.verify
59 import org.mockito.Mockito.`when`
60 import org.mockito.junit.MockitoJUnit
61 
62 @RunWith(AndroidJUnit4::class)
63 @RunWithLooper
64 @SmallTest
65 class NotificationShadeDepthControllerTest : SysuiTestCase() {
66 
67     @Mock private lateinit var statusBarStateController: StatusBarStateController
68     @Mock private lateinit var blurUtils: BlurUtils
69     @Mock private lateinit var biometricUnlockController: BiometricUnlockController
70     @Mock private lateinit var keyguardStateController: KeyguardStateController
71     @Mock private lateinit var choreographer: Choreographer
72     @Mock private lateinit var wallpaperController: WallpaperController
73     @Mock private lateinit var notificationShadeWindowController: NotificationShadeWindowController
74     @Mock private lateinit var dumpManager: DumpManager
75     @Mock private lateinit var root: View
76     @Mock private lateinit var viewRootImpl: ViewRootImpl
77     @Mock private lateinit var windowToken: IBinder
78     @Mock private lateinit var shadeAnimation: NotificationShadeDepthController.DepthAnimation
79     @Mock private lateinit var brightnessSpring: NotificationShadeDepthController.DepthAnimation
80     @Mock private lateinit var listener: NotificationShadeDepthController.DepthListener
81     @Mock private lateinit var dozeParameters: DozeParameters
82     @Captor private lateinit var scrimVisibilityCaptor: ArgumentCaptor<Consumer<Int>>
83     @JvmField @Rule val mockitoRule = MockitoJUnit.rule()
84 
85     private lateinit var statusBarStateListener: StatusBarStateController.StateListener
86     private var statusBarState = StatusBarState.SHADE
87     private val maxBlur = 150
88     private lateinit var notificationShadeDepthController: NotificationShadeDepthController
89     private val configurationController = FakeConfigurationController()
90 
91     @Before
92     fun setup() {
93         `when`(root.viewRootImpl).thenReturn(viewRootImpl)
94         `when`(root.windowToken).thenReturn(windowToken)
95         `when`(root.isAttachedToWindow).thenReturn(true)
96         `when`(statusBarStateController.state).then { statusBarState }
97         `when`(blurUtils.blurRadiusOfRatio(anyFloat())).then { answer ->
98             answer.arguments[0] as Float * maxBlur.toFloat()
99         }
100         `when`(blurUtils.ratioOfBlurRadius(anyFloat())).then { answer ->
101             answer.arguments[0] as Float / maxBlur.toFloat()
102         }
103         `when`(blurUtils.supportsBlursOnWindows()).thenReturn(true)
104         `when`(blurUtils.maxBlurRadius).thenReturn(maxBlur)
105         `when`(blurUtils.maxBlurRadius).thenReturn(maxBlur)
106 
107         notificationShadeDepthController =
108             NotificationShadeDepthController(
109                 statusBarStateController,
110                 blurUtils,
111                 biometricUnlockController,
112                 keyguardStateController,
113                 choreographer,
114                 wallpaperController,
115                 notificationShadeWindowController,
116                 dozeParameters,
117                 context,
118                     ResourcesSplitShadeStateController(),
119                 dumpManager,
120                 configurationController,)
121         notificationShadeDepthController.shadeAnimation = shadeAnimation
122         notificationShadeDepthController.brightnessMirrorSpring = brightnessSpring
123         notificationShadeDepthController.root = root
124 
125         val captor = ArgumentCaptor.forClass(StatusBarStateController.StateListener::class.java)
126         verify(statusBarStateController).addCallback(captor.capture())
127         statusBarStateListener = captor.value
128         verify(notificationShadeWindowController)
129             .setScrimsVisibilityListener(scrimVisibilityCaptor.capture())
130 
131         disableSplitShade()
132     }
133 
134     @Test
135     fun setupListeners() {
136         verify(dumpManager).registerCriticalDumpable(
137             anyString(), eq(notificationShadeDepthController)
138         )
139     }
140 
141     @Test
142     fun onPanelExpansionChanged_apliesBlur_ifShade() {
143         notificationShadeDepthController.onPanelExpansionChanged(
144             ShadeExpansionChangeEvent(
145                 fraction = 1f, expanded = true, tracking = false))
146         verify(shadeAnimation).animateTo(eq(maxBlur))
147     }
148 
149     @Test
150     fun onPanelExpansionChanged_animatesBlurIn_ifShade() {
151         notificationShadeDepthController.onPanelExpansionChanged(
152             ShadeExpansionChangeEvent(
153                 fraction = 0.01f, expanded = false, tracking = false))
154         verify(shadeAnimation).animateTo(eq(maxBlur))
155     }
156 
157     @Test
158     fun onPanelExpansionChanged_animatesBlurOut_ifShade() {
159         onPanelExpansionChanged_animatesBlurIn_ifShade()
160         clearInvocations(shadeAnimation)
161         notificationShadeDepthController.onPanelExpansionChanged(
162             ShadeExpansionChangeEvent(
163                 fraction = 0f, expanded = false, tracking = false))
164         verify(shadeAnimation).animateTo(eq(0))
165     }
166 
167     @Test
168     fun onPanelExpansionChanged_animatesBlurOut_ifFlick() {
169         val event =
170             ShadeExpansionChangeEvent(
171                 fraction = 1f, expanded = true, tracking = false)
172         onPanelExpansionChanged_apliesBlur_ifShade()
173         clearInvocations(shadeAnimation)
174         notificationShadeDepthController.onPanelExpansionChanged(event)
175         verify(shadeAnimation, never()).animateTo(anyInt())
176 
177         notificationShadeDepthController.onPanelExpansionChanged(
178             event.copy(fraction = 0.9f, tracking = true))
179         verify(shadeAnimation, never()).animateTo(anyInt())
180 
181         notificationShadeDepthController.onPanelExpansionChanged(
182             event.copy(fraction = 0.8f, tracking = false))
183         verify(shadeAnimation).animateTo(eq(0))
184     }
185 
186     @Test
187     fun onPanelExpansionChanged_animatesBlurIn_ifFlickCancelled() {
188         onPanelExpansionChanged_animatesBlurOut_ifFlick()
189         clearInvocations(shadeAnimation)
190         notificationShadeDepthController.onPanelExpansionChanged(
191             ShadeExpansionChangeEvent(
192                 fraction = 0.6f, expanded = true, tracking = true))
193         verify(shadeAnimation).animateTo(eq(maxBlur))
194     }
195 
196     @Test
197     fun onPanelExpansionChanged_respectsMinPanelPullDownFraction() {
198         val event =
199             ShadeExpansionChangeEvent(
200                 fraction = 0.5f, expanded = true, tracking = true)
201         notificationShadeDepthController.panelPullDownMinFraction = 0.5f
202         notificationShadeDepthController.onPanelExpansionChanged(event)
203         assertThat(notificationShadeDepthController.shadeExpansion).isEqualTo(0f)
204 
205         notificationShadeDepthController.onPanelExpansionChanged(event.copy(fraction = 0.75f))
206         assertThat(notificationShadeDepthController.shadeExpansion).isEqualTo(0.5f)
207 
208         notificationShadeDepthController.onPanelExpansionChanged(event.copy(fraction = 1f))
209         assertThat(notificationShadeDepthController.shadeExpansion).isEqualTo(1f)
210     }
211 
212     @Test
213     fun onStateChanged_reevalutesBlurs_ifSameRadiusAndNewState() {
214         onPanelExpansionChanged_apliesBlur_ifShade()
215         clearInvocations(choreographer)
216 
217         statusBarState = StatusBarState.KEYGUARD
218         statusBarStateListener.onStateChanged(statusBarState)
219         verify(shadeAnimation).animateTo(eq(0))
220     }
221 
222     @Test
223     fun setQsPanelExpansion_appliesBlur() {
224         statusBarState = StatusBarState.KEYGUARD
225         notificationShadeDepthController.qsPanelExpansion = 1f
226         notificationShadeDepthController.onPanelExpansionChanged(
227             ShadeExpansionChangeEvent(
228                 fraction = 1f, expanded = true, tracking = false))
229         notificationShadeDepthController.updateBlurCallback.doFrame(0)
230         verify(blurUtils).applyBlur(any(), eq(maxBlur), eq(false))
231     }
232 
233     @Test
234     fun setQsPanelExpansion_easing() {
235         statusBarState = StatusBarState.KEYGUARD
236         notificationShadeDepthController.qsPanelExpansion = 0.25f
237         notificationShadeDepthController.onPanelExpansionChanged(
238             ShadeExpansionChangeEvent(
239                 fraction = 1f, expanded = true, tracking = false))
240         notificationShadeDepthController.updateBlurCallback.doFrame(0)
241         verify(wallpaperController)
242             .setNotificationShadeZoom(eq(ShadeInterpolation.getNotificationScrimAlpha(0.25f)))
243     }
244 
245     @Test
246     fun expandPanel_inSplitShade_setsZoomToZero() {
247         enableSplitShade()
248 
249         notificationShadeDepthController.onPanelExpansionChanged(
250             ShadeExpansionChangeEvent(
251                 fraction = 1f, expanded = true, tracking = false))
252         notificationShadeDepthController.updateBlurCallback.doFrame(0)
253 
254         verify(wallpaperController).setNotificationShadeZoom(0f)
255     }
256 
257     @Test
258     fun expandPanel_notInSplitShade_setsZoomValue() {
259         disableSplitShade()
260 
261         notificationShadeDepthController.onPanelExpansionChanged(
262             ShadeExpansionChangeEvent(
263                 fraction = 1f, expanded = true, tracking = false))
264         notificationShadeDepthController.updateBlurCallback.doFrame(0)
265 
266         verify(wallpaperController).setNotificationShadeZoom(floatThat { it > 0 })
267     }
268 
269     @Test
270     fun expandPanel_splitShadeEnabledChanged_setsCorrectZoomValueAfterChange() {
271         disableSplitShade()
272         val rawFraction = 1f
273         val expanded = true
274         val tracking = false
275         val dragDownPxAmount = 0f
276         val event = ShadeExpansionChangeEvent(rawFraction, expanded, tracking)
277         val inOrder = Mockito.inOrder(wallpaperController)
278 
279         notificationShadeDepthController.onPanelExpansionChanged(event)
280         notificationShadeDepthController.updateBlurCallback.doFrame(0)
281         inOrder.verify(wallpaperController).setNotificationShadeZoom(floatThat { it > 0 })
282 
283         enableSplitShade()
284         notificationShadeDepthController.onPanelExpansionChanged(event)
285         notificationShadeDepthController.updateBlurCallback.doFrame(0)
286         inOrder.verify(wallpaperController).setNotificationShadeZoom(0f)
287     }
288 
289     @Test
290     fun setFullShadeTransition_appliesBlur() {
291         notificationShadeDepthController.transitionToFullShadeProgress = 1f
292         notificationShadeDepthController.updateBlurCallback.doFrame(0)
293         verify(blurUtils).applyBlur(any(), eq(maxBlur), eq(false))
294     }
295 
296     @Test
297     fun onDozeAmountChanged_appliesBlur() {
298         statusBarStateListener.onDozeAmountChanged(1f, 1f)
299         notificationShadeDepthController.updateBlurCallback.doFrame(0)
300         verify(blurUtils).applyBlur(any(), eq(maxBlur), eq(false))
301     }
302 
303     @Test
304     fun setFullShadeTransition_appliesBlur_onlyIfSupported() {
305         reset(blurUtils)
306         `when`(blurUtils.blurRadiusOfRatio(anyFloat())).then { answer ->
307             answer.arguments[0] as Float * maxBlur
308         }
309         `when`(blurUtils.ratioOfBlurRadius(anyFloat())).then { answer ->
310             answer.arguments[0] as Float / maxBlur.toFloat()
311         }
312         `when`(blurUtils.maxBlurRadius).thenReturn(maxBlur)
313         `when`(blurUtils.maxBlurRadius).thenReturn(maxBlur)
314 
315         notificationShadeDepthController.transitionToFullShadeProgress = 1f
316         notificationShadeDepthController.updateBlurCallback.doFrame(0)
317         verify(blurUtils).applyBlur(any(), eq(0), eq(false))
318         verify(wallpaperController).setNotificationShadeZoom(eq(1f))
319     }
320 
321     @Test
322     fun updateBlurCallback_setsBlurAndZoom() {
323         notificationShadeDepthController.addListener(listener)
324         notificationShadeDepthController.updateBlurCallback.doFrame(0)
325         verify(wallpaperController).setNotificationShadeZoom(anyFloat())
326         verify(listener).onWallpaperZoomOutChanged(anyFloat())
327         verify(blurUtils).applyBlur(any(), anyInt(), eq(false))
328     }
329 
330     @Test
331     fun updateBlurCallback_setsOpaque_whenScrim() {
332         scrimVisibilityCaptor.value.accept(ScrimController.OPAQUE)
333         notificationShadeDepthController.updateBlurCallback.doFrame(0)
334         verify(blurUtils).applyBlur(any(), anyInt(), eq(true))
335     }
336 
337     @Test
338     fun updateBlurCallback_setsBlur_whenExpanded() {
339         notificationShadeDepthController.onPanelExpansionChanged(
340             ShadeExpansionChangeEvent(
341                 fraction = 1f, expanded = true, tracking = false))
342         `when`(shadeAnimation.radius).thenReturn(maxBlur.toFloat())
343         notificationShadeDepthController.updateBlurCallback.doFrame(0)
344         verify(blurUtils).applyBlur(any(), eq(maxBlur), eq(false))
345     }
346 
347     @Test
348     fun updateBlurCallback_ignoreShadeBlurUntilHidden_overridesZoom() {
349         notificationShadeDepthController.onPanelExpansionChanged(
350             ShadeExpansionChangeEvent(
351                 fraction = 1f, expanded = true, tracking = false))
352         `when`(shadeAnimation.radius).thenReturn(maxBlur.toFloat())
353         notificationShadeDepthController.blursDisabledForAppLaunch = true
354         notificationShadeDepthController.updateBlurCallback.doFrame(0)
355         verify(blurUtils).applyBlur(any(), eq(0), eq(false))
356     }
357 
358     @Test
359     fun ignoreShadeBlurUntilHidden_schedulesFrame() {
360         notificationShadeDepthController.blursDisabledForAppLaunch = true
361         verify(blurUtils).prepareBlur(any(), anyInt())
362         verify(choreographer)
363             .postFrameCallback(eq(notificationShadeDepthController.updateBlurCallback))
364     }
365 
366     @Test
367     fun ignoreBlurForUnlock_ignores() {
368         notificationShadeDepthController.onPanelExpansionChanged(
369             ShadeExpansionChangeEvent(
370                 fraction = 1f, expanded = true, tracking = false))
371         `when`(shadeAnimation.radius).thenReturn(maxBlur.toFloat())
372 
373         notificationShadeDepthController.blursDisabledForAppLaunch = false
374         notificationShadeDepthController.blursDisabledForUnlock = true
375 
376         notificationShadeDepthController.updateBlurCallback.doFrame(0)
377 
378         // Since we are ignoring blurs for unlock, we should be applying blur = 0 despite setting it
379         // to maxBlur above.
380         verify(blurUtils).applyBlur(any(), eq(0), eq(false))
381     }
382 
383     @Test
384     fun ignoreBlurForUnlock_doesNotIgnore() {
385         notificationShadeDepthController.onPanelExpansionChanged(
386             ShadeExpansionChangeEvent(
387                 fraction = 1f, expanded = true, tracking = false))
388         `when`(shadeAnimation.radius).thenReturn(maxBlur.toFloat())
389 
390         notificationShadeDepthController.blursDisabledForAppLaunch = false
391         notificationShadeDepthController.blursDisabledForUnlock = false
392 
393         notificationShadeDepthController.updateBlurCallback.doFrame(0)
394 
395         // Since we are not ignoring blurs for unlock (or app launch), we should apply the blur we
396         // returned above (maxBlur).
397         verify(blurUtils).applyBlur(any(), eq(maxBlur), eq(false))
398     }
399 
400     @Test
401     fun brightnessMirrorVisible_whenVisible() {
402         notificationShadeDepthController.brightnessMirrorVisible = true
403         verify(brightnessSpring).animateTo(eq(maxBlur))
404     }
405 
406     @Test
407     fun brightnessMirrorVisible_whenHidden() {
408         notificationShadeDepthController.brightnessMirrorVisible = false
409         verify(brightnessSpring).animateTo(eq(0))
410     }
411 
412     @Test
413     fun brightnessMirror_hidesShadeBlur() {
414         // Brightness mirror is fully visible
415         `when`(brightnessSpring.ratio).thenReturn(1f)
416         // And shade is blurred
417         notificationShadeDepthController.onPanelExpansionChanged(
418             ShadeExpansionChangeEvent(
419                 fraction = 1f, expanded = true, tracking = false))
420         `when`(shadeAnimation.radius).thenReturn(maxBlur.toFloat())
421 
422         notificationShadeDepthController.updateBlurCallback.doFrame(0)
423         verify(notificationShadeWindowController).setBackgroundBlurRadius(eq(0))
424         verify(wallpaperController).setNotificationShadeZoom(eq(1f))
425         verify(blurUtils).prepareBlur(any(), eq(0))
426         verify(blurUtils).applyBlur(eq(viewRootImpl), eq(0), eq(false))
427     }
428 
429     @Test
430     fun ignoreShadeBlurUntilHidden_whennNull_ignoresIfShadeHasNoBlur() {
431         `when`(shadeAnimation.radius).thenReturn(0f)
432         notificationShadeDepthController.blursDisabledForAppLaunch = true
433         verify(shadeAnimation, never()).animateTo(anyInt())
434     }
435 
436     private fun enableSplitShade() {
437         setSplitShadeEnabled(true)
438     }
439 
440     private fun disableSplitShade() {
441         setSplitShadeEnabled(false)
442     }
443 
444     private fun setSplitShadeEnabled(enabled: Boolean) {
445         overrideResource(R.bool.config_use_split_notification_shade, enabled)
446         configurationController.notifyConfigurationChanged()
447     }
448 }
449