blob: 634531215002e072010b8002a4835eecc7eeb7dc [file] [log] [blame]
/*
* 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.app.animation.Interpolators
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.KeyguardUpdateMonitorCallback
import com.android.keyguard.logging.KeyguardLogger
import com.android.settingslib.Utils
import com.android.systemui.CoreStartable
import com.android.systemui.Flags.lightRevealMigration
import com.android.systemui.res.R
import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.flags.FeatureFlags
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<UdfpsController>,
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<AuthRippleView>(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 (!lightRevealMigration()) {
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 (lightRevealMigration()) {
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() {
// only show dwell ripple for device entry
if (keyguardUpdateMonitor.isFingerprintDetectionRunning) {
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<String>) {
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 <command>")
pw.println("Available commands:")
pw.println(" dwell")
pw.println(" fingerprint")
pw.println(" face")
pw.println(" custom <x-location: int> <y-location: int>")
}
fun invalidCommand(pw: PrintWriter) {
pw.println("invalid command")
help(pw)
}
}
companion object {
const val RIPPLE_ANIMATION_DURATION: Long = 800
const val TAG = "AuthRippleController"
}
}