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