1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.biometrics 18 19 import android.animation.Animator 20 import android.animation.AnimatorListenerAdapter 21 import android.animation.ValueAnimator 22 import android.content.Context 23 import android.graphics.Point 24 import android.hardware.biometrics.BiometricFingerprintConstants 25 import android.hardware.biometrics.BiometricSourceType 26 import android.util.DisplayMetrics 27 import androidx.annotation.VisibleForTesting 28 import com.android.keyguard.KeyguardUpdateMonitor 29 import com.android.keyguard.KeyguardUpdateMonitorCallback 30 import com.android.keyguard.logging.KeyguardLogger 31 import com.android.settingslib.Utils 32 import com.android.settingslib.udfps.UdfpsOverlayParams 33 import com.android.systemui.CoreStartable 34 import com.android.systemui.R 35 import com.android.app.animation.Interpolators 36 import com.android.systemui.dagger.SysUISingleton 37 import com.android.systemui.flags.FeatureFlags 38 import com.android.systemui.flags.Flags 39 import com.android.systemui.keyguard.WakefulnessLifecycle 40 import com.android.systemui.log.core.LogLevel 41 import com.android.systemui.plugins.statusbar.StatusBarStateController 42 import com.android.systemui.statusbar.CircleReveal 43 import com.android.systemui.statusbar.LiftReveal 44 import com.android.systemui.statusbar.LightRevealEffect 45 import com.android.systemui.statusbar.LightRevealScrim 46 import com.android.systemui.statusbar.NotificationShadeWindowController 47 import com.android.systemui.statusbar.commandline.Command 48 import com.android.systemui.statusbar.commandline.CommandRegistry 49 import com.android.systemui.statusbar.phone.BiometricUnlockController 50 import com.android.systemui.statusbar.policy.ConfigurationController 51 import com.android.systemui.statusbar.policy.KeyguardStateController 52 import com.android.systemui.util.ViewController 53 import java.io.PrintWriter 54 import javax.inject.Inject 55 import javax.inject.Provider 56 57 /** 58 * Controls two ripple effects: 59 * 1. Unlocked ripple: shows when authentication is successful 60 * 2. UDFPS dwell ripple: shows when the user has their finger down on the UDFPS area and reacts 61 * to errors and successes 62 * 63 * The ripple uses the accent color of the current theme. 64 */ 65 @SysUISingleton 66 class AuthRippleController @Inject constructor( 67 private val sysuiContext: Context, 68 private val authController: AuthController, 69 private val configurationController: ConfigurationController, 70 private val keyguardUpdateMonitor: KeyguardUpdateMonitor, 71 private val keyguardStateController: KeyguardStateController, 72 private val wakefulnessLifecycle: WakefulnessLifecycle, 73 private val commandRegistry: CommandRegistry, 74 private val notificationShadeWindowController: NotificationShadeWindowController, 75 private val udfpsControllerProvider: Provider<UdfpsController>, 76 private val statusBarStateController: StatusBarStateController, 77 private val displayMetrics: DisplayMetrics, 78 private val featureFlags: FeatureFlags, 79 private val logger: KeyguardLogger, 80 private val biometricUnlockController: BiometricUnlockController, 81 private val lightRevealScrim: LightRevealScrim, 82 rippleView: AuthRippleView? 83 ) : 84 ViewController<AuthRippleView>(rippleView), 85 CoreStartable, 86 KeyguardStateController.Callback, 87 WakefulnessLifecycle.Observer { 88 89 @VisibleForTesting 90 internal var startLightRevealScrimOnKeyguardFadingAway = false 91 var lightRevealScrimAnimator: ValueAnimator? = null 92 var fingerprintSensorLocation: Point? = null 93 private var faceSensorLocation: Point? = null 94 private var circleReveal: LightRevealEffect? = null 95 96 private var udfpsController: UdfpsController? = null 97 private var udfpsRadius: Float = -1f 98 99 override fun start() { 100 init() 101 } 102 103 @VisibleForTesting 104 public override fun onViewAttached() { 105 authController.addCallback(authControllerCallback) 106 updateRippleColor() 107 updateUdfpsDependentParams() 108 udfpsController?.addCallback(udfpsControllerCallback) 109 configurationController.addCallback(configurationChangedListener) 110 keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) 111 keyguardStateController.addCallback(this) 112 wakefulnessLifecycle.addObserver(this) 113 commandRegistry.registerCommand("auth-ripple") { AuthRippleCommand() } 114 biometricUnlockController.addListener(biometricModeListener) 115 } 116 117 private val biometricModeListener = 118 object : BiometricUnlockController.BiometricUnlockEventsListener { 119 override fun onBiometricUnlockedWithKeyguardDismissal( 120 biometricSourceType: BiometricSourceType? 121 ) { 122 if (biometricSourceType != null) { 123 showUnlockRipple(biometricSourceType) 124 } else { 125 logger.log(TAG, 126 LogLevel.ERROR, 127 "Unexpected scenario where biometricSourceType is null") 128 } 129 } 130 } 131 132 @VisibleForTesting 133 public override fun onViewDetached() { 134 udfpsController?.removeCallback(udfpsControllerCallback) 135 authController.removeCallback(authControllerCallback) 136 keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback) 137 configurationController.removeCallback(configurationChangedListener) 138 keyguardStateController.removeCallback(this) 139 wakefulnessLifecycle.removeObserver(this) 140 commandRegistry.unregisterCommand("auth-ripple") 141 biometricUnlockController.removeListener(biometricModeListener) 142 143 notificationShadeWindowController.setForcePluginOpen(false, this) 144 } 145 146 fun showUnlockRipple(biometricSourceType: BiometricSourceType) { 147 val keyguardNotShowing = !keyguardStateController.isShowing 148 val unlockNotAllowed = !keyguardUpdateMonitor 149 .isUnlockingWithBiometricAllowed(biometricSourceType) 150 if (keyguardNotShowing || unlockNotAllowed) { 151 logger.notShowingUnlockRipple(keyguardNotShowing, unlockNotAllowed) 152 return 153 } 154 155 updateSensorLocation() 156 if (biometricSourceType == BiometricSourceType.FINGERPRINT) { 157 fingerprintSensorLocation?.let { 158 mView.setFingerprintSensorLocation(it, udfpsRadius) 159 circleReveal = CircleReveal( 160 it.x, 161 it.y, 162 0, 163 Math.max( 164 Math.max(it.x, displayMetrics.widthPixels - it.x), 165 Math.max(it.y, displayMetrics.heightPixels - it.y) 166 ) 167 ) 168 logger.showingUnlockRippleAt(it.x, it.y, "FP sensor radius: $udfpsRadius") 169 showUnlockedRipple() 170 } 171 } else if (biometricSourceType == BiometricSourceType.FACE) { 172 faceSensorLocation?.let { 173 mView.setSensorLocation(it) 174 circleReveal = CircleReveal( 175 it.x, 176 it.y, 177 0, 178 Math.max( 179 Math.max(it.x, displayMetrics.widthPixels - it.x), 180 Math.max(it.y, displayMetrics.heightPixels - it.y) 181 ) 182 ) 183 logger.showingUnlockRippleAt(it.x, it.y, "Face unlock ripple") 184 showUnlockedRipple() 185 } 186 } 187 } 188 189 private fun showUnlockedRipple() { 190 notificationShadeWindowController.setForcePluginOpen(true, this) 191 192 // This code path is not used if the KeyguardTransitionRepository is managing the light 193 // reveal scrim. 194 if (!featureFlags.isEnabled(Flags.LIGHT_REVEAL_MIGRATION)) { 195 if (statusBarStateController.isDozing || biometricUnlockController.isWakeAndUnlock) { 196 circleReveal?.let { 197 lightRevealScrim.revealAmount = 0f 198 lightRevealScrim.revealEffect = it 199 startLightRevealScrimOnKeyguardFadingAway = true 200 } 201 } 202 } 203 204 mView.startUnlockedRipple( 205 /* end runnable */ 206 Runnable { 207 notificationShadeWindowController.setForcePluginOpen(false, this) 208 } 209 ) 210 } 211 212 override fun onKeyguardFadingAwayChanged() { 213 if (featureFlags.isEnabled(Flags.LIGHT_REVEAL_MIGRATION)) { 214 return 215 } 216 217 if (keyguardStateController.isKeyguardFadingAway) { 218 if (startLightRevealScrimOnKeyguardFadingAway) { 219 lightRevealScrimAnimator?.cancel() 220 lightRevealScrimAnimator = ValueAnimator.ofFloat(.1f, 1f).apply { 221 interpolator = Interpolators.LINEAR_OUT_SLOW_IN 222 duration = RIPPLE_ANIMATION_DURATION 223 startDelay = keyguardStateController.keyguardFadingAwayDelay 224 addUpdateListener { animator -> 225 if (lightRevealScrim.revealEffect != circleReveal) { 226 // if something else took over the reveal, let's cancel ourselves 227 cancel() 228 return@addUpdateListener 229 } 230 lightRevealScrim.revealAmount = animator.animatedValue as Float 231 } 232 addListener(object : AnimatorListenerAdapter() { 233 override fun onAnimationEnd(animation: Animator) { 234 // Reset light reveal scrim to the default, so the CentralSurfaces 235 // can handle any subsequent light reveal changes 236 // (ie: from dozing changes) 237 if (lightRevealScrim.revealEffect == circleReveal) { 238 lightRevealScrim.revealEffect = LiftReveal 239 } 240 241 lightRevealScrimAnimator = null 242 } 243 }) 244 start() 245 } 246 startLightRevealScrimOnKeyguardFadingAway = false 247 } 248 } 249 } 250 251 /** 252 * Whether we're animating the light reveal scrim from a call to [onKeyguardFadingAwayChanged]. 253 */ 254 fun isAnimatingLightRevealScrim(): Boolean { 255 return lightRevealScrimAnimator?.isRunning ?: false 256 } 257 258 override fun onStartedGoingToSleep() { 259 // reset the light reveal start in case we were pending an unlock 260 startLightRevealScrimOnKeyguardFadingAway = false 261 } 262 263 fun updateSensorLocation() { 264 fingerprintSensorLocation = authController.fingerprintSensorLocation 265 faceSensorLocation = authController.faceSensorLocation 266 } 267 268 private fun updateRippleColor() { 269 mView.setLockScreenColor(Utils.getColorAttrDefaultColor(sysuiContext, 270 R.attr.wallpaperTextColorAccent)) 271 } 272 273 private fun showDwellRipple() { 274 updateSensorLocation() 275 fingerprintSensorLocation?.let { 276 mView.setFingerprintSensorLocation(it, udfpsRadius) 277 mView.startDwellRipple(statusBarStateController.isDozing) 278 } 279 } 280 281 private val keyguardUpdateMonitorCallback = 282 object : KeyguardUpdateMonitorCallback() { 283 override fun onBiometricAuthenticated( 284 userId: Int, 285 biometricSourceType: BiometricSourceType, 286 isStrongBiometric: Boolean 287 ) { 288 if (biometricSourceType == BiometricSourceType.FINGERPRINT) { 289 mView.fadeDwellRipple() 290 } 291 } 292 293 override fun onBiometricAuthFailed(biometricSourceType: BiometricSourceType) { 294 if (biometricSourceType == BiometricSourceType.FINGERPRINT) { 295 mView.retractDwellRipple() 296 } 297 } 298 299 override fun onBiometricAcquired( 300 biometricSourceType: BiometricSourceType, 301 acquireInfo: Int 302 ) { 303 if (biometricSourceType == BiometricSourceType.FINGERPRINT && 304 BiometricFingerprintConstants.shouldDisableUdfpsDisplayMode(acquireInfo) && 305 acquireInfo != BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_GOOD) { 306 // received an 'acquiredBad' message, so immediately retract 307 mView.retractDwellRipple() 308 } 309 } 310 311 override fun onKeyguardBouncerStateChanged(bouncerIsOrWillBeShowing: Boolean) { 312 if (bouncerIsOrWillBeShowing) { 313 mView.fadeDwellRipple() 314 } 315 } 316 } 317 318 private val configurationChangedListener = 319 object : ConfigurationController.ConfigurationListener { 320 override fun onUiModeChanged() { 321 updateRippleColor() 322 } 323 override fun onThemeChanged() { 324 updateRippleColor() 325 } 326 } 327 328 private val udfpsControllerCallback = 329 object : UdfpsController.Callback { 330 override fun onFingerDown() { 331 showDwellRipple() 332 } 333 334 override fun onFingerUp() { 335 mView.retractDwellRipple() 336 } 337 } 338 339 private val authControllerCallback = 340 object : AuthController.Callback { 341 override fun onAllAuthenticatorsRegistered(modality: Int) { 342 updateUdfpsDependentParams() 343 } 344 345 override fun onUdfpsLocationChanged(udfpsOverlayParams: UdfpsOverlayParams) { 346 updateUdfpsDependentParams() 347 } 348 } 349 350 private fun updateUdfpsDependentParams() { 351 authController.udfpsProps?.let { 352 if (it.size > 0) { 353 udfpsController = udfpsControllerProvider.get() 354 udfpsRadius = authController.udfpsRadius 355 356 if (mView.isAttachedToWindow) { 357 udfpsController?.addCallback(udfpsControllerCallback) 358 } 359 } 360 } 361 } 362 363 inner class AuthRippleCommand : Command { 364 override fun execute(pw: PrintWriter, args: List<String>) { 365 if (args.isEmpty()) { 366 invalidCommand(pw) 367 } else { 368 when (args[0]) { 369 "dwell" -> { 370 showDwellRipple() 371 pw.println("lock screen dwell ripple: " + 372 "\n\tsensorLocation=$fingerprintSensorLocation" + 373 "\n\tudfpsRadius=$udfpsRadius") 374 } 375 "fingerprint" -> { 376 pw.println("fingerprint ripple sensorLocation=$fingerprintSensorLocation") 377 showUnlockRipple(BiometricSourceType.FINGERPRINT) 378 } 379 "face" -> { 380 // note: only shows when about to proceed to the home screen 381 pw.println("face ripple sensorLocation=$faceSensorLocation") 382 showUnlockRipple(BiometricSourceType.FACE) 383 } 384 "custom" -> { 385 if (args.size != 3 || 386 args[1].toFloatOrNull() == null || 387 args[2].toFloatOrNull() == null) { 388 invalidCommand(pw) 389 return 390 } 391 pw.println("custom ripple sensorLocation=" + args[1] + ", " + args[2]) 392 mView.setSensorLocation(Point(args[1].toInt(), args[2].toInt())) 393 showUnlockedRipple() 394 } 395 else -> invalidCommand(pw) 396 } 397 } 398 } 399 400 override fun help(pw: PrintWriter) { 401 pw.println("Usage: adb shell cmd statusbar auth-ripple <command>") 402 pw.println("Available commands:") 403 pw.println(" dwell") 404 pw.println(" fingerprint") 405 pw.println(" face") 406 pw.println(" custom <x-location: int> <y-location: int>") 407 } 408 409 fun invalidCommand(pw: PrintWriter) { 410 pw.println("invalid command") 411 help(pw) 412 } 413 } 414 415 companion object { 416 const val RIPPLE_ANIMATION_DURATION: Long = 800 417 const val TAG = "AuthRippleController" 418 } 419 } 420