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