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