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