xref: /aosp_15_r20/frameworks/base/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt (revision d57664e9bc4670b3ecf6748a746a57c557b6bc9e)
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