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