diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java index ea366eaf45681..ffded659c2acc 100644 --- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java @@ -652,7 +652,7 @@ public class LockIconViewController extends ViewController impleme // pre-emptively set to true to hide view mIsBouncerShowing = true; if (mUdfpsSupported && mShowUnlockIcon && mAuthRippleController != null) { - mAuthRippleController.showRipple(FINGERPRINT); + mAuthRippleController.showUnlockRipple(FINGERPRINT); } updateVisibility(); if (mOnGestureDetectedRunnable != null) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt index b811c51c952b2..0cde745d2e128 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt @@ -52,7 +52,11 @@ import javax.inject.Inject import javax.inject.Provider /*** - * Controls the ripple effect that shows when authentication is successful. + * Controls two ripple effects: + * 1. Unlocked ripple: shows when authentication is successful + * 2. UDFPS dwell ripple: shows when the user has their finger down on the UDFPS area and reacts + * to errors and successes + * * The ripple uses the accent color of the current theme. */ @CentralSurfacesScope @@ -115,7 +119,7 @@ class AuthRippleController @Inject constructor( notificationShadeWindowController.setForcePluginOpen(false, this) } - fun showRipple(biometricSourceType: BiometricSourceType?) { + fun showUnlockRipple(biometricSourceType: BiometricSourceType?) { if (!(keyguardUpdateMonitor.isKeyguardVisible || keyguardUpdateMonitor.isDreaming) || keyguardUpdateMonitor.userNeedsStrongAuth()) { return @@ -252,11 +256,16 @@ class AuthRippleController @Inject constructor( biometricSourceType: BiometricSourceType?, isStrongBiometric: Boolean ) { - showRipple(biometricSourceType) + if (biometricSourceType == BiometricSourceType.FINGERPRINT) { + mView.fadeDwellRipple() + } + showUnlockRipple(biometricSourceType) } override fun onBiometricAuthFailed(biometricSourceType: BiometricSourceType?) { - mView.retractRipple() + if (biometricSourceType == BiometricSourceType.FINGERPRINT) { + mView.retractDwellRipple() + } } override fun onBiometricAcquired( @@ -264,8 +273,16 @@ class AuthRippleController @Inject constructor( acquireInfo: Int ) { if (biometricSourceType == BiometricSourceType.FINGERPRINT && - acquireInfo == BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_PARTIAL) { - mView.retractRipple() + BiometricFingerprintConstants.shouldTurnOffHbm(acquireInfo) && + acquireInfo != BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_GOOD) { + // received an 'acquiredBad' message, so immediately retract + mView.retractDwellRipple() + } + } + + override fun onKeyguardBouncerChanged(bouncerIsOrWillBeShowing: Boolean) { + if (bouncerIsOrWillBeShowing) { + mView.fadeDwellRipple() } } } @@ -294,7 +311,7 @@ class AuthRippleController @Inject constructor( } override fun onFingerUp() { - mView.retractRipple() + mView.retractDwellRipple() } } @@ -337,12 +354,12 @@ class AuthRippleController @Inject constructor( "fingerprint" -> { updateSensorLocation() pw.println("fingerprint ripple sensorLocation=$fingerprintSensorLocation") - showRipple(BiometricSourceType.FINGERPRINT) + showUnlockRipple(BiometricSourceType.FINGERPRINT) } "face" -> { updateSensorLocation() pw.println("face ripple sensorLocation=$faceSensorLocation") - showRipple(BiometricSourceType.FACE) + showUnlockRipple(BiometricSourceType.FACE) } "custom" -> { if (args.size != 3 || diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt index d67363079e171..378ae14f0327d 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt @@ -35,12 +35,13 @@ import com.android.systemui.statusbar.charging.RippleShader private const val RIPPLE_SPARKLE_STRENGTH: Float = 0.4f /** - * Expanding ripple effect - * - startUnlockedRipple for the transition from biometric authentication success to showing - * launcher. - * - startDwellRipple for the ripple expansion out when the user has their finger down on the UDFPS - * sensor area - * - retractRipple for the ripple animation inwards to signal a failure + * Handles two ripple effects: dwell ripple and unlocked ripple + * Dwell Ripple: + * - startDwellRipple: dwell ripple expands outwards around the biometric area + * - retractDwellRipple: retracts the dwell ripple to radius 0 to signal a failure + * - fadeDwellRipple: fades the dwell ripple away to alpha 0 + * Unlocked ripple: + * - startUnlockedRipple: ripple expands from biometric auth location to the edges of the screen */ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) { private val retractInterpolator = PathInterpolator(.05f, .93f, .1f, 1f) @@ -52,6 +53,7 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at private var drawRipple: Boolean = false private var lockScreenColorVal = Color.WHITE + private val fadeDuration = 83L private val retractDuration = 400L private var alphaInDuration: Long = 0 private var unlockedRippleInProgress: Boolean = false @@ -59,7 +61,8 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at private val dwellPaint = Paint() private val rippleShader = RippleShader() private val ripplePaint = Paint() - private var retractAnimator: Animator? = null + private var fadeDwellAnimator: Animator? = null + private var retractDwellAnimator: Animator? = null private var dwellPulseOutAnimator: Animator? = null private var dwellRadius: Float = 0f set(value) { @@ -112,15 +115,15 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at } /** - * Animate ripple inwards back to radius 0 + * Animate dwell ripple inwards back to radius 0 */ - fun retractRipple() { - if (retractAnimator?.isRunning == true) { + fun retractDwellRipple() { + if (retractDwellAnimator?.isRunning == true || fadeDwellAnimator?.isRunning == true) { return // let the animation finish } if (dwellPulseOutAnimator?.isRunning == true) { - val retractRippleAnimator = ValueAnimator.ofFloat(dwellShader.progress, 0f) + val retractDwellRippleAnimator = ValueAnimator.ofFloat(dwellShader.progress, 0f) .apply { interpolator = retractInterpolator duration = retractDuration @@ -145,8 +148,8 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at } } - retractAnimator = AnimatorSet().apply { - playTogether(retractRippleAnimator, retractAlphaAnimator) + retractDwellAnimator = AnimatorSet().apply { + playTogether(retractDwellRippleAnimator, retractAlphaAnimator) addListener(object : AnimatorListenerAdapter() { override fun onAnimationStart(animation: Animator?) { dwellPulseOutAnimator?.cancel() @@ -163,6 +166,42 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at } } + /** + * Animate ripple fade to alpha=0 + */ + fun fadeDwellRipple() { + if (fadeDwellAnimator?.isRunning == true) { + return // let the animation finish + } + + if (dwellPulseOutAnimator?.isRunning == true || retractDwellAnimator?.isRunning == true) { + fadeDwellAnimator = ValueAnimator.ofInt(Color.alpha(dwellShader.color), 0).apply { + interpolator = Interpolators.LINEAR + duration = fadeDuration + addUpdateListener { animator -> + dwellShader.color = ColorUtils.setAlphaComponent( + dwellShader.color, + animator.animatedValue as Int + ) + invalidate() + } + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator?) { + retractDwellAnimator?.cancel() + dwellPulseOutAnimator?.cancel() + drawDwell = true + } + + override fun onAnimationEnd(animation: Animator?) { + drawDwell = false + resetDwellAlpha() + } + }) + start() + } + } + } + /** * Plays a ripple animation that grows to the dwellRadius with distortion. */ @@ -205,7 +244,8 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at ) addListener(object : AnimatorListenerAdapter() { override fun onAnimationStart(animation: Animator?) { - retractAnimator?.cancel() + retractDwellAnimator?.cancel() + fadeDwellAnimator?.cancel() visibility = VISIBLE drawDwell = true } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt index a95da6295350e..5440b45a778f0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt @@ -299,7 +299,7 @@ class AuthRippleControllerTest : SysuiTestCase() { `when`(keyguardUpdateMonitor.isKeyguardVisible).thenReturn(true) `when`(biometricUnlockController.isWakeAndUnlock).thenReturn(true) - controller.showRipple(BiometricSourceType.FINGERPRINT) + controller.showUnlockRipple(BiometricSourceType.FINGERPRINT) assertTrue("reveal didn't start on keyguardFadingAway", controller.startLightRevealScrimOnKeyguardFadingAway) `when`(keyguardStateController.isKeyguardFadingAway).thenReturn(true)