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