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