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.PointF 24 import android.hardware.biometrics.BiometricSourceType 25 import android.util.DisplayMetrics 26 import android.util.Log 27 import androidx.annotation.VisibleForTesting 28 import com.android.keyguard.KeyguardUpdateMonitor 29 import com.android.keyguard.KeyguardUpdateMonitorCallback 30 import com.android.settingslib.Utils 31 import com.android.systemui.R 32 import com.android.systemui.animation.Interpolators 33 import com.android.systemui.keyguard.WakefulnessLifecycle 34 import com.android.systemui.plugins.statusbar.StatusBarStateController 35 import com.android.systemui.statusbar.CircleReveal 36 import com.android.systemui.statusbar.LiftReveal 37 import com.android.systemui.statusbar.LightRevealEffect 38 import com.android.systemui.statusbar.NotificationShadeWindowController 39 import com.android.systemui.statusbar.commandline.Command 40 import com.android.systemui.statusbar.commandline.CommandRegistry 41 import com.android.systemui.statusbar.phone.BiometricUnlockController 42 import com.android.systemui.statusbar.phone.KeyguardBypassController 43 import com.android.systemui.statusbar.phone.StatusBar 44 import com.android.systemui.statusbar.phone.dagger.StatusBarComponent.StatusBarScope 45 import com.android.systemui.statusbar.policy.ConfigurationController 46 import com.android.systemui.statusbar.policy.KeyguardStateController 47 import com.android.systemui.util.ViewController 48 import com.android.systemui.util.leak.RotationUtils 49 import java.io.PrintWriter 50 import javax.inject.Inject 51 import javax.inject.Provider 52 53 /*** 54 * Controls the ripple effect that shows when authentication is successful. 55 * The ripple uses the accent color of the current theme. 56 */ 57 @StatusBarScope 58 class AuthRippleController @Inject constructor( 59 private val statusBar: StatusBar, 60 private val sysuiContext: Context, 61 private val authController: AuthController, 62 private val configurationController: ConfigurationController, 63 private val keyguardUpdateMonitor: KeyguardUpdateMonitor, 64 private val keyguardStateController: KeyguardStateController, 65 private val wakefulnessLifecycle: WakefulnessLifecycle, 66 private val commandRegistry: CommandRegistry, 67 private val notificationShadeWindowController: NotificationShadeWindowController, 68 private val bypassController: KeyguardBypassController, 69 private val biometricUnlockController: BiometricUnlockController, 70 private val udfpsControllerProvider: Provider<UdfpsController>, 71 private val statusBarStateController: StatusBarStateController, 72 rippleView: AuthRippleView? 73 ) : ViewController<AuthRippleView>(rippleView), KeyguardStateController.Callback, 74 WakefulnessLifecycle.Observer { 75 76 @VisibleForTesting 77 internal var startLightRevealScrimOnKeyguardFadingAway = false 78 var fingerprintSensorLocation: PointF? = null 79 private var faceSensorLocation: PointF? = null 80 private var circleReveal: LightRevealEffect? = null 81 82 private var udfpsController: UdfpsController? = null 83 private var udfpsRadius: Float = -1f 84 85 override fun onInit() { 86 mView.setAlphaInDuration(sysuiContext.resources.getInteger( 87 R.integer.auth_ripple_alpha_in_duration).toLong()) 88 } 89 90 @VisibleForTesting 91 public override fun onViewAttached() { 92 authController.addCallback(authControllerCallback) 93 updateRippleColor() 94 updateSensorLocation() 95 updateUdfpsDependentParams() 96 udfpsController?.addCallback(udfpsControllerCallback) 97 configurationController.addCallback(configurationChangedListener) 98 keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) 99 keyguardStateController.addCallback(this) 100 wakefulnessLifecycle.addObserver(this) 101 commandRegistry.registerCommand("auth-ripple") { AuthRippleCommand() } 102 } 103 104 @VisibleForTesting 105 public override fun onViewDetached() { 106 udfpsController?.removeCallback(udfpsControllerCallback) 107 authController.removeCallback(authControllerCallback) 108 keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback) 109 configurationController.removeCallback(configurationChangedListener) 110 keyguardStateController.removeCallback(this) 111 wakefulnessLifecycle.removeObserver(this) 112 commandRegistry.unregisterCommand("auth-ripple") 113 114 notificationShadeWindowController.setForcePluginOpen(false, this) 115 } 116 117 fun showRipple(biometricSourceType: BiometricSourceType?) { 118 if (!keyguardUpdateMonitor.isKeyguardVisible || 119 keyguardUpdateMonitor.userNeedsStrongAuth()) { 120 return 121 } 122 123 updateSensorLocation() 124 if (biometricSourceType == BiometricSourceType.FINGERPRINT && 125 fingerprintSensorLocation != null) { 126 mView.setFingerprintSensorLocation(fingerprintSensorLocation!!, udfpsRadius) 127 showUnlockedRipple() 128 } else if (biometricSourceType == BiometricSourceType.FACE && 129 faceSensorLocation != null) { 130 if (!bypassController.canBypass()) { 131 return 132 } 133 mView.setSensorLocation(faceSensorLocation!!) 134 showUnlockedRipple() 135 } 136 } 137 138 private fun showUnlockedRipple() { 139 notificationShadeWindowController.setForcePluginOpen(true, this) 140 val lightRevealScrim = statusBar.lightRevealScrim 141 if (statusBarStateController.isDozing || biometricUnlockController.isWakeAndUnlock) { 142 circleReveal?.let { 143 lightRevealScrim?.revealEffect = it 144 startLightRevealScrimOnKeyguardFadingAway = true 145 } 146 } 147 148 mView.startUnlockedRipple( 149 /* end runnable */ 150 Runnable { 151 notificationShadeWindowController.setForcePluginOpen(false, this) 152 } 153 ) 154 } 155 156 override fun onKeyguardFadingAwayChanged() { 157 if (keyguardStateController.isKeyguardFadingAway) { 158 val lightRevealScrim = statusBar.lightRevealScrim 159 if (startLightRevealScrimOnKeyguardFadingAway && lightRevealScrim != null) { 160 ValueAnimator.ofFloat(.1f, 1f).apply { 161 interpolator = Interpolators.LINEAR_OUT_SLOW_IN 162 duration = RIPPLE_ANIMATION_DURATION 163 startDelay = keyguardStateController.keyguardFadingAwayDelay 164 addUpdateListener { animator -> 165 if (lightRevealScrim.revealEffect != circleReveal) { 166 // if something else took over the reveal, let's do nothing. 167 return@addUpdateListener 168 } 169 lightRevealScrim.revealAmount = animator.animatedValue as Float 170 } 171 addListener(object : AnimatorListenerAdapter() { 172 override fun onAnimationEnd(animation: Animator?) { 173 // Reset light reveal scrim to the default, so the StatusBar 174 // can handle any subsequent light reveal changes 175 // (ie: from dozing changes) 176 if (lightRevealScrim.revealEffect == circleReveal) { 177 lightRevealScrim.revealEffect = LiftReveal 178 } 179 } 180 }) 181 start() 182 } 183 startLightRevealScrimOnKeyguardFadingAway = false 184 } 185 } 186 } 187 188 override fun onStartedGoingToSleep() { 189 // reset the light reveal start in case we were pending an unlock 190 startLightRevealScrimOnKeyguardFadingAway = false 191 } 192 193 fun updateSensorLocation() { 194 updateFingerprintLocation() 195 faceSensorLocation = authController.faceAuthSensorLocation 196 fingerprintSensorLocation?.let { 197 circleReveal = CircleReveal( 198 it.x, 199 it.y, 200 0f, 201 Math.max( 202 Math.max(it.x, statusBar.displayWidth - it.x), 203 Math.max(it.y, statusBar.displayHeight - it.y) 204 ) 205 ) 206 } 207 } 208 209 private fun updateFingerprintLocation() { 210 val displayMetrics = DisplayMetrics() 211 sysuiContext.display?.getRealMetrics(displayMetrics) 212 val width = displayMetrics.widthPixels 213 val height = displayMetrics.heightPixels 214 215 authController.fingerprintSensorLocation?.let { 216 fingerprintSensorLocation = when (RotationUtils.getRotation(sysuiContext)) { 217 RotationUtils.ROTATION_LANDSCAPE -> { 218 val normalizedYPos: Float = it.y / width 219 val normalizedXPos: Float = it.x / height 220 PointF(width * normalizedYPos, height * (1 - normalizedXPos)) 221 } 222 RotationUtils.ROTATION_UPSIDE_DOWN -> { 223 PointF(width - it.x, height - it.y) 224 } 225 RotationUtils.ROTATION_SEASCAPE -> { 226 val normalizedYPos: Float = it.y / width 227 val normalizedXPos: Float = it.x / height 228 PointF(width * (1 - normalizedYPos), height * normalizedXPos) 229 } 230 else -> { 231 // ROTATION_NONE 232 PointF(it.x, it.y) 233 } 234 } 235 } 236 } 237 238 private fun updateRippleColor() { 239 mView.setLockScreenColor(Utils.getColorAttrDefaultColor(sysuiContext, 240 R.attr.wallpaperTextColorAccent)) 241 } 242 243 private fun showDwellRipple() { 244 mView.startDwellRipple(statusBarStateController.isDozing) 245 } 246 247 private val keyguardUpdateMonitorCallback = 248 object : KeyguardUpdateMonitorCallback() { 249 override fun onBiometricAuthenticated( 250 userId: Int, 251 biometricSourceType: BiometricSourceType?, 252 isStrongBiometric: Boolean 253 ) { 254 showRipple(biometricSourceType) 255 } 256 257 override fun onBiometricAuthFailed(biometricSourceType: BiometricSourceType?) { 258 mView.retractRipple() 259 } 260 } 261 262 private val configurationChangedListener = 263 object : ConfigurationController.ConfigurationListener { 264 override fun onUiModeChanged() { 265 updateRippleColor() 266 } 267 override fun onThemeChanged() { 268 updateRippleColor() 269 } 270 } 271 272 private val udfpsControllerCallback = 273 object : UdfpsController.Callback { 274 override fun onFingerDown() { 275 if (fingerprintSensorLocation == null) { 276 Log.e("AuthRipple", "fingerprintSensorLocation=null onFingerDown. " + 277 "Skip showing dwell ripple") 278 return 279 } 280 281 mView.setFingerprintSensorLocation(fingerprintSensorLocation!!, udfpsRadius) 282 showDwellRipple() 283 } 284 285 override fun onFingerUp() { 286 mView.retractRipple() 287 } 288 } 289 290 private val authControllerCallback = 291 object : AuthController.Callback { 292 override fun onAllAuthenticatorsRegistered() { 293 updateUdfpsDependentParams() 294 updateSensorLocation() 295 } 296 297 override fun onEnrollmentsChanged() { 298 } 299 } 300 301 private fun updateUdfpsDependentParams() { 302 authController.udfpsProps?.let { 303 if (it.size > 0) { 304 udfpsRadius = it[0].location.sensorRadius.toFloat() 305 udfpsController = udfpsControllerProvider.get() 306 307 if (mView.isAttachedToWindow) { 308 udfpsController?.addCallback(udfpsControllerCallback) 309 } 310 } 311 } 312 } 313 314 inner class AuthRippleCommand : Command { 315 override fun execute(pw: PrintWriter, args: List<String>) { 316 if (args.isEmpty()) { 317 invalidCommand(pw) 318 } else { 319 when (args[0]) { 320 "dwell" -> { 321 showDwellRipple() 322 pw.println("lock screen dwell ripple: " + 323 "\n\tsensorLocation=$fingerprintSensorLocation" + 324 "\n\tudfpsRadius=$udfpsRadius") 325 } 326 "fingerprint" -> { 327 updateSensorLocation() 328 pw.println("fingerprint ripple sensorLocation=$fingerprintSensorLocation") 329 showRipple(BiometricSourceType.FINGERPRINT) 330 } 331 "face" -> { 332 updateSensorLocation() 333 pw.println("face ripple sensorLocation=$faceSensorLocation") 334 showRipple(BiometricSourceType.FACE) 335 } 336 "custom" -> { 337 if (args.size != 3 || 338 args[1].toFloatOrNull() == null || 339 args[2].toFloatOrNull() == null) { 340 invalidCommand(pw) 341 return 342 } 343 pw.println("custom ripple sensorLocation=" + args[1].toFloat() + ", " + 344 args[2].toFloat()) 345 mView.setSensorLocation(PointF(args[1].toFloat(), args[2].toFloat())) 346 showUnlockedRipple() 347 } 348 else -> invalidCommand(pw) 349 } 350 } 351 } 352 353 override fun help(pw: PrintWriter) { 354 pw.println("Usage: adb shell cmd statusbar auth-ripple <command>") 355 pw.println("Available commands:") 356 pw.println(" dwell") 357 pw.println(" fingerprint") 358 pw.println(" face") 359 pw.println(" custom <x-location: int> <y-location: int>") 360 } 361 362 fun invalidCommand(pw: PrintWriter) { 363 pw.println("invalid command") 364 help(pw) 365 } 366 } 367 368 companion object { 369 const val RIPPLE_ANIMATION_DURATION: Long = 1533 370 } 371 } 372