diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt index 3a33c4b480c46..8945f360f7b82 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt @@ -22,6 +22,7 @@ import android.animation.ValueAnimator import android.app.WallpaperManager import android.view.Choreographer import android.view.View +import androidx.annotation.VisibleForTesting import androidx.dynamicanimation.animation.FloatPropertyCompat import androidx.dynamicanimation.animation.SpringAnimation import androidx.dynamicanimation.animation.SpringForce @@ -29,6 +30,7 @@ import com.android.internal.util.IndentingPrintWriter import com.android.systemui.Dumpable import com.android.systemui.Interpolators import com.android.systemui.dump.DumpManager +import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.phone.BiometricUnlockController import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK import com.android.systemui.statusbar.phone.NotificationShadeWindowController @@ -45,7 +47,7 @@ import kotlin.math.max */ @Singleton class NotificationShadeDepthController @Inject constructor( - private val statusBarStateController: SysuiStatusBarStateController, + private val statusBarStateController: StatusBarStateController, private val blurUtils: BlurUtils, private val biometricUnlockController: BiometricUnlockController, private val keyguardStateController: KeyguardStateController, @@ -56,7 +58,6 @@ class NotificationShadeDepthController @Inject constructor( ) : PanelExpansionListener, Dumpable { companion object { private const val WAKE_UP_ANIMATION_ENABLED = true - private const val SHADE_BLUR_ENABLED = true } lateinit var root: View @@ -64,7 +65,9 @@ class NotificationShadeDepthController @Inject constructor( private var keyguardAnimator: Animator? = null private var notificationAnimator: Animator? = null private var updateScheduled: Boolean = false - private val shadeSpring = SpringAnimation(this, object : + private var shadeExpansion = 0f + @VisibleForTesting + var shadeSpring = SpringAnimation(this, object : FloatPropertyCompat("shadeBlurRadius") { override fun setValue(rect: NotificationShadeDepthController?, value: Float) { shadeBlurRadius = value.toInt() @@ -75,12 +78,25 @@ class NotificationShadeDepthController @Inject constructor( } }) private val zoomInterpolator = Interpolators.ACCELERATE_DECELERATE + + /** + * Radius that we're animating to. + */ + private var pendingShadeBlurRadius = -1 + + /** + * Shade blur radius on the current frame. + */ private var shadeBlurRadius = 0 set(value) { if (field == value) return field = value scheduleUpdate() } + + /** + * Blur radius of the wake-up animation on this frame. + */ private var wakeAndUnlockBlurRadius = 0 set(value) { if (field == value) return @@ -141,6 +157,18 @@ class NotificationShadeDepthController @Inject constructor( } } + private val statusBarStateCallback = object : StatusBarStateController.StateListener { + override fun onStateChanged(newState: Int) { + updateShadeBlur() + } + + override fun onDozingChanged(isDozing: Boolean) { + if (isDozing && shadeSpring.isRunning) { + shadeSpring.skipToEnd() + } + } + } + init { dumpManager.registerDumpable(javaClass.name, this) if (WAKE_UP_ANIMATION_ENABLED) { @@ -149,24 +177,31 @@ class NotificationShadeDepthController @Inject constructor( shadeSpring.spring = SpringForce(0.0f) shadeSpring.spring.dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY shadeSpring.spring.stiffness = SpringForce.STIFFNESS_LOW + shadeSpring.addEndListener { _, _, _, _ -> pendingShadeBlurRadius = -1 } + statusBarStateController.addCallback(statusBarStateCallback) } /** * Update blurs when pulling down the shade */ override fun onPanelExpansionChanged(expansion: Float, tracking: Boolean) { - if (!SHADE_BLUR_ENABLED) { + if (expansion == shadeExpansion) { return } + shadeExpansion = expansion + updateShadeBlur() + } + private fun updateShadeBlur() { var newBlur = 0 if (statusBarStateController.state == StatusBarState.SHADE) { - newBlur = blurUtils.blurRadiusOfRatio(expansion) + newBlur = blurUtils.blurRadiusOfRatio(shadeExpansion) } - if (shadeBlurRadius == newBlur) { + if (pendingShadeBlurRadius == newBlur) { return } + pendingShadeBlurRadius = newBlur shadeSpring.animateToFinalPosition(newBlur.toFloat()) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt new file mode 100644 index 0000000000000..f061f34072d04 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar + +import android.app.WallpaperManager +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.view.Choreographer +import android.view.View +import android.view.ViewRootImpl +import androidx.dynamicanimation.animation.SpringAnimation +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.dump.DumpManager +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.phone.BiometricUnlockController +import com.android.systemui.statusbar.phone.NotificationShadeWindowController +import com.android.systemui.statusbar.policy.KeyguardStateController +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mock +import org.mockito.Mockito.* +import org.mockito.junit.MockitoJUnit + +@RunWith(AndroidTestingRunner::class) +@RunWithLooper +@SmallTest +class NotificationShadeDepthControllerTest : SysuiTestCase() { + + @Mock private lateinit var statusBarStateController: StatusBarStateController + @Mock private lateinit var blurUtils: BlurUtils + @Mock private lateinit var biometricUnlockController: BiometricUnlockController + @Mock private lateinit var keyguardStateController: KeyguardStateController + @Mock private lateinit var choreographer: Choreographer + @Mock private lateinit var wallpaperManager: WallpaperManager + @Mock private lateinit var notificationShadeWindowController: NotificationShadeWindowController + @Mock private lateinit var dumpManager: DumpManager + @Mock private lateinit var root: View + @Mock private lateinit var viewRootImpl: ViewRootImpl + @Mock private lateinit var shadeSpring: SpringAnimation + @JvmField @Rule val mockitoRule = MockitoJUnit.rule() + + private lateinit var statusBarStateListener: StatusBarStateController.StateListener + private var statusBarState = StatusBarState.SHADE + private val maxBlur = 150 + private lateinit var notificationShadeDepthController: NotificationShadeDepthController + + @Before + fun setup() { + `when`(root.viewRootImpl).thenReturn(viewRootImpl) + `when`(statusBarStateController.state).then { statusBarState } + `when`(blurUtils.blurRadiusOfRatio(anyFloat())).then { answer -> + (answer.arguments[0] as Float * maxBlur).toInt() + } + notificationShadeDepthController = NotificationShadeDepthController( + statusBarStateController, blurUtils, biometricUnlockController, + keyguardStateController, choreographer, wallpaperManager, + notificationShadeWindowController, dumpManager) + notificationShadeDepthController.shadeSpring = shadeSpring + notificationShadeDepthController.root = root + + val captor = ArgumentCaptor.forClass(StatusBarStateController.StateListener::class.java) + verify(statusBarStateController).addCallback(captor.capture()) + statusBarStateListener = captor.value + } + + @Test + fun setupListeners() { + verify(dumpManager).registerDumpable(anyString(), safeEq(notificationShadeDepthController)) + } + + @Test + fun onPanelExpansionChanged_apliesBlur_ifShade() { + notificationShadeDepthController.onPanelExpansionChanged(1f /* expansion */, + false /* tracking */) + verify(shadeSpring).animateToFinalPosition(eq(maxBlur.toFloat())) + } + + @Test + fun onStateChanged_reevalutesBlurs_ifSameRadiusAndNewState() { + onPanelExpansionChanged_apliesBlur_ifShade() + clearInvocations(shadeSpring) + + statusBarState = StatusBarState.KEYGUARD + statusBarStateListener.onStateChanged(statusBarState) + verify(shadeSpring).animateToFinalPosition(eq(0f)) + } + + @Test + fun updateGlobalDialogVisibility_schedulesUpdate() { + notificationShadeDepthController.updateGlobalDialogVisibility(0.5f, root) + verify(choreographer).postFrameCallback(any()) + } + + private fun safeEq(value: T): T { + return eq(value) ?: value + } +} \ No newline at end of file