• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.annotation.NonNull;
20 import android.content.Context;
21 import android.graphics.drawable.Animatable2;
22 import android.graphics.drawable.AnimatedVectorDrawable;
23 import android.graphics.drawable.Drawable;
24 import android.hardware.biometrics.BiometricAuthenticator.Modality;
25 import android.os.Handler;
26 import android.os.Looper;
27 import android.util.AttributeSet;
28 import android.util.Log;
29 import android.view.View;
30 import android.widget.ImageView;
31 import android.widget.TextView;
32 
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 
AuthBiometricFaceView(Context context)167     public AuthBiometricFaceView(Context context) {
168         this(context, null);
169     }
170 
AuthBiometricFaceView(Context context, AttributeSet attrs)171     public AuthBiometricFaceView(Context context, AttributeSet attrs) {
172         super(context, attrs);
173     }
174 
175     @VisibleForTesting
AuthBiometricFaceView(Context context, AttributeSet attrs, Injector injector)176     AuthBiometricFaceView(Context context, AttributeSet attrs, Injector injector) {
177         super(context, attrs, injector);
178     }
179 
180     @Override
onFinishInflate()181     protected void onFinishInflate() {
182         super.onFinishInflate();
183         mFaceIconController = new IconController(mContext, mIconView, mIndicatorView);
184     }
185 
186     @Override
getDelayAfterAuthenticatedDurationMs()187     protected int getDelayAfterAuthenticatedDurationMs() {
188         return HIDE_DELAY_MS;
189     }
190 
191     @Override
getStateForAfterError()192     protected int getStateForAfterError() {
193         return STATE_IDLE;
194     }
195 
196     @Override
handleResetAfterError()197     protected void handleResetAfterError() {
198         resetErrorView();
199     }
200 
201     @Override
handleResetAfterHelp()202     protected void handleResetAfterHelp() {
203         resetErrorView();
204     }
205 
206     @Override
supportsSmallDialog()207     protected boolean supportsSmallDialog() {
208         return true;
209     }
210 
211     @Override
supportsManualRetry()212     protected boolean supportsManualRetry() {
213         return true;
214     }
215 
216     @Override
updateState(@iometricState int newState)217     public void updateState(@BiometricState int newState) {
218         mFaceIconController.updateState(mState, newState);
219 
220         if (newState == STATE_AUTHENTICATING_ANIMATING_IN ||
221                 (newState == STATE_AUTHENTICATING && getSize() == AuthDialog.SIZE_MEDIUM)) {
222             resetErrorView();
223         }
224 
225         // Do this last since the state variable gets updated.
226         super.updateState(newState);
227     }
228 
229     @Override
onAuthenticationFailed(@odality int modality, @Nullable String failureReason)230     public void onAuthenticationFailed(@Modality int modality, @Nullable String failureReason) {
231         if (getSize() == AuthDialog.SIZE_MEDIUM) {
232             if (supportsManualRetry()) {
233                 mTryAgainButton.setVisibility(View.VISIBLE);
234                 mConfirmButton.setVisibility(View.GONE);
235             }
236         }
237 
238         // Do this last since we want to know if the button is being animated (in the case of
239         // small -> medium dialog)
240         super.onAuthenticationFailed(modality, failureReason);
241     }
242 
resetErrorView()243     private void resetErrorView() {
244         mIndicatorView.setTextColor(mTextColorHint);
245         mIndicatorView.setVisibility(View.INVISIBLE);
246     }
247 }
248