1 /*
2  * Copyright (C) 2022 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 package com.android.systemui.biometrics
17 
18 import android.content.Context
19 import android.graphics.drawable.Drawable
20 import android.util.Log
21 import com.airbnb.lottie.LottieAnimationView
22 import com.android.systemui.R
23 import com.android.systemui.biometrics.AuthBiometricView.BiometricState
24 import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATED
25 import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATING
26 import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATING_ANIMATING_IN
27 import com.android.systemui.biometrics.AuthBiometricView.STATE_ERROR
28 import com.android.systemui.biometrics.AuthBiometricView.STATE_HELP
29 import com.android.systemui.biometrics.AuthBiometricView.STATE_IDLE
30 import com.android.systemui.biometrics.AuthBiometricView.STATE_PENDING_CONFIRMATION
31 
32 private const val TAG = "AuthBiometricFaceIconController"
33 
34 /** Face only icon animator for BiometricPrompt. */
35 class AuthBiometricFaceIconController(
36         context: Context,
37         iconView: LottieAnimationView
38 ) : AuthIconController(context, iconView) {
39 
40     // false = dark to light, true = light to dark
41     private var lastPulseLightToDark = false
42 
43     @BiometricState
44     private var state = 0
45 
46     init {
47         val size = context.resources.getDimensionPixelSize(R.dimen.biometric_dialog_face_icon_size)
48         iconView.layoutParams.width = size
49         iconView.layoutParams.height = size
50         showStaticDrawable(R.drawable.face_dialog_pulse_dark_to_light)
51     }
52 
53     private fun startPulsing() {
54         lastPulseLightToDark = false
55         animateIcon(R.drawable.face_dialog_pulse_dark_to_light, true)
56     }
57 
58     private fun pulseInNextDirection() {
59         val iconRes = if (lastPulseLightToDark) {
60             R.drawable.face_dialog_pulse_dark_to_light
61         } else {
62             R.drawable.face_dialog_pulse_light_to_dark
63         }
64         animateIcon(iconRes, true /* repeat */)
65         lastPulseLightToDark = !lastPulseLightToDark
66     }
67 
68     override fun handleAnimationEnd(drawable: Drawable) {
69         if (state == STATE_AUTHENTICATING || state == STATE_HELP) {
70             pulseInNextDirection()
71         }
72     }
73 
74     override fun updateIcon(@BiometricState oldState: Int, @BiometricState newState: Int) {
75         val lastStateIsErrorIcon = (oldState == STATE_ERROR || oldState == STATE_HELP)
76         if (newState == STATE_AUTHENTICATING_ANIMATING_IN) {
77             showStaticDrawable(R.drawable.face_dialog_pulse_dark_to_light)
78             iconView.contentDescription = context.getString(
79                     R.string.biometric_dialog_face_icon_description_authenticating
80             )
81         } else if (newState == STATE_AUTHENTICATING) {
82             startPulsing()
83             iconView.contentDescription = context.getString(
84                     R.string.biometric_dialog_face_icon_description_authenticating
85             )
86         } else if (oldState == STATE_PENDING_CONFIRMATION && newState == STATE_AUTHENTICATED) {
87             animateIconOnce(R.drawable.face_dialog_dark_to_checkmark)
88             iconView.contentDescription = context.getString(
89                     R.string.biometric_dialog_face_icon_description_confirmed
90             )
91         } else if (lastStateIsErrorIcon && newState == STATE_IDLE) {
92             animateIconOnce(R.drawable.face_dialog_error_to_idle)
93             iconView.contentDescription = context.getString(
94                     R.string.biometric_dialog_face_icon_description_idle
95             )
96         } else if (lastStateIsErrorIcon && newState == STATE_AUTHENTICATED) {
97             animateIconOnce(R.drawable.face_dialog_dark_to_checkmark)
98             iconView.contentDescription = context.getString(
99                     R.string.biometric_dialog_face_icon_description_authenticated
100             )
101         } else if (newState == STATE_ERROR && oldState != STATE_ERROR) {
102             animateIconOnce(R.drawable.face_dialog_dark_to_error)
103             iconView.contentDescription = context.getString(
104                     R.string.keyguard_face_failed
105             )
106         } else if (oldState == STATE_AUTHENTICATING && newState == STATE_AUTHENTICATED) {
107             animateIconOnce(R.drawable.face_dialog_dark_to_checkmark)
108             iconView.contentDescription = context.getString(
109                     R.string.biometric_dialog_face_icon_description_authenticated
110             )
111         } else if (newState == STATE_PENDING_CONFIRMATION) {
112             animateIconOnce(R.drawable.face_dialog_wink_from_dark)
113             iconView.contentDescription = context.getString(
114                     R.string.biometric_dialog_face_icon_description_authenticated
115             )
116         } else if (newState == STATE_IDLE) {
117             showStaticDrawable(R.drawable.face_dialog_idle_static)
118             iconView.contentDescription = context.getString(
119                     R.string.biometric_dialog_face_icon_description_idle
120             )
121         } else {
122             Log.w(TAG, "Unhandled state: $newState")
123         }
124         state = newState
125     }
126 }
127