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