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