1 /*
2  * Copyright (C) 2019 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.content.Context;
20 import android.graphics.drawable.Animatable2;
21 import android.graphics.drawable.AnimatedVectorDrawable;
22 import android.graphics.drawable.Drawable;
23 import android.hardware.biometrics.BiometricAuthenticator.Modality;
24 import android.os.Handler;
25 import android.os.Looper;
26 import android.util.AttributeSet;
27 import android.util.Log;
28 import android.view.View;
29 import android.widget.ImageView;
30 import android.widget.TextView;
31 
32 import androidx.annotation.NonNull;
33 import androidx.annotation.Nullable;
34 
35 import com.android.internal.annotations.VisibleForTesting;
36 import com.android.systemui.R;
37 
38 public class AuthBiometricFaceView extends AuthBiometricView {
39 
40     private static final String TAG = "BiometricPrompt/AuthBiometricFaceView";
41 
42     // Delay before dismissing after being authenticated/confirmed.
43     private static final int HIDE_DELAY_MS = 500;
44 
45     protected static class IconController extends Animatable2.AnimationCallback {
46         protected Context mContext;
47         protected ImageView mIconView;
48         protected TextView mTextView;
49         protected Handler mHandler;
50         protected boolean mLastPulseLightToDark; // false = dark to light, true = light to dark
51         protected @BiometricState int mState;
52         protected boolean mDeactivated;
53 
IconController(Context context, ImageView iconView, TextView textView)54         protected IconController(Context context, ImageView iconView, TextView textView) {
55             mContext = context;
56             mIconView = iconView;
57             mTextView = textView;
58             mHandler = new Handler(Looper.getMainLooper());
59             showStaticDrawable(R.drawable.face_dialog_pulse_dark_to_light);
60         }
61 
animateOnce(int iconRes)62         protected void animateOnce(int iconRes) {
63             animateIcon(iconRes, false);
64         }
65 
showStaticDrawable(int iconRes)66         protected void showStaticDrawable(int iconRes) {
67             mIconView.setImageDrawable(mContext.getDrawable(iconRes));
68         }
69 
animateIcon(int iconRes, boolean repeat)70         protected void animateIcon(int iconRes, boolean repeat) {
71             Log.d(TAG, "animateIcon, state: " + mState + ", deactivated: " + mDeactivated);
72             if (mDeactivated) {
73                 return;
74             }
75 
76             final AnimatedVectorDrawable icon =
77                     (AnimatedVectorDrawable) mContext.getDrawable(iconRes);
78             mIconView.setImageDrawable(icon);
79             icon.forceAnimationOnUI();
80             if (repeat) {
81                 icon.registerAnimationCallback(this);
82             }
83             icon.start();
84         }
85 
startPulsing()86         protected void startPulsing() {
87             mLastPulseLightToDark = false;
88             animateIcon(R.drawable.face_dialog_pulse_dark_to_light, true);
89         }
90 
pulseInNextDirection()91         protected void pulseInNextDirection() {
92             int iconRes = mLastPulseLightToDark ? R.drawable.face_dialog_pulse_dark_to_light
93                     : R.drawable.face_dialog_pulse_light_to_dark;
94             animateIcon(iconRes, true /* repeat */);
95             mLastPulseLightToDark = !mLastPulseLightToDark;
96         }
97 
98         @Override
onAnimationEnd(Drawable drawable)99         public void onAnimationEnd(Drawable drawable) {
100             super.onAnimationEnd(drawable);
101             Log.d(TAG, "onAnimationEnd, mState: " + mState + ", deactivated: " + mDeactivated);
102             if (mDeactivated) {
103                 return;
104             }
105 
106             if (mState == STATE_AUTHENTICATING || mState == STATE_HELP) {
107                 pulseInNextDirection();
108             }
109         }
110 
deactivate()111         protected void deactivate() {
112             mDeactivated = true;
113         }
114 
updateState(int lastState, int newState)115         protected void updateState(int lastState, int newState) {
116             if (mDeactivated) {
117                 Log.w(TAG, "Ignoring updateState when deactivated: " + newState);
118                 return;
119             }
120 
121             final boolean lastStateIsErrorIcon =
122                     lastState == STATE_ERROR || lastState == STATE_HELP;
123 
124             if (newState == STATE_AUTHENTICATING_ANIMATING_IN) {
125                 showStaticDrawable(R.drawable.face_dialog_pulse_dark_to_light);
126                 mIconView.setContentDescription(mContext.getString(
127                         R.string.biometric_dialog_face_icon_description_authenticating));
128             } else if (newState == STATE_AUTHENTICATING) {
129                 startPulsing();
130                 mIconView.setContentDescription(mContext.getString(
131                         R.string.biometric_dialog_face_icon_description_authenticating));
132             } else if (lastState == STATE_PENDING_CONFIRMATION && newState == STATE_AUTHENTICATED) {
133                 animateOnce(R.drawable.face_dialog_dark_to_checkmark);
134                 mIconView.setContentDescription(mContext.getString(
135                         R.string.biometric_dialog_face_icon_description_confirmed));
136             } else if (lastStateIsErrorIcon && newState == STATE_IDLE) {
137                 animateOnce(R.drawable.face_dialog_error_to_idle);
138                 mIconView.setContentDescription(mContext.getString(
139                         R.string.biometric_dialog_face_icon_description_idle));
140             } else if (lastStateIsErrorIcon && newState == STATE_AUTHENTICATED) {
141                 animateOnce(R.drawable.face_dialog_dark_to_checkmark);
142                 mIconView.setContentDescription(mContext.getString(
143                         R.string.biometric_dialog_face_icon_description_authenticated));
144             } else if (newState == STATE_ERROR && lastState != STATE_ERROR) {
145                 animateOnce(R.drawable.face_dialog_dark_to_error);
146             } else if (lastState == STATE_AUTHENTICATING && newState == STATE_AUTHENTICATED) {
147                 animateOnce(R.drawable.face_dialog_dark_to_checkmark);
148                 mIconView.setContentDescription(mContext.getString(
149                         R.string.biometric_dialog_face_icon_description_authenticated));
150             } else if (newState == STATE_PENDING_CONFIRMATION) {
151                 animateOnce(R.drawable.face_dialog_wink_from_dark);
152                 mIconView.setContentDescription(mContext.getString(
153                         R.string.biometric_dialog_face_icon_description_authenticated));
154             } else if (newState == STATE_IDLE) {
155                 showStaticDrawable(R.drawable.face_dialog_idle_static);
156                 mIconView.setContentDescription(mContext.getString(
157                         R.string.biometric_dialog_face_icon_description_idle));
158             } else {
159                 Log.w(TAG, "Unhandled state: " + newState);
160             }
161             mState = newState;
162         }
163     }
164 
165     @Nullable @VisibleForTesting IconController mFaceIconController;
166     @NonNull private final OnAttachStateChangeListener mOnAttachStateChangeListener =
167             new OnAttachStateChangeListener() {
168         @Override
169         public void onViewAttachedToWindow(View v) {
170 
171         }
172 
173         @Override
174         public void onViewDetachedFromWindow(View v) {
175             mFaceIconController.deactivate();
176         }
177     };
178 
AuthBiometricFaceView(Context context)179     public AuthBiometricFaceView(Context context) {
180         this(context, null);
181     }
182 
AuthBiometricFaceView(Context context, AttributeSet attrs)183     public AuthBiometricFaceView(Context context, AttributeSet attrs) {
184         super(context, attrs);
185     }
186 
187     @VisibleForTesting
AuthBiometricFaceView(Context context, AttributeSet attrs, Injector injector)188     AuthBiometricFaceView(Context context, AttributeSet attrs, Injector injector) {
189         super(context, attrs, injector);
190     }
191 
192     @Override
onFinishInflate()193     protected void onFinishInflate() {
194         super.onFinishInflate();
195         mFaceIconController = new IconController(mContext, mIconView, mIndicatorView);
196 
197         addOnAttachStateChangeListener(mOnAttachStateChangeListener);
198     }
199 
200     @Override
getDelayAfterAuthenticatedDurationMs()201     protected int getDelayAfterAuthenticatedDurationMs() {
202         return HIDE_DELAY_MS;
203     }
204 
205     @Override
getStateForAfterError()206     protected int getStateForAfterError() {
207         return STATE_IDLE;
208     }
209 
210     @Override
handleResetAfterError()211     protected void handleResetAfterError() {
212         resetErrorView();
213     }
214 
215     @Override
handleResetAfterHelp()216     protected void handleResetAfterHelp() {
217         resetErrorView();
218     }
219 
220     @Override
supportsSmallDialog()221     protected boolean supportsSmallDialog() {
222         return true;
223     }
224 
225     @Override
supportsManualRetry()226     protected boolean supportsManualRetry() {
227         return true;
228     }
229 
230     @Override
updateState(@iometricState int newState)231     public void updateState(@BiometricState int newState) {
232         mFaceIconController.updateState(mState, newState);
233 
234         if (newState == STATE_AUTHENTICATING_ANIMATING_IN ||
235                 (newState == STATE_AUTHENTICATING && getSize() == AuthDialog.SIZE_MEDIUM)) {
236             resetErrorView();
237         }
238 
239         // Do this last since the state variable gets updated.
240         super.updateState(newState);
241     }
242 
243     @Override
onAuthenticationFailed(@odality int modality, @Nullable String failureReason)244     public void onAuthenticationFailed(@Modality int modality, @Nullable String failureReason) {
245         if (getSize() == AuthDialog.SIZE_MEDIUM) {
246             if (supportsManualRetry()) {
247                 mTryAgainButton.setVisibility(View.VISIBLE);
248                 mConfirmButton.setVisibility(View.GONE);
249             }
250         }
251 
252         // Do this last since we want to know if the button is being animated (in the case of
253         // small -> medium dialog)
254         super.onAuthenticationFailed(modality, failureReason);
255     }
256 
resetErrorView()257     private void resetErrorView() {
258         mIndicatorView.setTextColor(mTextColorHint);
259         mIndicatorView.setVisibility(View.INVISIBLE);
260     }
261 }
262