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