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