Kevin Chyn | 6cf54e8 | 2018-09-18 19:13:27 -0700 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2018 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 | |
Kevin Chyn | e191271 | 2019-01-04 14:22:34 -0800 | [diff] [blame] | 19 | import android.animation.Animator; |
| 20 | import android.animation.AnimatorListenerAdapter; |
| 21 | import android.animation.AnimatorSet; |
| 22 | import android.animation.ValueAnimator; |
Kevin Chyn | 6cf54e8 | 2018-09-18 19:13:27 -0700 | [diff] [blame] | 23 | import android.content.Context; |
Kevin Chyn | e191271 | 2019-01-04 14:22:34 -0800 | [diff] [blame] | 24 | import android.graphics.Outline; |
Kevin Chyn | 3b53d6f | 2019-05-01 11:49:05 -0700 | [diff] [blame] | 25 | import android.graphics.drawable.Animatable2; |
| 26 | import android.graphics.drawable.AnimatedVectorDrawable; |
Kevin Chyn | 6cf54e8 | 2018-09-18 19:13:27 -0700 | [diff] [blame] | 27 | import android.graphics.drawable.Drawable; |
Kevin Chyn | 3b53d6f | 2019-05-01 11:49:05 -0700 | [diff] [blame] | 28 | import android.hardware.biometrics.BiometricPrompt; |
Kevin Chyn | e191271 | 2019-01-04 14:22:34 -0800 | [diff] [blame] | 29 | import android.os.Bundle; |
| 30 | import android.text.TextUtils; |
| 31 | import android.util.DisplayMetrics; |
Kevin Chyn | 3b53d6f | 2019-05-01 11:49:05 -0700 | [diff] [blame] | 32 | import android.util.Log; |
Kevin Chyn | e191271 | 2019-01-04 14:22:34 -0800 | [diff] [blame] | 33 | import android.view.View; |
| 34 | import android.view.ViewOutlineProvider; |
Kevin Chyn | 6cf54e8 | 2018-09-18 19:13:27 -0700 | [diff] [blame] | 35 | |
| 36 | import com.android.systemui.R; |
| 37 | |
| 38 | /** |
| 39 | * This class loads the view for the system-provided dialog. The view consists of: |
Kevin Chyn | 6bb2077 | 2018-12-27 15:14:44 -0800 | [diff] [blame] | 40 | * Application Icon, Title, Subtitle, Description, Biometric Icon, Error/Help message area, |
Kevin Chyn | 6cf54e8 | 2018-09-18 19:13:27 -0700 | [diff] [blame] | 41 | * and positive/negative buttons. |
| 42 | */ |
| 43 | public class FaceDialogView extends BiometricDialogView { |
Kevin Chyn | 6bb2077 | 2018-12-27 15:14:44 -0800 | [diff] [blame] | 44 | |
Kevin Chyn | e191271 | 2019-01-04 14:22:34 -0800 | [diff] [blame] | 45 | private static final String TAG = "FaceDialogView"; |
| 46 | private static final String KEY_DIALOG_SIZE = "key_dialog_size"; |
Kevin Chyn | 8cfd7a4 | 2019-05-07 20:22:31 -0700 | [diff] [blame] | 47 | private static final String KEY_DIALOG_ANIMATED_IN = "key_dialog_animated_in"; |
Kevin Chyn | e191271 | 2019-01-04 14:22:34 -0800 | [diff] [blame] | 48 | |
Kevin Chyn | 6bb2077 | 2018-12-27 15:14:44 -0800 | [diff] [blame] | 49 | private static final int HIDE_DIALOG_DELAY = 500; // ms |
Kevin Chyn | e191271 | 2019-01-04 14:22:34 -0800 | [diff] [blame] | 50 | private static final int IMPLICIT_Y_PADDING = 16; // dp |
| 51 | private static final int GROW_DURATION = 150; // ms |
| 52 | private static final int TEXT_ANIMATE_DISTANCE = 32; // dp |
| 53 | |
| 54 | private static final int SIZE_UNKNOWN = 0; |
| 55 | private static final int SIZE_SMALL = 1; |
| 56 | private static final int SIZE_GROWING = 2; |
| 57 | private static final int SIZE_BIG = 3; |
| 58 | |
| 59 | private int mSize; |
| 60 | private float mIconOriginalY; |
| 61 | private DialogOutlineProvider mOutlineProvider = new DialogOutlineProvider(); |
Kevin Chyn | 3b53d6f | 2019-05-01 11:49:05 -0700 | [diff] [blame] | 62 | private IconController mIconController; |
| 63 | private boolean mDialogAnimatedIn; |
| 64 | |
| 65 | /** |
| 66 | * Class that handles the biometric icon animations. |
| 67 | */ |
| 68 | private final class IconController extends Animatable2.AnimationCallback { |
| 69 | |
| 70 | private boolean mLastPulseDirection; // false = dark to light, true = light to dark |
| 71 | |
| 72 | int mState; |
| 73 | |
| 74 | IconController() { |
| 75 | mState = STATE_IDLE; |
| 76 | } |
| 77 | |
| 78 | public void animateOnce(int iconRes) { |
| 79 | animateIcon(iconRes, false); |
| 80 | } |
| 81 | |
Kevin Chyn | 8cfd7a4 | 2019-05-07 20:22:31 -0700 | [diff] [blame] | 82 | public void showStatic(int iconRes) { |
| 83 | mBiometricIcon.setImageDrawable(mContext.getDrawable(iconRes)); |
| 84 | } |
| 85 | |
Kevin Chyn | 3b53d6f | 2019-05-01 11:49:05 -0700 | [diff] [blame] | 86 | public void startPulsing() { |
| 87 | mLastPulseDirection = false; |
| 88 | animateIcon(R.drawable.face_dialog_pulse_dark_to_light, true); |
| 89 | } |
| 90 | |
| 91 | public void showIcon(int iconRes) { |
| 92 | final Drawable drawable = mContext.getDrawable(iconRes); |
| 93 | mBiometricIcon.setImageDrawable(drawable); |
| 94 | } |
| 95 | |
| 96 | private void animateIcon(int iconRes, boolean repeat) { |
| 97 | final AnimatedVectorDrawable icon = |
| 98 | (AnimatedVectorDrawable) mContext.getDrawable(iconRes); |
| 99 | mBiometricIcon.setImageDrawable(icon); |
| 100 | icon.forceAnimationOnUI(); |
| 101 | if (repeat) { |
| 102 | icon.registerAnimationCallback(this); |
| 103 | } |
| 104 | icon.start(); |
| 105 | } |
| 106 | |
| 107 | private void pulseInNextDirection() { |
| 108 | int iconRes = mLastPulseDirection ? R.drawable.face_dialog_pulse_dark_to_light |
| 109 | : R.drawable.face_dialog_pulse_light_to_dark; |
| 110 | animateIcon(iconRes, true /* repeat */); |
| 111 | mLastPulseDirection = !mLastPulseDirection; |
| 112 | } |
| 113 | |
| 114 | @Override |
| 115 | public void onAnimationEnd(Drawable drawable) { |
| 116 | super.onAnimationEnd(drawable); |
| 117 | |
| 118 | if (mState == STATE_AUTHENTICATING) { |
| 119 | // Still authenticating, pulse the icon |
| 120 | pulseInNextDirection(); |
| 121 | } |
| 122 | } |
| 123 | } |
Kevin Chyn | e191271 | 2019-01-04 14:22:34 -0800 | [diff] [blame] | 124 | |
| 125 | private final class DialogOutlineProvider extends ViewOutlineProvider { |
| 126 | |
| 127 | float mY; |
| 128 | |
| 129 | @Override |
| 130 | public void getOutline(View view, Outline outline) { |
| 131 | outline.setRoundRect( |
| 132 | 0 /* left */, |
| 133 | (int) mY, /* top */ |
| 134 | mDialog.getWidth() /* right */, |
| 135 | mDialog.getBottom(), /* bottom */ |
| 136 | getResources().getDimension(R.dimen.biometric_dialog_corner_size)); |
| 137 | } |
| 138 | |
| 139 | int calculateSmall() { |
| 140 | final float padding = dpToPixels(IMPLICIT_Y_PADDING); |
| 141 | return mDialog.getHeight() - mBiometricIcon.getHeight() - 2 * (int) padding; |
| 142 | } |
| 143 | |
| 144 | void setOutlineY(float y) { |
| 145 | mY = y; |
| 146 | } |
| 147 | } |
Kevin Chyn | 6bb2077 | 2018-12-27 15:14:44 -0800 | [diff] [blame] | 148 | |
Kevin Chyn | 3b53d6f | 2019-05-01 11:49:05 -0700 | [diff] [blame] | 149 | private final Runnable mErrorToIdleAnimationRunnable = () -> { |
| 150 | updateState(STATE_IDLE); |
| 151 | mErrorText.setVisibility(View.INVISIBLE); |
| 152 | }; |
| 153 | |
Kevin Chyn | 6cf54e8 | 2018-09-18 19:13:27 -0700 | [diff] [blame] | 154 | public FaceDialogView(Context context, |
| 155 | DialogViewCallback callback) { |
| 156 | super(context, callback); |
Kevin Chyn | 3b53d6f | 2019-05-01 11:49:05 -0700 | [diff] [blame] | 157 | mIconController = new IconController(); |
Kevin Chyn | 6cf54e8 | 2018-09-18 19:13:27 -0700 | [diff] [blame] | 158 | } |
| 159 | |
Kevin Chyn | e191271 | 2019-01-04 14:22:34 -0800 | [diff] [blame] | 160 | private void updateSize(int newSize) { |
| 161 | final float padding = dpToPixels(IMPLICIT_Y_PADDING); |
| 162 | final float iconSmallPositionY = mDialog.getHeight() - mBiometricIcon.getHeight() - padding; |
| 163 | |
| 164 | if (newSize == SIZE_SMALL) { |
| 165 | // These fields are required and/or always hold a spot on the UI, so should be set to |
| 166 | // INVISIBLE so they keep their position |
| 167 | mTitleText.setVisibility(View.INVISIBLE); |
| 168 | mErrorText.setVisibility(View.INVISIBLE); |
| 169 | mNegativeButton.setVisibility(View.INVISIBLE); |
| 170 | |
| 171 | // These fields are optional, so set them to gone or invisible depending on their |
| 172 | // usage. If they're empty, they're already set to GONE in BiometricDialogView. |
| 173 | if (!TextUtils.isEmpty(mSubtitleText.getText())) { |
| 174 | mSubtitleText.setVisibility(View.INVISIBLE); |
| 175 | } |
| 176 | if (!TextUtils.isEmpty(mDescriptionText.getText())) { |
| 177 | mDescriptionText.setVisibility(View.INVISIBLE); |
| 178 | } |
| 179 | |
| 180 | // Move the biometric icon to the small spot |
| 181 | mBiometricIcon.setY(iconSmallPositionY); |
| 182 | |
| 183 | // Clip the dialog to the small size |
| 184 | mDialog.setOutlineProvider(mOutlineProvider); |
| 185 | mOutlineProvider.setOutlineY(mOutlineProvider.calculateSmall()); |
| 186 | |
| 187 | mDialog.setClipToOutline(true); |
| 188 | mDialog.invalidateOutline(); |
| 189 | |
| 190 | mSize = newSize; |
| 191 | } else if (mSize == SIZE_SMALL && newSize == SIZE_BIG) { |
| 192 | mSize = SIZE_GROWING; |
| 193 | |
| 194 | // Animate the outline |
| 195 | final ValueAnimator outlineAnimator = |
| 196 | ValueAnimator.ofFloat(mOutlineProvider.calculateSmall(), 0); |
| 197 | outlineAnimator.addUpdateListener((animation) -> { |
| 198 | final float y = (float) animation.getAnimatedValue(); |
| 199 | mOutlineProvider.setOutlineY(y); |
| 200 | mDialog.invalidateOutline(); |
| 201 | }); |
| 202 | |
| 203 | // Animate the icon back to original big position |
| 204 | final ValueAnimator iconAnimator = |
| 205 | ValueAnimator.ofFloat(iconSmallPositionY, mIconOriginalY); |
| 206 | iconAnimator.addUpdateListener((animation) -> { |
| 207 | final float y = (float) animation.getAnimatedValue(); |
| 208 | mBiometricIcon.setY(y); |
| 209 | }); |
| 210 | |
| 211 | // Animate the error text so it slides up with the icon |
| 212 | final ValueAnimator textSlideAnimator = |
| 213 | ValueAnimator.ofFloat(dpToPixels(TEXT_ANIMATE_DISTANCE), 0); |
| 214 | textSlideAnimator.addUpdateListener((animation) -> { |
| 215 | final float y = (float) animation.getAnimatedValue(); |
| 216 | mErrorText.setTranslationY(y); |
| 217 | }); |
| 218 | |
| 219 | // Opacity animator for things that should fade in (title, subtitle, details, negative |
| 220 | // button) |
| 221 | final ValueAnimator opacityAnimator = ValueAnimator.ofFloat(0, 1); |
| 222 | opacityAnimator.addUpdateListener((animation) -> { |
| 223 | final float opacity = (float) animation.getAnimatedValue(); |
| 224 | |
| 225 | // These fields are required and/or always hold a spot on the UI |
| 226 | mTitleText.setAlpha(opacity); |
| 227 | mErrorText.setAlpha(opacity); |
| 228 | mNegativeButton.setAlpha(opacity); |
| 229 | mTryAgainButton.setAlpha(opacity); |
| 230 | |
| 231 | // These fields are optional, so only animate them if they're supposed to be showing |
| 232 | if (!TextUtils.isEmpty(mSubtitleText.getText())) { |
| 233 | mSubtitleText.setAlpha(opacity); |
| 234 | } |
| 235 | if (!TextUtils.isEmpty(mDescriptionText.getText())) { |
| 236 | mDescriptionText.setAlpha(opacity); |
| 237 | } |
| 238 | }); |
| 239 | |
| 240 | // Choreograph together |
| 241 | final AnimatorSet as = new AnimatorSet(); |
| 242 | as.setDuration(GROW_DURATION); |
| 243 | as.addListener(new AnimatorListenerAdapter() { |
| 244 | @Override |
| 245 | public void onAnimationStart(Animator animation) { |
| 246 | super.onAnimationStart(animation); |
| 247 | // Set the visibility of opacity-animating views back to VISIBLE |
| 248 | mTitleText.setVisibility(View.VISIBLE); |
| 249 | mErrorText.setVisibility(View.VISIBLE); |
| 250 | mNegativeButton.setVisibility(View.VISIBLE); |
| 251 | mTryAgainButton.setVisibility(View.VISIBLE); |
| 252 | |
| 253 | if (!TextUtils.isEmpty(mSubtitleText.getText())) { |
| 254 | mSubtitleText.setVisibility(View.VISIBLE); |
| 255 | } |
| 256 | if (!TextUtils.isEmpty(mDescriptionText.getText())) { |
| 257 | mDescriptionText.setVisibility(View.VISIBLE); |
| 258 | } |
| 259 | } |
| 260 | |
| 261 | @Override |
| 262 | public void onAnimationEnd(Animator animation) { |
| 263 | super.onAnimationEnd(animation); |
| 264 | mSize = SIZE_BIG; |
| 265 | } |
| 266 | }); |
| 267 | as.play(outlineAnimator).with(iconAnimator).with(opacityAnimator) |
| 268 | .with(textSlideAnimator); |
| 269 | as.start(); |
| 270 | } else if (mSize == SIZE_BIG) { |
| 271 | mDialog.setClipToOutline(false); |
| 272 | mDialog.invalidateOutline(); |
| 273 | |
| 274 | mBiometricIcon.setY(mIconOriginalY); |
| 275 | |
| 276 | mSize = newSize; |
| 277 | } |
| 278 | } |
| 279 | |
| 280 | @Override |
| 281 | public void onSaveState(Bundle bundle) { |
| 282 | super.onSaveState(bundle); |
| 283 | bundle.putInt(KEY_DIALOG_SIZE, mSize); |
Kevin Chyn | 8cfd7a4 | 2019-05-07 20:22:31 -0700 | [diff] [blame] | 284 | bundle.putBoolean(KEY_DIALOG_ANIMATED_IN, mDialogAnimatedIn); |
Kevin Chyn | e191271 | 2019-01-04 14:22:34 -0800 | [diff] [blame] | 285 | } |
| 286 | |
Kevin Chyn | c9744ac | 2019-01-11 18:49:50 -0800 | [diff] [blame] | 287 | |
| 288 | @Override |
Kevin Chyn | 8cfd7a4 | 2019-05-07 20:22:31 -0700 | [diff] [blame] | 289 | protected void handleResetMessage() { |
Kevin Chyn | 3b53d6f | 2019-05-01 11:49:05 -0700 | [diff] [blame] | 290 | mErrorText.setText(getHintStringResourceId()); |
Kevin Chyn | bb79c00 | 2019-05-14 12:53:20 -0700 | [diff] [blame] | 291 | mErrorText.setContentDescription(mContext.getString(getHintStringResourceId())); |
Kevin Chyn | 3b53d6f | 2019-05-01 11:49:05 -0700 | [diff] [blame] | 292 | mErrorText.setTextColor(mTextColor); |
Kevin Chyn | 8cfd7a4 | 2019-05-07 20:22:31 -0700 | [diff] [blame] | 293 | if (getState() == STATE_AUTHENTICATING) { |
| 294 | mErrorText.setVisibility(View.VISIBLE); |
| 295 | } else { |
| 296 | mErrorText.setVisibility(View.INVISIBLE); |
| 297 | } |
Kevin Chyn | c9744ac | 2019-01-11 18:49:50 -0800 | [diff] [blame] | 298 | } |
| 299 | |
Kevin Chyn | e191271 | 2019-01-04 14:22:34 -0800 | [diff] [blame] | 300 | @Override |
| 301 | public void restoreState(Bundle bundle) { |
| 302 | super.restoreState(bundle); |
| 303 | // Keep in mind that this happens before onAttachedToWindow() |
| 304 | mSize = bundle.getInt(KEY_DIALOG_SIZE); |
Kevin Chyn | 8cfd7a4 | 2019-05-07 20:22:31 -0700 | [diff] [blame] | 305 | mDialogAnimatedIn = bundle.getBoolean(KEY_DIALOG_ANIMATED_IN); |
Kevin Chyn | e191271 | 2019-01-04 14:22:34 -0800 | [diff] [blame] | 306 | } |
| 307 | |
| 308 | /** |
| 309 | * Do small/big layout here instead of onAttachedToWindow, since: |
| 310 | * 1) We need the big layout to be measured, etc for small -> big animation |
| 311 | * 2) We need the dialog measurements to know where to move the biometric icon to |
| 312 | * |
| 313 | * BiometricDialogView already sets the views to their default big state, so here we only |
| 314 | * need to hide the ones that are unnecessary. |
| 315 | */ |
| 316 | @Override |
| 317 | public void onLayout(boolean changed, int left, int top, int right, int bottom) { |
| 318 | super.onLayout(changed, left, top, right, bottom); |
| 319 | |
| 320 | if (mIconOriginalY == 0) { |
| 321 | mIconOriginalY = mBiometricIcon.getY(); |
| 322 | } |
| 323 | |
| 324 | // UNKNOWN means size hasn't been set yet. First time we create the dialog. |
| 325 | // onLayout can happen when visibility of views change (during animation, etc). |
| 326 | if (mSize != SIZE_UNKNOWN) { |
| 327 | // Probably not the cleanest way to do this, but since dialog is big by default, |
| 328 | // and small dialogs can persist across orientation changes, we need to set it to |
| 329 | // small size here again. |
| 330 | if (mSize == SIZE_SMALL) { |
| 331 | updateSize(SIZE_SMALL); |
| 332 | } |
| 333 | return; |
| 334 | } |
| 335 | |
| 336 | // If we don't require confirmation, show the small dialog first (until errors occur). |
| 337 | if (!requiresConfirmation()) { |
| 338 | updateSize(SIZE_SMALL); |
| 339 | } else { |
| 340 | updateSize(SIZE_BIG); |
| 341 | } |
| 342 | } |
| 343 | |
| 344 | @Override |
Kevin Chyn | 3b53d6f | 2019-05-01 11:49:05 -0700 | [diff] [blame] | 345 | public void onErrorReceived(String error) { |
| 346 | super.onErrorReceived(error); |
Kevin Chyn | e191271 | 2019-01-04 14:22:34 -0800 | [diff] [blame] | 347 | // All error messages will cause the dialog to go from small -> big. Error messages |
| 348 | // are messages such as lockout, auth failed, etc. |
| 349 | if (mSize == SIZE_SMALL) { |
| 350 | updateSize(SIZE_BIG); |
| 351 | } |
| 352 | } |
| 353 | |
| 354 | @Override |
Kevin Chyn | 3b53d6f | 2019-05-01 11:49:05 -0700 | [diff] [blame] | 355 | public void onAuthenticationFailed(String message) { |
| 356 | super.onAuthenticationFailed(message); |
| 357 | showTryAgainButton(true); |
| 358 | } |
| 359 | |
| 360 | @Override |
Kevin Chyn | e191271 | 2019-01-04 14:22:34 -0800 | [diff] [blame] | 361 | public void showTryAgainButton(boolean show) { |
| 362 | if (show && mSize == SIZE_SMALL) { |
| 363 | // Do not call super, we will nicely animate the alpha together with the rest |
| 364 | // of the elements in here. |
| 365 | updateSize(SIZE_BIG); |
| 366 | } else { |
Kevin Chyn | c9744ac | 2019-01-11 18:49:50 -0800 | [diff] [blame] | 367 | if (show) { |
| 368 | mTryAgainButton.setVisibility(View.VISIBLE); |
| 369 | } else { |
| 370 | mTryAgainButton.setVisibility(View.GONE); |
| 371 | } |
Kevin Chyn | e191271 | 2019-01-04 14:22:34 -0800 | [diff] [blame] | 372 | } |
Kevin Chyn | 8d39b4e | 2019-04-26 11:02:13 -0700 | [diff] [blame] | 373 | |
| 374 | if (show) { |
| 375 | mPositiveButton.setVisibility(View.GONE); |
Kevin Chyn | 8d39b4e | 2019-04-26 11:02:13 -0700 | [diff] [blame] | 376 | } |
Kevin Chyn | e191271 | 2019-01-04 14:22:34 -0800 | [diff] [blame] | 377 | } |
| 378 | |
Kevin Chyn | 6cf54e8 | 2018-09-18 19:13:27 -0700 | [diff] [blame] | 379 | @Override |
| 380 | protected int getHintStringResourceId() { |
| 381 | return R.string.face_dialog_looking_for_face; |
| 382 | } |
| 383 | |
| 384 | @Override |
| 385 | protected int getAuthenticatedAccessibilityResourceId() { |
| 386 | if (mRequireConfirmation) { |
| 387 | return com.android.internal.R.string.face_authenticated_confirmation_required; |
| 388 | } else { |
| 389 | return com.android.internal.R.string.face_authenticated_no_confirmation_required; |
| 390 | } |
| 391 | } |
| 392 | |
| 393 | @Override |
| 394 | protected int getIconDescriptionResourceId() { |
| 395 | return R.string.accessibility_face_dialog_face_icon; |
| 396 | } |
| 397 | |
| 398 | @Override |
Kevin Chyn | 3b53d6f | 2019-05-01 11:49:05 -0700 | [diff] [blame] | 399 | protected void updateIcon(int oldState, int newState) { |
| 400 | mIconController.mState = newState; |
| 401 | |
Kevin Chyn | 8cfd7a4 | 2019-05-07 20:22:31 -0700 | [diff] [blame] | 402 | if (newState == STATE_AUTHENTICATING) { |
| 403 | mHandler.removeCallbacks(mErrorToIdleAnimationRunnable); |
Kevin Chyn | 3b53d6f | 2019-05-01 11:49:05 -0700 | [diff] [blame] | 404 | if (mDialogAnimatedIn) { |
| 405 | mIconController.startPulsing(); |
| 406 | mErrorText.setVisibility(View.VISIBLE); |
| 407 | } else { |
| 408 | mIconController.showIcon(R.drawable.face_dialog_pulse_dark_to_light); |
| 409 | } |
Kevin Chyn | bb79c00 | 2019-05-14 12:53:20 -0700 | [diff] [blame] | 410 | mBiometricIcon.setContentDescription(mContext.getString( |
| 411 | R.string.biometric_dialog_face_icon_description_authenticating)); |
Kevin Chyn | 6bb2077 | 2018-12-27 15:14:44 -0800 | [diff] [blame] | 412 | } else if (oldState == STATE_PENDING_CONFIRMATION && newState == STATE_AUTHENTICATED) { |
Kevin Chyn | 3b53d6f | 2019-05-01 11:49:05 -0700 | [diff] [blame] | 413 | mIconController.animateOnce(R.drawable.face_dialog_dark_to_checkmark); |
Kevin Chyn | bb79c00 | 2019-05-14 12:53:20 -0700 | [diff] [blame] | 414 | mBiometricIcon.setContentDescription(mContext.getString( |
| 415 | R.string.biometric_dialog_face_icon_description_confirmed)); |
Kevin Chyn | 3b53d6f | 2019-05-01 11:49:05 -0700 | [diff] [blame] | 416 | } else if (oldState == STATE_ERROR && newState == STATE_IDLE) { |
| 417 | mIconController.animateOnce(R.drawable.face_dialog_error_to_idle); |
Kevin Chyn | bb79c00 | 2019-05-14 12:53:20 -0700 | [diff] [blame] | 418 | mBiometricIcon.setContentDescription(mContext.getString( |
| 419 | R.string.biometric_dialog_face_icon_description_idle)); |
Kevin Chyn | a60118c | 2019-05-07 16:02:53 -0700 | [diff] [blame] | 420 | } else if (oldState == STATE_ERROR && newState == STATE_AUTHENTICATED) { |
| 421 | mHandler.removeCallbacks(mErrorToIdleAnimationRunnable); |
| 422 | mIconController.animateOnce(R.drawable.face_dialog_dark_to_checkmark); |
Kevin Chyn | bb79c00 | 2019-05-14 12:53:20 -0700 | [diff] [blame] | 423 | mBiometricIcon.setContentDescription(mContext.getString( |
| 424 | R.string.biometric_dialog_face_icon_description_authenticated)); |
Kevin Chyn | 8cfd7a4 | 2019-05-07 20:22:31 -0700 | [diff] [blame] | 425 | } else if (newState == STATE_ERROR) { |
| 426 | // It's easier to only check newState and gate showing the animation on the |
| 427 | // mErrorToIdleAnimationRunnable as a proxy, than add a ton of extra state. For example, |
| 428 | // we may go from error -> error due to configuration change which is valid and we |
| 429 | // should show the animation, or we can go from error -> error by receiving repeated |
| 430 | // acquire messages in which case we do not want to repeatedly start the animation. |
| 431 | if (!mHandler.hasCallbacks(mErrorToIdleAnimationRunnable)) { |
| 432 | mIconController.animateOnce(R.drawable.face_dialog_dark_to_error); |
| 433 | mHandler.postDelayed(mErrorToIdleAnimationRunnable, |
| 434 | BiometricPrompt.HIDE_DIALOG_DELAY); |
| 435 | } |
Kevin Chyn | 6bb2077 | 2018-12-27 15:14:44 -0800 | [diff] [blame] | 436 | } else if (oldState == STATE_AUTHENTICATING && newState == STATE_AUTHENTICATED) { |
Kevin Chyn | 3b53d6f | 2019-05-01 11:49:05 -0700 | [diff] [blame] | 437 | mIconController.animateOnce(R.drawable.face_dialog_dark_to_checkmark); |
Kevin Chyn | bb79c00 | 2019-05-14 12:53:20 -0700 | [diff] [blame] | 438 | mBiometricIcon.setContentDescription(mContext.getString( |
| 439 | R.string.biometric_dialog_face_icon_description_authenticated)); |
Kevin Chyn | 8cfd7a4 | 2019-05-07 20:22:31 -0700 | [diff] [blame] | 440 | } else if (newState == STATE_PENDING_CONFIRMATION) { |
| 441 | mHandler.removeCallbacks(mErrorToIdleAnimationRunnable); |
Kevin Chyn | 3b53d6f | 2019-05-01 11:49:05 -0700 | [diff] [blame] | 442 | mIconController.animateOnce(R.drawable.face_dialog_wink_from_dark); |
Kevin Chyn | bb79c00 | 2019-05-14 12:53:20 -0700 | [diff] [blame] | 443 | mBiometricIcon.setContentDescription(mContext.getString( |
| 444 | R.string.biometric_dialog_face_icon_description_authenticated)); |
Kevin Chyn | 8cfd7a4 | 2019-05-07 20:22:31 -0700 | [diff] [blame] | 445 | } else if (newState == STATE_IDLE) { |
| 446 | mIconController.showStatic(R.drawable.face_dialog_idle_static); |
Kevin Chyn | bb79c00 | 2019-05-14 12:53:20 -0700 | [diff] [blame] | 447 | mBiometricIcon.setContentDescription(mContext.getString( |
| 448 | R.string.biometric_dialog_face_icon_description_idle)); |
Kevin Chyn | 3b53d6f | 2019-05-01 11:49:05 -0700 | [diff] [blame] | 449 | } else { |
| 450 | Log.w(TAG, "Unknown animation from " + oldState + " -> " + newState); |
Kevin Chyn | 6bb2077 | 2018-12-27 15:14:44 -0800 | [diff] [blame] | 451 | } |
Kevin Chyn | 8cfd7a4 | 2019-05-07 20:22:31 -0700 | [diff] [blame] | 452 | |
| 453 | // Note that this must be after the newState == STATE_ERROR check above since this affects |
| 454 | // the logic. |
| 455 | if (oldState == STATE_ERROR && newState == STATE_ERROR) { |
| 456 | // Keep the error icon and text around for a while longer if we keep receiving |
| 457 | // STATE_ERROR |
| 458 | mHandler.removeCallbacks(mErrorToIdleAnimationRunnable); |
| 459 | mHandler.postDelayed(mErrorToIdleAnimationRunnable, BiometricPrompt.HIDE_DIALOG_DELAY); |
| 460 | } |
Kevin Chyn | 3b53d6f | 2019-05-01 11:49:05 -0700 | [diff] [blame] | 461 | } |
| 462 | |
| 463 | @Override |
| 464 | public void onDialogAnimatedIn() { |
| 465 | mDialogAnimatedIn = true; |
| 466 | mIconController.startPulsing(); |
Kevin Chyn | 6bb2077 | 2018-12-27 15:14:44 -0800 | [diff] [blame] | 467 | } |
Kevin Chyn | 6cf54e8 | 2018-09-18 19:13:27 -0700 | [diff] [blame] | 468 | |
Kevin Chyn | 6bb2077 | 2018-12-27 15:14:44 -0800 | [diff] [blame] | 469 | @Override |
| 470 | protected int getDelayAfterAuthenticatedDurationMs() { |
| 471 | return HIDE_DIALOG_DELAY; |
| 472 | } |
| 473 | |
| 474 | @Override |
Kevin Chyn | e191271 | 2019-01-04 14:22:34 -0800 | [diff] [blame] | 475 | protected boolean shouldGrayAreaDismissDialog() { |
| 476 | if (mSize == SIZE_SMALL) { |
| 477 | return false; |
| 478 | } |
| 479 | return true; |
| 480 | } |
| 481 | |
Kevin Chyn | e191271 | 2019-01-04 14:22:34 -0800 | [diff] [blame] | 482 | private float dpToPixels(float dp) { |
| 483 | return dp * ((float) mContext.getResources().getDisplayMetrics().densityDpi |
| 484 | / DisplayMetrics.DENSITY_DEFAULT); |
| 485 | } |
| 486 | |
| 487 | private float pixelsToDp(float pixels) { |
| 488 | return pixels / ((float) mContext.getResources().getDisplayMetrics().densityDpi |
| 489 | / DisplayMetrics.DENSITY_DEFAULT); |
| 490 | } |
Kevin Chyn | 6cf54e8 | 2018-09-18 19:13:27 -0700 | [diff] [blame] | 491 | } |