| /* |
| * 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" |
| } |
| } |