/* * 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.PointF import android.hardware.biometrics.BiometricSourceType import android.util.DisplayMetrics import android.util.Log import androidx.annotation.VisibleForTesting import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.settingslib.Utils import com.android.systemui.R import com.android.systemui.animation.Interpolators import com.android.systemui.keyguard.WakefulnessLifecycle 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.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.phone.KeyguardBypassController import com.android.systemui.statusbar.phone.StatusBar import com.android.systemui.statusbar.phone.dagger.StatusBarComponent.StatusBarScope import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.ViewController import com.android.systemui.util.leak.RotationUtils import java.io.PrintWriter import javax.inject.Inject import javax.inject.Provider /*** * Controls the ripple effect that shows when authentication is successful. * The ripple uses the accent color of the current theme. */ @StatusBarScope class AuthRippleController @Inject constructor( private val statusBar: StatusBar, 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 bypassController: KeyguardBypassController, private val biometricUnlockController: BiometricUnlockController, private val udfpsControllerProvider: Provider, private val statusBarStateController: StatusBarStateController, rippleView: AuthRippleView? ) : ViewController(rippleView), KeyguardStateController.Callback, WakefulnessLifecycle.Observer { @VisibleForTesting internal var startLightRevealScrimOnKeyguardFadingAway = false var fingerprintSensorLocation: PointF? = null private var faceSensorLocation: PointF? = null private var circleReveal: LightRevealEffect? = null private var udfpsController: UdfpsController? = null private var udfpsRadius: Float = -1f override fun onInit() { mView.setAlphaInDuration(sysuiContext.resources.getInteger( R.integer.auth_ripple_alpha_in_duration).toLong()) } @VisibleForTesting public override fun onViewAttached() { authController.addCallback(authControllerCallback) updateRippleColor() updateSensorLocation() updateUdfpsDependentParams() udfpsController?.addCallback(udfpsControllerCallback) configurationController.addCallback(configurationChangedListener) keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) keyguardStateController.addCallback(this) wakefulnessLifecycle.addObserver(this) commandRegistry.registerCommand("auth-ripple") { AuthRippleCommand() } } @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") notificationShadeWindowController.setForcePluginOpen(false, this) } fun showRipple(biometricSourceType: BiometricSourceType?) { if (!keyguardUpdateMonitor.isKeyguardVisible || keyguardUpdateMonitor.userNeedsStrongAuth()) { return } updateSensorLocation() if (biometricSourceType == BiometricSourceType.FINGERPRINT && fingerprintSensorLocation != null) { mView.setFingerprintSensorLocation(fingerprintSensorLocation!!, udfpsRadius) showUnlockedRipple() } else if (biometricSourceType == BiometricSourceType.FACE && faceSensorLocation != null) { if (!bypassController.canBypass()) { return } mView.setSensorLocation(faceSensorLocation!!) showUnlockedRipple() } } private fun showUnlockedRipple() { notificationShadeWindowController.setForcePluginOpen(true, this) val lightRevealScrim = statusBar.lightRevealScrim if (statusBarStateController.isDozing || biometricUnlockController.isWakeAndUnlock) { circleReveal?.let { lightRevealScrim?.revealEffect = it startLightRevealScrimOnKeyguardFadingAway = true } } mView.startUnlockedRipple( /* end runnable */ Runnable { notificationShadeWindowController.setForcePluginOpen(false, this) } ) } override fun onKeyguardFadingAwayChanged() { if (keyguardStateController.isKeyguardFadingAway) { val lightRevealScrim = statusBar.lightRevealScrim if (startLightRevealScrimOnKeyguardFadingAway && lightRevealScrim != null) { 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 do nothing. 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 StatusBar // can handle any subsequent light reveal changes // (ie: from dozing changes) if (lightRevealScrim.revealEffect == circleReveal) { lightRevealScrim.revealEffect = LiftReveal } } }) start() } startLightRevealScrimOnKeyguardFadingAway = false } } } override fun onStartedGoingToSleep() { // reset the light reveal start in case we were pending an unlock startLightRevealScrimOnKeyguardFadingAway = false } fun updateSensorLocation() { updateFingerprintLocation() faceSensorLocation = authController.faceAuthSensorLocation fingerprintSensorLocation?.let { circleReveal = CircleReveal( it.x, it.y, 0f, Math.max( Math.max(it.x, statusBar.displayWidth - it.x), Math.max(it.y, statusBar.displayHeight - it.y) ) ) } } private fun updateFingerprintLocation() { val displayMetrics = DisplayMetrics() sysuiContext.display?.getRealMetrics(displayMetrics) val width = displayMetrics.widthPixels val height = displayMetrics.heightPixels authController.fingerprintSensorLocation?.let { fingerprintSensorLocation = when (RotationUtils.getRotation(sysuiContext)) { RotationUtils.ROTATION_LANDSCAPE -> { val normalizedYPos: Float = it.y / width val normalizedXPos: Float = it.x / height PointF(width * normalizedYPos, height * (1 - normalizedXPos)) } RotationUtils.ROTATION_UPSIDE_DOWN -> { PointF(width - it.x, height - it.y) } RotationUtils.ROTATION_SEASCAPE -> { val normalizedYPos: Float = it.y / width val normalizedXPos: Float = it.x / height PointF(width * (1 - normalizedYPos), height * normalizedXPos) } else -> { // ROTATION_NONE PointF(it.x, it.y) } } } } private fun updateRippleColor() { mView.setLockScreenColor(Utils.getColorAttrDefaultColor(sysuiContext, R.attr.wallpaperTextColorAccent)) } private fun showDwellRipple() { mView.startDwellRipple(statusBarStateController.isDozing) } private val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() { override fun onBiometricAuthenticated( userId: Int, biometricSourceType: BiometricSourceType?, isStrongBiometric: Boolean ) { showRipple(biometricSourceType) } override fun onBiometricAuthFailed(biometricSourceType: BiometricSourceType?) { mView.retractRipple() } } private val configurationChangedListener = object : ConfigurationController.ConfigurationListener { override fun onUiModeChanged() { updateRippleColor() } override fun onThemeChanged() { updateRippleColor() } } private val udfpsControllerCallback = object : UdfpsController.Callback { override fun onFingerDown() { if (fingerprintSensorLocation == null) { Log.e("AuthRipple", "fingerprintSensorLocation=null onFingerDown. " + "Skip showing dwell ripple") return } mView.setFingerprintSensorLocation(fingerprintSensorLocation!!, udfpsRadius) showDwellRipple() } override fun onFingerUp() { mView.retractRipple() } } private val authControllerCallback = object : AuthController.Callback { override fun onAllAuthenticatorsRegistered() { updateUdfpsDependentParams() updateSensorLocation() } override fun onEnrollmentsChanged() { } } private fun updateUdfpsDependentParams() { authController.udfpsProps?.let { if (it.size > 0) { udfpsRadius = it[0].location.sensorRadius.toFloat() udfpsController = udfpsControllerProvider.get() 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" -> { updateSensorLocation() pw.println("fingerprint ripple sensorLocation=$fingerprintSensorLocation") showRipple(BiometricSourceType.FINGERPRINT) } "face" -> { updateSensorLocation() pw.println("face ripple sensorLocation=$faceSensorLocation") showRipple(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].toFloat() + ", " + args[2].toFloat()) mView.setSensorLocation(PointF(args[1].toFloat(), args[2].toFloat())) 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 = 1533 } }