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