/* * Copyright (C) 2021 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.biometrics import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ValueAnimator import android.content.Context import android.graphics.Point import android.hardware.biometrics.BiometricFingerprintConstants import android.hardware.biometrics.BiometricSourceType import android.util.DisplayMetrics import androidx.annotation.VisibleForTesting import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.keyguard.logging.KeyguardLogger import com.android.settingslib.Utils import com.android.settingslib.udfps.UdfpsOverlayParams import com.android.systemui.CoreStartable import com.android.systemui.R import com.android.app.animation.Interpolators import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.log.core.LogLevel import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.CircleReveal import com.android.systemui.statusbar.LiftReveal import com.android.systemui.statusbar.LightRevealEffect import com.android.systemui.statusbar.LightRevealScrim import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.statusbar.commandline.Command import com.android.systemui.statusbar.commandline.CommandRegistry import com.android.systemui.statusbar.phone.BiometricUnlockController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.ViewController import java.io.PrintWriter import javax.inject.Inject import javax.inject.Provider /** * 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. */ @SysUISingleton class AuthRippleController @Inject constructor( private val sysuiContext: Context, private val authController: AuthController, private val configurationController: ConfigurationController, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, private val keyguardStateController: KeyguardStateController, private val wakefulnessLifecycle: WakefulnessLifecycle, private val commandRegistry: CommandRegistry, private val notificationShadeWindowController: NotificationShadeWindowController, private val udfpsControllerProvider: Provider, private val statusBarStateController: StatusBarStateController, private val displayMetrics: DisplayMetrics, private val featureFlags: FeatureFlags, private val logger: KeyguardLogger, private val biometricUnlockController: BiometricUnlockController, private val lightRevealScrim: LightRevealScrim, rippleView: AuthRippleView? ) : ViewController(rippleView), CoreStartable, KeyguardStateController.Callback, WakefulnessLifecycle.Observer { @VisibleForTesting internal var startLightRevealScrimOnKeyguardFadingAway = false var lightRevealScrimAnimator: ValueAnimator? = null var fingerprintSensorLocation: Point? = null private var faceSensorLocation: Point? = null private var circleReveal: LightRevealEffect? = null private var udfpsController: UdfpsController? = null private var udfpsRadius: Float = -1f override fun start() { init() } @VisibleForTesting public override fun onViewAttached() { authController.addCallback(authControllerCallback) updateRippleColor() updateUdfpsDependentParams() udfpsController?.addCallback(udfpsControllerCallback) configurationController.addCallback(configurationChangedListener) keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) keyguardStateController.addCallback(this) wakefulnessLifecycle.addObserver(this) commandRegistry.registerCommand("auth-ripple") { AuthRippleCommand() } biometricUnlockController.addListener(biometricModeListener) } private val biometricModeListener = object : BiometricUnlockController.BiometricUnlockEventsListener { override fun onBiometricUnlockedWithKeyguardDismissal( biometricSourceType: BiometricSourceType? ) { if (biometricSourceType != null) { showUnlockRipple(biometricSourceType) } else { logger.log(TAG, LogLevel.ERROR, "Unexpected scenario where biometricSourceType is null") } } } @VisibleForTesting public override fun onViewDetached() { udfpsController?.removeCallback(udfpsControllerCallback) authController.removeCallback(authControllerCallback) keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback) configurationController.removeCallback(configurationChangedListener) keyguardStateController.removeCallback(this) wakefulnessLifecycle.removeObserver(this) commandRegistry.unregisterCommand("auth-ripple") biometricUnlockController.removeListener(biometricModeListener) notificationShadeWindowController.setForcePluginOpen(false, this) } fun showUnlockRipple(biometricSourceType: BiometricSourceType) { val keyguardNotShowing = !keyguardStateController.isShowing val unlockNotAllowed = !keyguardUpdateMonitor .isUnlockingWithBiometricAllowed(biometricSourceType) if (keyguardNotShowing || unlockNotAllowed) { logger.notShowingUnlockRipple(keyguardNotShowing, unlockNotAllowed) return } updateSensorLocation() if (biometricSourceType == BiometricSourceType.FINGERPRINT) { fingerprintSensorLocation?.let { mView.setFingerprintSensorLocation(it, udfpsRadius) circleReveal = CircleReveal( it.x, it.y, 0, Math.max( Math.max(it.x, displayMetrics.widthPixels - it.x), Math.max(it.y, displayMetrics.heightPixels - it.y) ) ) logger.showingUnlockRippleAt(it.x, it.y, "FP sensor radius: $udfpsRadius") showUnlockedRipple() } } else if (biometricSourceType == BiometricSourceType.FACE) { faceSensorLocation?.let { mView.setSensorLocation(it) circleReveal = CircleReveal( it.x, it.y, 0, Math.max( Math.max(it.x, displayMetrics.widthPixels - it.x), Math.max(it.y, displayMetrics.heightPixels - it.y) ) ) logger.showingUnlockRippleAt(it.x, it.y, "Face unlock ripple") showUnlockedRipple() } } } private fun showUnlockedRipple() { notificationShadeWindowController.setForcePluginOpen(true, this) // This code path is not used if the KeyguardTransitionRepository is managing the light // reveal scrim. if (!featureFlags.isEnabled(Flags.LIGHT_REVEAL_MIGRATION)) { if (statusBarStateController.isDozing || biometricUnlockController.isWakeAndUnlock) { circleReveal?.let { lightRevealScrim.revealAmount = 0f lightRevealScrim.revealEffect = it startLightRevealScrimOnKeyguardFadingAway = true } } } mView.startUnlockedRipple( /* end runnable */ Runnable { notificationShadeWindowController.setForcePluginOpen(false, this) } ) } override fun onKeyguardFadingAwayChanged() { if (featureFlags.isEnabled(Flags.LIGHT_REVEAL_MIGRATION)) { return } if (keyguardStateController.isKeyguardFadingAway) { if (startLightRevealScrimOnKeyguardFadingAway) { lightRevealScrimAnimator?.cancel() lightRevealScrimAnimator = ValueAnimator.ofFloat(.1f, 1f).apply { interpolator = Interpolators.LINEAR_OUT_SLOW_IN duration = RIPPLE_ANIMATION_DURATION startDelay = keyguardStateController.keyguardFadingAwayDelay addUpdateListener { animator -> if (lightRevealScrim.revealEffect != circleReveal) { // if something else took over the reveal, let's cancel ourselves cancel() return@addUpdateListener } lightRevealScrim.revealAmount = animator.animatedValue as Float } addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { // Reset light reveal scrim to the default, so the CentralSurfaces // can handle any subsequent light reveal changes // (ie: from dozing changes) if (lightRevealScrim.revealEffect == circleReveal) { lightRevealScrim.revealEffect = LiftReveal } lightRevealScrimAnimator = null } }) start() } startLightRevealScrimOnKeyguardFadingAway = false } } } /** * Whether we're animating the light reveal scrim from a call to [onKeyguardFadingAwayChanged]. */ fun isAnimatingLightRevealScrim(): Boolean { return lightRevealScrimAnimator?.isRunning ?: false } override fun onStartedGoingToSleep() { // reset the light reveal start in case we were pending an unlock startLightRevealScrimOnKeyguardFadingAway = false } fun updateSensorLocation() { fingerprintSensorLocation = authController.fingerprintSensorLocation faceSensorLocation = authController.faceSensorLocation } private fun updateRippleColor() { mView.setLockScreenColor(Utils.getColorAttrDefaultColor(sysuiContext, R.attr.wallpaperTextColorAccent)) } private fun showDwellRipple() { updateSensorLocation() fingerprintSensorLocation?.let { mView.setFingerprintSensorLocation(it, udfpsRadius) mView.startDwellRipple(statusBarStateController.isDozing) } } private val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() { override fun onBiometricAuthenticated( userId: Int, biometricSourceType: BiometricSourceType, isStrongBiometric: Boolean ) { if (biometricSourceType == BiometricSourceType.FINGERPRINT) { mView.fadeDwellRipple() } } override fun onBiometricAuthFailed(biometricSourceType: BiometricSourceType) { if (biometricSourceType == BiometricSourceType.FINGERPRINT) { mView.retractDwellRipple() } } override fun onBiometricAcquired( biometricSourceType: BiometricSourceType, acquireInfo: Int ) { if (biometricSourceType == BiometricSourceType.FINGERPRINT && BiometricFingerprintConstants.shouldDisableUdfpsDisplayMode(acquireInfo) && acquireInfo != BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_GOOD) { // received an 'acquiredBad' message, so immediately retract mView.retractDwellRipple() } } override fun onKeyguardBouncerStateChanged(bouncerIsOrWillBeShowing: Boolean) { if (bouncerIsOrWillBeShowing) { mView.fadeDwellRipple() } } } private val configurationChangedListener = object : ConfigurationController.ConfigurationListener { override fun onUiModeChanged() { updateRippleColor() } override fun onThemeChanged() { updateRippleColor() } } private val udfpsControllerCallback = object : UdfpsController.Callback { override fun onFingerDown() { showDwellRipple() } override fun onFingerUp() { mView.retractDwellRipple() } } private val authControllerCallback = object : AuthController.Callback { override fun onAllAuthenticatorsRegistered(modality: Int) { updateUdfpsDependentParams() } override fun onUdfpsLocationChanged(udfpsOverlayParams: UdfpsOverlayParams) { updateUdfpsDependentParams() } } private fun updateUdfpsDependentParams() { authController.udfpsProps?.let { if (it.size > 0) { udfpsController = udfpsControllerProvider.get() udfpsRadius = authController.udfpsRadius if (mView.isAttachedToWindow) { udfpsController?.addCallback(udfpsControllerCallback) } } } } inner class AuthRippleCommand : Command { override fun execute(pw: PrintWriter, args: List) { if (args.isEmpty()) { invalidCommand(pw) } else { when (args[0]) { "dwell" -> { showDwellRipple() pw.println("lock screen dwell ripple: " + "\n\tsensorLocation=$fingerprintSensorLocation" + "\n\tudfpsRadius=$udfpsRadius") } "fingerprint" -> { pw.println("fingerprint ripple sensorLocation=$fingerprintSensorLocation") showUnlockRipple(BiometricSourceType.FINGERPRINT) } "face" -> { // note: only shows when about to proceed to the home screen pw.println("face ripple sensorLocation=$faceSensorLocation") showUnlockRipple(BiometricSourceType.FACE) } "custom" -> { if (args.size != 3 || args[1].toFloatOrNull() == null || args[2].toFloatOrNull() == null) { invalidCommand(pw) return } pw.println("custom ripple sensorLocation=" + args[1] + ", " + args[2]) mView.setSensorLocation(Point(args[1].toInt(), args[2].toInt())) showUnlockedRipple() } else -> invalidCommand(pw) } } } override fun help(pw: PrintWriter) { pw.println("Usage: adb shell cmd statusbar auth-ripple ") pw.println("Available commands:") pw.println(" dwell") pw.println(" fingerprint") pw.println(" face") pw.println(" custom ") } fun invalidCommand(pw: PrintWriter) { pw.println("invalid command") help(pw) } } companion object { const val RIPPLE_ANIMATION_DURATION: Long = 800 const val TAG = "AuthRippleController" } }