1 /* 2 * Copyright (C) 2015 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.settings.biometrics.fingerprint; 18 19 import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_USER_CANCELED; 20 import static android.text.Layout.HYPHENATION_FREQUENCY_NONE; 21 22 import android.animation.Animator; 23 import android.animation.ObjectAnimator; 24 import android.annotation.IntDef; 25 import android.annotation.NonNull; 26 import android.annotation.Nullable; 27 import android.annotation.RawRes; 28 import android.app.Dialog; 29 import android.app.settings.SettingsEnums; 30 import android.content.Context; 31 import android.content.DialogInterface; 32 import android.content.Intent; 33 import android.content.res.ColorStateList; 34 import android.content.res.Configuration; 35 import android.content.res.Resources; 36 import android.graphics.PorterDuff; 37 import android.graphics.PorterDuffColorFilter; 38 import android.graphics.drawable.Animatable2; 39 import android.graphics.drawable.AnimatedVectorDrawable; 40 import android.graphics.drawable.Drawable; 41 import android.graphics.drawable.LayerDrawable; 42 import android.hardware.fingerprint.FingerprintManager; 43 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; 44 import android.os.Bundle; 45 import android.os.Process; 46 import android.os.VibrationAttributes; 47 import android.os.VibrationEffect; 48 import android.os.Vibrator; 49 import android.text.TextUtils; 50 import android.util.Log; 51 import android.view.MotionEvent; 52 import android.view.OrientationEventListener; 53 import android.view.Surface; 54 import android.view.View; 55 import android.view.accessibility.AccessibilityEvent; 56 import android.view.accessibility.AccessibilityManager; 57 import android.view.animation.AccelerateDecelerateInterpolator; 58 import android.view.animation.AnimationUtils; 59 import android.view.animation.Interpolator; 60 import android.widget.LinearLayout; 61 import android.widget.ProgressBar; 62 import android.widget.RelativeLayout; 63 import android.widget.TextView; 64 65 import androidx.annotation.IdRes; 66 import androidx.appcompat.app.AlertDialog; 67 68 import com.android.internal.annotations.VisibleForTesting; 69 import com.android.settings.R; 70 import com.android.settings.biometrics.BiometricEnrollSidecar; 71 import com.android.settings.biometrics.BiometricUtils; 72 import com.android.settings.biometrics.BiometricsEnrollEnrolling; 73 import com.android.settings.core.instrumentation.InstrumentedDialogFragment; 74 import com.android.settingslib.display.DisplayDensityUtils; 75 76 import com.airbnb.lottie.LottieAnimationView; 77 import com.airbnb.lottie.LottieCompositionFactory; 78 import com.airbnb.lottie.LottieProperty; 79 import com.airbnb.lottie.model.KeyPath; 80 import com.google.android.setupcompat.template.FooterBarMixin; 81 import com.google.android.setupcompat.template.FooterButton; 82 import com.google.android.setupcompat.util.WizardManagerHelper; 83 import com.google.android.setupdesign.GlifLayout; 84 import com.google.android.setupdesign.template.DescriptionMixin; 85 import com.google.android.setupdesign.template.HeaderMixin; 86 87 import java.lang.annotation.Retention; 88 import java.lang.annotation.RetentionPolicy; 89 import java.util.List; 90 import java.util.Locale; 91 92 /** 93 * Activity which handles the actual enrolling for fingerprint. 94 */ 95 public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling { 96 97 private static final String TAG = "FingerprintEnrollEnrolling"; 98 static final String TAG_SIDECAR = "sidecar"; 99 static final String KEY_STATE_CANCELED = "is_canceled"; 100 static final String KEY_STATE_PREVIOUS_ROTATION = "previous_rotation"; 101 102 private static final int PROGRESS_BAR_MAX = 10000; 103 104 private static final int STAGE_UNKNOWN = -1; 105 private static final int STAGE_CENTER = 0; 106 private static final int STAGE_GUIDED = 1; 107 private static final int STAGE_FINGERTIP = 2; 108 private static final int STAGE_LEFT_EDGE = 3; 109 private static final int STAGE_RIGHT_EDGE = 4; 110 111 @VisibleForTesting 112 protected static final int SFPS_STAGE_NO_ANIMATION = 0; 113 114 @VisibleForTesting 115 protected static final int SFPS_STAGE_CENTER = 1; 116 117 @VisibleForTesting 118 protected static final int SFPS_STAGE_FINGERTIP = 2; 119 120 @VisibleForTesting 121 protected static final int SFPS_STAGE_LEFT_EDGE = 3; 122 123 @VisibleForTesting 124 protected static final int SFPS_STAGE_RIGHT_EDGE = 4; 125 126 @IntDef({STAGE_UNKNOWN, STAGE_CENTER, STAGE_GUIDED, STAGE_FINGERTIP, STAGE_LEFT_EDGE, 127 STAGE_RIGHT_EDGE}) 128 @Retention(RetentionPolicy.SOURCE) 129 private @interface EnrollStage {} 130 131 132 @VisibleForTesting 133 @IntDef({STAGE_UNKNOWN, SFPS_STAGE_NO_ANIMATION, SFPS_STAGE_CENTER, SFPS_STAGE_FINGERTIP, 134 SFPS_STAGE_LEFT_EDGE, SFPS_STAGE_RIGHT_EDGE}) 135 @Retention(RetentionPolicy.SOURCE) 136 protected @interface SfpsEnrollStage {} 137 138 /** 139 * If we don't see progress during this time, we show an error message to remind the users that 140 * they need to lift the finger and touch again. 141 */ 142 private static final int HINT_TIMEOUT_DURATION = 2500; 143 144 /** 145 * How long the user needs to touch the icon until we show the dialog. 146 */ 147 private static final long ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN = 500; 148 149 /** 150 * How many times the user needs to touch the icon until we show the dialog that this is not the 151 * fingerprint sensor. 152 */ 153 private static final int ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN = 3; 154 155 private static final VibrationEffect VIBRATE_EFFECT_ERROR = 156 VibrationEffect.createWaveform(new long[] {0, 5, 55, 60}, -1); 157 private static final VibrationAttributes FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES = 158 VibrationAttributes.createForUsage(VibrationAttributes.USAGE_ACCESSIBILITY); 159 160 private FingerprintManager mFingerprintManager; 161 private boolean mCanAssumeUdfps; 162 private boolean mCanAssumeSfps; 163 @Nullable private ProgressBar mProgressBar; 164 private ObjectAnimator mProgressAnim; 165 private TextView mErrorText; 166 private Interpolator mFastOutSlowInInterpolator; 167 private Interpolator mLinearOutSlowInInterpolator; 168 private Interpolator mFastOutLinearInInterpolator; 169 private int mIconTouchCount; 170 private boolean mAnimationCancelled; 171 @Nullable private AnimatedVectorDrawable mIconAnimationDrawable; 172 @Nullable private AnimatedVectorDrawable mIconBackgroundBlinksDrawable; 173 private boolean mRestoring; 174 private Vibrator mVibrator; 175 private boolean mIsSetupWizard; 176 private boolean mIsOrientationChanged; 177 @VisibleForTesting 178 boolean mIsCanceled; 179 private AccessibilityManager mAccessibilityManager; 180 private boolean mIsAccessibilityEnabled; 181 private LottieAnimationView mIllustrationLottie; 182 private boolean mHaveShownUdfpsTipLottie; 183 private boolean mHaveShownUdfpsLeftEdgeLottie; 184 private boolean mHaveShownUdfpsRightEdgeLottie; 185 private boolean mHaveShownSfpsNoAnimationLottie; 186 private boolean mHaveShownSfpsCenterLottie; 187 private boolean mHaveShownSfpsTipLottie; 188 private boolean mHaveShownSfpsLeftEdgeLottie; 189 private boolean mHaveShownSfpsRightEdgeLottie; 190 private boolean mShouldShowLottie; 191 private ObjectAnimator mHelpAnimation; 192 193 private OrientationEventListener mOrientationEventListener; 194 private int mPreviousRotation = 0; 195 196 @VisibleForTesting shouldShowLottie()197 protected boolean shouldShowLottie() { 198 DisplayDensityUtils displayDensity = new DisplayDensityUtils(getApplicationContext()); 199 int currentDensityIndex = displayDensity.getCurrentIndexForDefaultDisplay(); 200 final int currentDensity = displayDensity.getDefaultDisplayDensityValues() 201 [currentDensityIndex]; 202 final int defaultDensity = displayDensity.getDefaultDensityForDefaultDisplay(); 203 return defaultDensity == currentDensity; 204 } 205 206 @Override onWindowFocusChanged(boolean hasFocus)207 public void onWindowFocusChanged(boolean hasFocus) { 208 if (hasFocus || mIsCanceled) { 209 return; 210 } 211 212 // By UX design, we should ensure seamless enrollment CUJ even though user rotate device. 213 // Do NOT cancel enrollment progress after rotating, adding mIsOrientationChanged 214 // to judge if the focus changed was triggered by rotation, current WMS has triple callbacks 215 // (true > false > true), we need to reset mIsOrientationChanged when !hasFocus callback. 216 // Side fps do not have to synchronize udfpsController overlay state, we should bypass sfps 217 // from onWindowFocusChanged() as long press sfps power key will prompt dialog to users. 218 if (!mIsOrientationChanged && !mCanAssumeSfps) { 219 onCancelEnrollment(FINGERPRINT_ERROR_USER_CANCELED); 220 } else { 221 mIsOrientationChanged = false; 222 } 223 } 224 225 @Override onApplyThemeResource(Resources.Theme theme, int resid, boolean first)226 protected void onApplyThemeResource(Resources.Theme theme, int resid, boolean first) { 227 theme.applyStyle(R.style.SetupWizardPartnerResource, true); 228 super.onApplyThemeResource(theme, resid, first); 229 } 230 231 @Override onCreate(Bundle savedInstanceState)232 protected void onCreate(Bundle savedInstanceState) { 233 super.onCreate(savedInstanceState); 234 235 if (savedInstanceState != null) { 236 restoreSavedState(savedInstanceState); 237 } 238 mFingerprintManager = getSystemService(FingerprintManager.class); 239 final List<FingerprintSensorPropertiesInternal> props = 240 mFingerprintManager.getSensorPropertiesInternal(); 241 mCanAssumeUdfps = props != null && props.size() == 1 && props.get(0).isAnyUdfpsType(); 242 mCanAssumeSfps = props != null && props.size() == 1 && props.get(0).isAnySidefpsType(); 243 244 mAccessibilityManager = getSystemService(AccessibilityManager.class); 245 mIsAccessibilityEnabled = mAccessibilityManager.isEnabled(); 246 247 final boolean isLayoutRtl = (TextUtils.getLayoutDirectionFromLocale( 248 Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL); 249 listenOrientationEvent(); 250 251 if (mCanAssumeUdfps) { 252 switch(getApplicationContext().getDisplay().getRotation()) { 253 case Surface.ROTATION_90: 254 final GlifLayout layout = (GlifLayout) getLayoutInflater().inflate( 255 R.layout.udfps_enroll_enrolling, null, false); 256 final LinearLayout layoutContainer = layout.findViewById( 257 R.id.layout_container); 258 final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( 259 LinearLayout.LayoutParams.MATCH_PARENT, 260 LinearLayout.LayoutParams.MATCH_PARENT); 261 262 lp.setMarginEnd((int) getResources().getDimension( 263 R.dimen.rotation_90_enroll_margin_end)); 264 layoutContainer.setPaddingRelative((int) getResources().getDimension( 265 R.dimen.rotation_90_enroll_padding_start), 0, isLayoutRtl 266 ? 0 : (int) getResources().getDimension( 267 R.dimen.rotation_90_enroll_padding_end), 0); 268 layoutContainer.setLayoutParams(lp); 269 setContentView(layout, lp); 270 break; 271 272 case Surface.ROTATION_0: 273 case Surface.ROTATION_180: 274 case Surface.ROTATION_270: 275 default: 276 setContentView(R.layout.udfps_enroll_enrolling); 277 break; 278 } 279 setDescriptionText(R.string.security_settings_udfps_enroll_start_message); 280 } else if (mCanAssumeSfps) { 281 setContentView(R.layout.sfps_enroll_enrolling); 282 setHelpAnimation(); 283 } else { 284 setContentView(R.layout.fingerprint_enroll_enrolling); 285 setDescriptionText(R.string.security_settings_fingerprint_enroll_start_message); 286 } 287 288 mIsSetupWizard = WizardManagerHelper.isAnySetupWizard(getIntent()); 289 if (mCanAssumeUdfps || mCanAssumeSfps) { 290 updateTitleAndDescription(); 291 } else { 292 setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title); 293 } 294 295 mShouldShowLottie = shouldShowLottie(); 296 // On non-SFPS devices, only show the lottie if the current display density is the default 297 // density. Otherwise, the lottie will overlap with the settings header text. 298 boolean isLandscape = BiometricUtils.isReverseLandscape(getApplicationContext()) 299 || BiometricUtils.isLandscape(getApplicationContext()); 300 301 updateOrientation((isLandscape 302 ? Configuration.ORIENTATION_LANDSCAPE : Configuration.ORIENTATION_PORTRAIT)); 303 304 mErrorText = findViewById(R.id.error_text); 305 mProgressBar = findViewById(R.id.fingerprint_progress_bar); 306 mVibrator = getSystemService(Vibrator.class); 307 308 mFooterBarMixin = getLayout().getMixin(FooterBarMixin.class); 309 mFooterBarMixin.setSecondaryButton( 310 new FooterButton.Builder(this) 311 .setText(R.string.security_settings_fingerprint_enroll_enrolling_skip) 312 .setListener(this::onSkipButtonClick) 313 .setButtonType(FooterButton.ButtonType.SKIP) 314 .setTheme(R.style.SudGlifButton_Secondary) 315 .build() 316 ); 317 318 final LayerDrawable fingerprintDrawable = mProgressBar != null 319 ? (LayerDrawable) mProgressBar.getBackground() : null; 320 if (fingerprintDrawable != null) { 321 mIconAnimationDrawable = (AnimatedVectorDrawable) 322 fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_animation); 323 mIconBackgroundBlinksDrawable = (AnimatedVectorDrawable) 324 fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_background); 325 mIconAnimationDrawable.registerAnimationCallback(mIconAnimationCallback); 326 } 327 328 mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator( 329 this, android.R.interpolator.fast_out_slow_in); 330 mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator( 331 this, android.R.interpolator.linear_out_slow_in); 332 mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator( 333 this, android.R.interpolator.fast_out_linear_in); 334 if (mProgressBar != null) { 335 mProgressBar.setProgressBackgroundTintMode(PorterDuff.Mode.SRC); 336 mProgressBar.setOnTouchListener((v, event) -> { 337 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { 338 mIconTouchCount++; 339 if (mIconTouchCount == ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN) { 340 showIconTouchDialog(); 341 } else { 342 mProgressBar.postDelayed(mShowDialogRunnable, 343 ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN); 344 } 345 } else if (event.getActionMasked() == MotionEvent.ACTION_CANCEL 346 || event.getActionMasked() == MotionEvent.ACTION_UP) { 347 mProgressBar.removeCallbacks(mShowDialogRunnable); 348 } 349 return true; 350 }); 351 } 352 353 final Configuration config = getApplicationContext().getResources().getConfiguration(); 354 maybeHideSfpsText(config); 355 } 356 setHelpAnimation()357 private void setHelpAnimation() { 358 final float translationX = 40; 359 final int duration = 550; 360 final RelativeLayout progressLottieLayout = findViewById(R.id.progress_lottie); 361 mHelpAnimation = ObjectAnimator.ofFloat(progressLottieLayout, 362 "translationX" /* propertyName */, 363 0, translationX, -1 * translationX, translationX, 0f); 364 mHelpAnimation.setInterpolator(new AccelerateDecelerateInterpolator()); 365 mHelpAnimation.setDuration(duration); 366 mHelpAnimation.setAutoCancel(false); 367 } 368 @Override getSidecar()369 protected BiometricEnrollSidecar getSidecar() { 370 final FingerprintEnrollSidecar sidecar = new FingerprintEnrollSidecar(this, 371 FingerprintManager.ENROLL_ENROLL); 372 return sidecar; 373 } 374 375 @Override shouldStartAutomatically()376 protected boolean shouldStartAutomatically() { 377 if (mCanAssumeUdfps) { 378 // Continue enrollment if restoring (e.g. configuration changed). Otherwise, wait 379 // for the entry animation to complete before starting. 380 return mRestoring && !mIsCanceled; 381 } 382 return true; 383 } 384 385 @Override onSaveInstanceState(Bundle outState)386 protected void onSaveInstanceState(Bundle outState) { 387 super.onSaveInstanceState(outState); 388 outState.putBoolean(KEY_STATE_CANCELED, mIsCanceled); 389 outState.putInt(KEY_STATE_PREVIOUS_ROTATION, mPreviousRotation); 390 } 391 restoreSavedState(Bundle savedInstanceState)392 private void restoreSavedState(Bundle savedInstanceState) { 393 mRestoring = true; 394 mIsCanceled = savedInstanceState.getBoolean(KEY_STATE_CANCELED, false); 395 mPreviousRotation = savedInstanceState.getInt(KEY_STATE_PREVIOUS_ROTATION, 396 getDisplay().getRotation()); 397 mIsOrientationChanged = mPreviousRotation != getDisplay().getRotation(); 398 } 399 400 @Override onStart()401 protected void onStart() { 402 super.onStart(); 403 updateProgress(false /* animate */); 404 updateTitleAndDescription(); 405 if (mRestoring) { 406 startIconAnimation(); 407 } 408 } 409 410 @Override onEnterAnimationComplete()411 public void onEnterAnimationComplete() { 412 super.onEnterAnimationComplete(); 413 414 if (mCanAssumeUdfps) { 415 startEnrollment(); 416 } 417 418 mAnimationCancelled = false; 419 startIconAnimation(); 420 } 421 startIconAnimation()422 private void startIconAnimation() { 423 if (mIconAnimationDrawable != null) { 424 mIconAnimationDrawable.start(); 425 } 426 } 427 stopIconAnimation()428 private void stopIconAnimation() { 429 mAnimationCancelled = true; 430 if (mIconAnimationDrawable != null) { 431 mIconAnimationDrawable.stop(); 432 } 433 } 434 435 @VisibleForTesting onCancelEnrollment(@dRes int errorMsgId)436 void onCancelEnrollment(@IdRes int errorMsgId) { 437 // showErrorDialog() will cause onWindowFocusChanged(false), set mIsCanceled to false 438 // before showErrorDialog() to prevent that another error dialog is triggered again. 439 mIsCanceled = true; 440 FingerprintErrorDialog.showErrorDialog(this, errorMsgId, mCanAssumeUdfps); 441 mIsOrientationChanged = false; 442 cancelEnrollment(); 443 stopIconAnimation(); 444 stopListenOrientationEvent(); 445 if (!mCanAssumeUdfps) { 446 mErrorText.removeCallbacks(mTouchAgainRunnable); 447 } 448 } 449 450 @Override onStop()451 protected void onStop() { 452 if (!isChangingConfigurations()) { 453 if (!WizardManagerHelper.isAnySetupWizard(getIntent()) 454 && !BiometricUtils.isAnyMultiBiometricFlow(this) 455 && !mFromSettingsSummary) { 456 setResult(RESULT_TIMEOUT); 457 } 458 finish(); 459 } 460 stopIconAnimation(); 461 462 super.onStop(); 463 } 464 465 @Override shouldFinishWhenBackgrounded()466 protected boolean shouldFinishWhenBackgrounded() { 467 // Prevent super.onStop() from finishing, since we handle this in our onStop(). 468 return false; 469 } 470 471 @Override onDestroy()472 protected void onDestroy() { 473 stopListenOrientationEvent(); 474 super.onDestroy(); 475 } 476 animateProgress(int progress)477 private void animateProgress(int progress) { 478 if (mCanAssumeUdfps) { 479 // UDFPS animations are owned by SystemUI 480 if (progress >= PROGRESS_BAR_MAX) { 481 // Wait for any animations in SysUI to finish, then proceed to next page 482 getMainThreadHandler().postDelayed(mDelayedFinishRunnable, getFinishDelay()); 483 } 484 return; 485 } 486 if (mProgressAnim != null) { 487 mProgressAnim.cancel(); 488 } 489 ObjectAnimator anim = ObjectAnimator.ofInt(mProgressBar, "progress", 490 mProgressBar.getProgress(), progress); 491 anim.addListener(mProgressAnimationListener); 492 anim.setInterpolator(mFastOutSlowInInterpolator); 493 anim.setDuration(250); 494 anim.start(); 495 mProgressAnim = anim; 496 } 497 animateFlash()498 private void animateFlash() { 499 if (mIconBackgroundBlinksDrawable != null) { 500 mIconBackgroundBlinksDrawable.start(); 501 } 502 } 503 getFinishIntent()504 protected Intent getFinishIntent() { 505 return new Intent(this, FingerprintEnrollFinish.class); 506 } 507 updateTitleAndDescription()508 private void updateTitleAndDescription() { 509 if (mCanAssumeUdfps) { 510 updateTitleAndDescriptionForUdfps(); 511 return; 512 } else if (mCanAssumeSfps) { 513 updateTitleAndDescriptionForSfps(); 514 return; 515 } 516 517 if (mSidecar == null || mSidecar.getEnrollmentSteps() == -1) { 518 setDescriptionText(R.string.security_settings_fingerprint_enroll_start_message); 519 } else { 520 setDescriptionText(R.string.security_settings_fingerprint_enroll_repeat_message); 521 } 522 } 523 updateTitleAndDescriptionForUdfps()524 private void updateTitleAndDescriptionForUdfps() { 525 switch (getCurrentStage()) { 526 case STAGE_CENTER: 527 setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title); 528 setDescriptionText(R.string.security_settings_udfps_enroll_start_message); 529 break; 530 531 case STAGE_GUIDED: 532 setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title); 533 if (mIsAccessibilityEnabled) { 534 setDescriptionText(R.string.security_settings_udfps_enroll_repeat_a11y_message); 535 } else { 536 setDescriptionText(R.string.security_settings_udfps_enroll_repeat_message); 537 } 538 break; 539 540 case STAGE_FINGERTIP: 541 setHeaderText(R.string.security_settings_udfps_enroll_fingertip_title); 542 if (!mHaveShownUdfpsTipLottie && mIllustrationLottie != null) { 543 mHaveShownUdfpsTipLottie = true; 544 mIllustrationLottie.setContentDescription( 545 getString(R.string.security_settings_udfps_tip_fingerprint_help) 546 ); 547 configureEnrollmentStage(R.raw.udfps_tip_hint_lottie); 548 } 549 break; 550 551 case STAGE_LEFT_EDGE: 552 setHeaderText(R.string.security_settings_udfps_enroll_left_edge_title); 553 if (!mHaveShownUdfpsLeftEdgeLottie && mIllustrationLottie != null) { 554 mHaveShownUdfpsLeftEdgeLottie = true; 555 mIllustrationLottie.setContentDescription( 556 getString(R.string.security_settings_udfps_side_fingerprint_help) 557 ); 558 configureEnrollmentStage(R.raw.udfps_left_edge_hint_lottie); 559 } else if (mIllustrationLottie == null) { 560 if (isStageHalfCompleted()) { 561 setDescriptionText( 562 R.string.security_settings_fingerprint_enroll_repeat_message); 563 } else { 564 setDescriptionText(R.string.security_settings_udfps_enroll_edge_message); 565 } 566 } 567 break; 568 case STAGE_RIGHT_EDGE: 569 setHeaderText(R.string.security_settings_udfps_enroll_right_edge_title); 570 if (!mHaveShownUdfpsRightEdgeLottie && mIllustrationLottie != null) { 571 mHaveShownUdfpsRightEdgeLottie = true; 572 mIllustrationLottie.setContentDescription( 573 getString(R.string.security_settings_udfps_side_fingerprint_help) 574 ); 575 configureEnrollmentStage(R.raw.udfps_right_edge_hint_lottie); 576 577 } else if (mIllustrationLottie == null) { 578 if (isStageHalfCompleted()) { 579 setDescriptionText( 580 R.string.security_settings_fingerprint_enroll_repeat_message); 581 } else { 582 setDescriptionText(R.string.security_settings_udfps_enroll_edge_message); 583 } 584 } 585 break; 586 587 case STAGE_UNKNOWN: 588 default: 589 // setHeaderText(R.string.security_settings_fingerprint_enroll_udfps_title); 590 // Don't use BiometricEnrollBase#setHeaderText, since that invokes setTitle, 591 // which gets announced for a11y upon entering the page. For UDFPS, we want to 592 // announce a different string for a11y upon entering the page. 593 getLayout().setHeaderText( 594 R.string.security_settings_fingerprint_enroll_udfps_title); 595 setDescriptionText(R.string.security_settings_udfps_enroll_start_message); 596 final CharSequence description = getString( 597 R.string.security_settings_udfps_enroll_a11y); 598 getLayout().getHeaderTextView().setContentDescription(description); 599 setTitle(description); 600 break; 601 602 } 603 } 604 605 // Interrupt any existing talkback speech to prevent stacking talkback messages clearTalkback()606 private void clearTalkback() { 607 AccessibilityManager.getInstance(getApplicationContext()).interrupt(); 608 } 609 updateTitleAndDescriptionForSfps()610 private void updateTitleAndDescriptionForSfps() { 611 if (mIsAccessibilityEnabled) { 612 clearTalkback(); 613 getLayout().getDescriptionTextView().setAccessibilityLiveRegion( 614 View.ACCESSIBILITY_LIVE_REGION_POLITE); 615 } 616 switch (getCurrentSfpsStage()) { 617 case SFPS_STAGE_NO_ANIMATION: 618 setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title); 619 if (!mHaveShownSfpsNoAnimationLottie && mIllustrationLottie != null) { 620 mHaveShownSfpsNoAnimationLottie = true; 621 mIllustrationLottie.setContentDescription( 622 getString( 623 R.string.security_settings_sfps_animation_a11y_label, 624 0 625 ) 626 ); 627 configureEnrollmentStage(R.raw.sfps_lottie_no_animation); 628 } 629 break; 630 631 case SFPS_STAGE_CENTER: 632 setHeaderText(R.string.security_settings_sfps_enroll_finger_center_title); 633 if (!mHaveShownSfpsCenterLottie && mIllustrationLottie != null) { 634 mHaveShownSfpsCenterLottie = true; 635 configureEnrollmentStage(R.raw.sfps_lottie_pad_center); 636 } 637 break; 638 639 case SFPS_STAGE_FINGERTIP: 640 setHeaderText(R.string.security_settings_sfps_enroll_fingertip_title); 641 if (!mHaveShownSfpsTipLottie && mIllustrationLottie != null) { 642 mHaveShownSfpsTipLottie = true; 643 configureEnrollmentStage(R.raw.sfps_lottie_tip); 644 } 645 break; 646 647 case SFPS_STAGE_LEFT_EDGE: 648 setHeaderText(R.string.security_settings_sfps_enroll_left_edge_title); 649 if (!mHaveShownSfpsLeftEdgeLottie && mIllustrationLottie != null) { 650 mHaveShownSfpsLeftEdgeLottie = true; 651 configureEnrollmentStage(R.raw.sfps_lottie_left_edge); 652 } 653 break; 654 655 case SFPS_STAGE_RIGHT_EDGE: 656 setHeaderText(R.string.security_settings_sfps_enroll_right_edge_title); 657 if (!mHaveShownSfpsRightEdgeLottie && mIllustrationLottie != null) { 658 mHaveShownSfpsRightEdgeLottie = true; 659 configureEnrollmentStage(R.raw.sfps_lottie_right_edge); 660 } 661 break; 662 663 case STAGE_UNKNOWN: 664 default: 665 // Don't use BiometricEnrollBase#setHeaderText, since that invokes setTitle, 666 // which gets announced for a11y upon entering the page. For SFPS, we want to 667 // announce a different string for a11y upon entering the page. 668 getLayout().setHeaderText( 669 R.string.security_settings_sfps_enroll_find_sensor_title); 670 final CharSequence description = getString( 671 R.string.security_settings_sfps_enroll_find_sensor_message); 672 getLayout().getHeaderTextView().setContentDescription(description); 673 setTitle(description); 674 break; 675 676 } 677 } 678 configureEnrollmentStage(@awRes int lottie)679 @VisibleForTesting void configureEnrollmentStage(@RawRes int lottie) { 680 if (!mCanAssumeSfps) { 681 setDescriptionText(""); 682 } 683 LottieCompositionFactory.fromRawRes(this, lottie) 684 .addListener((c) -> { 685 mIllustrationLottie.setComposition(c); 686 mIllustrationLottie.setVisibility(View.VISIBLE); 687 mIllustrationLottie.playAnimation(); 688 }); 689 } 690 691 @EnrollStage getCurrentStage()692 private int getCurrentStage() { 693 if (mSidecar == null || mSidecar.getEnrollmentSteps() == -1) { 694 return STAGE_UNKNOWN; 695 } 696 697 final int progressSteps = mSidecar.getEnrollmentSteps() - mSidecar.getEnrollmentRemaining(); 698 if (progressSteps < getStageThresholdSteps(0)) { 699 return STAGE_CENTER; 700 } else if (progressSteps < getStageThresholdSteps(1)) { 701 return STAGE_GUIDED; 702 } else if (progressSteps < getStageThresholdSteps(2)) { 703 return STAGE_FINGERTIP; 704 } else if (progressSteps < getStageThresholdSteps(3)) { 705 return STAGE_LEFT_EDGE; 706 } else { 707 return STAGE_RIGHT_EDGE; 708 } 709 } 710 711 @SfpsEnrollStage getCurrentSfpsStage()712 private int getCurrentSfpsStage() { 713 if (mSidecar == null) { 714 return STAGE_UNKNOWN; 715 } 716 717 final int progressSteps = mSidecar.getEnrollmentSteps() - mSidecar.getEnrollmentRemaining(); 718 if (progressSteps < getStageThresholdSteps(0)) { 719 return SFPS_STAGE_NO_ANIMATION; 720 } else if (progressSteps < getStageThresholdSteps(1)) { 721 return SFPS_STAGE_CENTER; 722 } else if (progressSteps < getStageThresholdSteps(2)) { 723 return SFPS_STAGE_FINGERTIP; 724 } else if (progressSteps < getStageThresholdSteps(3)) { 725 return SFPS_STAGE_LEFT_EDGE; 726 } else { 727 return SFPS_STAGE_RIGHT_EDGE; 728 } 729 } 730 isStageHalfCompleted()731 private boolean isStageHalfCompleted() { 732 // Prior to first enrollment step. 733 if (mSidecar == null || mSidecar.getEnrollmentSteps() == -1) { 734 return false; 735 } 736 737 final int progressSteps = mSidecar.getEnrollmentSteps() - mSidecar.getEnrollmentRemaining(); 738 int prevThresholdSteps = 0; 739 for (int i = 0; i < mFingerprintManager.getEnrollStageCount(); i++) { 740 final int thresholdSteps = getStageThresholdSteps(i); 741 if (progressSteps >= prevThresholdSteps && progressSteps < thresholdSteps) { 742 final int adjustedProgress = progressSteps - prevThresholdSteps; 743 final int adjustedThreshold = thresholdSteps - prevThresholdSteps; 744 return adjustedProgress >= adjustedThreshold / 2; 745 } 746 prevThresholdSteps = thresholdSteps; 747 } 748 749 // After last enrollment step. 750 return true; 751 } 752 753 @VisibleForTesting getStageThresholdSteps(int index)754 protected int getStageThresholdSteps(int index) { 755 if (mSidecar == null || mSidecar.getEnrollmentSteps() == -1) { 756 Log.w(TAG, "getStageThresholdSteps: Enrollment not started yet"); 757 return 1; 758 } 759 return Math.round(mSidecar.getEnrollmentSteps() 760 * mFingerprintManager.getEnrollStageThreshold(index)); 761 } 762 763 @Override onEnrollmentHelp(int helpMsgId, CharSequence helpString)764 public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) { 765 if (!TextUtils.isEmpty(helpString)) { 766 if (!(mCanAssumeUdfps || mCanAssumeSfps)) { 767 mErrorText.removeCallbacks(mTouchAgainRunnable); 768 } 769 showError(helpString); 770 } 771 } 772 773 @Override onEnrollmentError(int errMsgId, CharSequence errString)774 public void onEnrollmentError(int errMsgId, CharSequence errString) { 775 onCancelEnrollment(errMsgId); 776 } 777 announceEnrollmentProgress(CharSequence announcement)778 private void announceEnrollmentProgress(CharSequence announcement) { 779 AccessibilityEvent e = AccessibilityEvent.obtain(); 780 e.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT); 781 e.setClassName(getClass().getName()); 782 e.setPackageName(getPackageName()); 783 e.getText().add(announcement); 784 mAccessibilityManager.sendAccessibilityEvent(e); 785 } 786 787 @Override onEnrollmentProgressChange(int steps, int remaining)788 public void onEnrollmentProgressChange(int steps, int remaining) { 789 updateProgress(true /* animate */); 790 final int percent = (int) (((float) (steps - remaining) / (float) steps) * 100); 791 if (mCanAssumeSfps && mIsAccessibilityEnabled) { 792 CharSequence announcement = getString( 793 R.string.security_settings_sfps_enroll_progress_a11y_message, percent); 794 announceEnrollmentProgress(announcement); 795 if (mIllustrationLottie != null) { 796 mIllustrationLottie.setContentDescription( 797 getString( 798 R.string.security_settings_sfps_animation_a11y_label, 799 percent) 800 ); 801 } 802 } 803 updateTitleAndDescription(); 804 animateFlash(); 805 if (mCanAssumeUdfps) { 806 if (mIsAccessibilityEnabled) { 807 CharSequence announcement = getString( 808 R.string.security_settings_udfps_enroll_progress_a11y_message, percent); 809 announceEnrollmentProgress(announcement); 810 } 811 } else if (!mCanAssumeSfps) { 812 mErrorText.removeCallbacks(mTouchAgainRunnable); 813 mErrorText.postDelayed(mTouchAgainRunnable, HINT_TIMEOUT_DURATION); 814 } 815 } 816 updateProgress(boolean animate)817 private void updateProgress(boolean animate) { 818 if (mSidecar == null || !mSidecar.isEnrolling()) { 819 Log.d(TAG, "Enrollment not started yet"); 820 return; 821 } 822 823 int progress = getProgress( 824 mSidecar.getEnrollmentSteps(), mSidecar.getEnrollmentRemaining()); 825 // Only clear the error when progress has been made. 826 // TODO (b/234772728) Add tests. 827 if (mProgressBar != null && mProgressBar.getProgress() < progress) { 828 clearError(); 829 } 830 if (animate) { 831 animateProgress(progress); 832 } else { 833 if (mProgressBar != null) { 834 mProgressBar.setProgress(progress); 835 } 836 if (progress >= PROGRESS_BAR_MAX) { 837 mDelayedFinishRunnable.run(); 838 } 839 } 840 } 841 getProgress(int steps, int remaining)842 private int getProgress(int steps, int remaining) { 843 if (steps == -1) { 844 return 0; 845 } 846 int progress = Math.max(0, steps + 1 - remaining); 847 return PROGRESS_BAR_MAX * progress / (steps + 1); 848 } 849 showIconTouchDialog()850 private void showIconTouchDialog() { 851 mIconTouchCount = 0; 852 new IconTouchDialog().show(getSupportFragmentManager(), null /* tag */); 853 } 854 showError(CharSequence error)855 private void showError(CharSequence error) { 856 if (mCanAssumeSfps) { 857 setHeaderText(error); 858 if (!mHelpAnimation.isRunning()) { 859 mHelpAnimation.start(); 860 } 861 applySfpsErrorDynamicColors(getApplicationContext(), true); 862 } else if (mCanAssumeUdfps) { 863 setHeaderText(error); 864 // Show nothing for subtitle when getting an error message. 865 setDescriptionText(""); 866 } else { 867 mErrorText.setText(error); 868 if (mErrorText.getVisibility() == View.INVISIBLE) { 869 mErrorText.setVisibility(View.VISIBLE); 870 mErrorText.setTranslationY(getResources().getDimensionPixelSize( 871 R.dimen.fingerprint_error_text_appear_distance)); 872 mErrorText.setAlpha(0f); 873 mErrorText.animate() 874 .alpha(1f) 875 .translationY(0f) 876 .setDuration(200) 877 .setInterpolator(mLinearOutSlowInInterpolator) 878 .start(); 879 } else { 880 mErrorText.animate().cancel(); 881 mErrorText.setAlpha(1f); 882 mErrorText.setTranslationY(0f); 883 } 884 } 885 if (isResumed() && mIsAccessibilityEnabled && !mCanAssumeUdfps) { 886 mVibrator.vibrate(Process.myUid(), getApplicationContext().getOpPackageName(), 887 VIBRATE_EFFECT_ERROR, getClass().getSimpleName() + "::showError", 888 FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES); 889 } 890 } 891 clearError()892 private void clearError() { 893 if (mCanAssumeSfps) { 894 applySfpsErrorDynamicColors(getApplicationContext(), false); 895 } 896 if ((!(mCanAssumeUdfps || mCanAssumeSfps)) && mErrorText.getVisibility() == View.VISIBLE) { 897 mErrorText.animate() 898 .alpha(0f) 899 .translationY(getResources().getDimensionPixelSize( 900 R.dimen.fingerprint_error_text_disappear_distance)) 901 .setDuration(100) 902 .setInterpolator(mFastOutLinearInInterpolator) 903 .withEndAction(() -> mErrorText.setVisibility(View.INVISIBLE)) 904 .start(); 905 } 906 } 907 908 /** 909 * Applies dynamic colors corresponding to showing or clearing errors on the progress bar 910 * and finger lottie for SFPS 911 */ applySfpsErrorDynamicColors(Context context, boolean isError)912 private void applySfpsErrorDynamicColors(Context context, boolean isError) { 913 applyProgressBarDynamicColor(context, isError); 914 if (mIllustrationLottie != null) { 915 applyLottieDynamicColor(context, isError); 916 } 917 } 918 applyProgressBarDynamicColor(Context context, boolean isError)919 private void applyProgressBarDynamicColor(Context context, boolean isError) { 920 if (mProgressBar != null) { 921 int error_color = context.getColor(R.color.sfps_enrollment_progress_bar_error_color); 922 int progress_bar_fill_color = context.getColor( 923 R.color.sfps_enrollment_progress_bar_fill_color); 924 ColorStateList fillColor = ColorStateList.valueOf( 925 isError ? error_color : progress_bar_fill_color); 926 mProgressBar.setProgressTintList(fillColor); 927 mProgressBar.setProgressTintMode(PorterDuff.Mode.SRC); 928 mProgressBar.invalidate(); 929 } 930 } 931 applyLottieDynamicColor(Context context, boolean isError)932 private void applyLottieDynamicColor(Context context, boolean isError) { 933 int error_color = context.getColor(R.color.sfps_enrollment_fp_error_color); 934 int fp_captured_color = context.getColor(R.color.sfps_enrollment_fp_captured_color); 935 int color = isError ? error_color : fp_captured_color; 936 mIllustrationLottie.addValueCallback( 937 new KeyPath(".blue100", "**"), 938 LottieProperty.COLOR_FILTER, 939 frameInfo -> new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP) 940 ); 941 mIllustrationLottie.invalidate(); 942 } 943 listenOrientationEvent()944 private void listenOrientationEvent() { 945 mOrientationEventListener = new OrientationEventListener(this) { 946 @Override 947 public void onOrientationChanged(int orientation) { 948 final int currentRotation = getDisplay().getRotation(); 949 if ((mPreviousRotation == Surface.ROTATION_90 950 && currentRotation == Surface.ROTATION_270) || ( 951 mPreviousRotation == Surface.ROTATION_270 952 && currentRotation == Surface.ROTATION_90)) { 953 mPreviousRotation = currentRotation; 954 recreate(); 955 } 956 } 957 }; 958 mOrientationEventListener.enable(); 959 mPreviousRotation = getDisplay().getRotation(); 960 } 961 stopListenOrientationEvent()962 private void stopListenOrientationEvent() { 963 if (mOrientationEventListener != null) { 964 mOrientationEventListener.disable(); 965 } 966 mOrientationEventListener = null; 967 } 968 969 private final Animator.AnimatorListener mProgressAnimationListener = 970 new Animator.AnimatorListener() { 971 972 @Override 973 public void onAnimationStart(Animator animation) { 974 startIconAnimation(); 975 } 976 977 @Override 978 public void onAnimationRepeat(Animator animation) { } 979 980 @Override 981 public void onAnimationEnd(Animator animation) { 982 stopIconAnimation(); 983 984 if (mProgressBar.getProgress() >= PROGRESS_BAR_MAX) { 985 mProgressBar.postDelayed(mDelayedFinishRunnable, getFinishDelay()); 986 } 987 } 988 989 @Override 990 public void onAnimationCancel(Animator animation) { } 991 }; 992 getFinishDelay()993 private long getFinishDelay() { 994 return mCanAssumeUdfps ? 400L : 250L; 995 } 996 997 // Give the user a chance to see progress completed before jumping to the next stage. 998 private final Runnable mDelayedFinishRunnable = new Runnable() { 999 @Override 1000 public void run() { 1001 launchFinish(mToken); 1002 } 1003 }; 1004 1005 private final Animatable2.AnimationCallback mIconAnimationCallback = 1006 new Animatable2.AnimationCallback() { 1007 @Override 1008 public void onAnimationEnd(Drawable d) { 1009 if (mAnimationCancelled) { 1010 return; 1011 } 1012 1013 // Start animation after it has ended. 1014 mProgressBar.post(new Runnable() { 1015 @Override 1016 public void run() { 1017 startIconAnimation(); 1018 } 1019 }); 1020 } 1021 }; 1022 1023 private final Runnable mShowDialogRunnable = new Runnable() { 1024 @Override 1025 public void run() { 1026 showIconTouchDialog(); 1027 } 1028 }; 1029 1030 private final Runnable mTouchAgainRunnable = new Runnable() { 1031 @Override 1032 public void run() { 1033 showError(getString(R.string.security_settings_fingerprint_enroll_lift_touch_again)); 1034 } 1035 }; 1036 1037 @Override getMetricsCategory()1038 public int getMetricsCategory() { 1039 return SettingsEnums.FINGERPRINT_ENROLLING; 1040 } 1041 updateOrientation(int orientation)1042 private void updateOrientation(int orientation) { 1043 if (mCanAssumeSfps) { 1044 mIllustrationLottie = findViewById(R.id.illustration_lottie); 1045 } else { 1046 switch(orientation) { 1047 case Configuration.ORIENTATION_LANDSCAPE: { 1048 mIllustrationLottie = null; 1049 break; 1050 } 1051 case Configuration.ORIENTATION_PORTRAIT: { 1052 if (mShouldShowLottie) { 1053 mIllustrationLottie = findViewById(R.id.illustration_lottie); 1054 } 1055 break; 1056 } 1057 default: 1058 Log.e(TAG, "Error unhandled configuration change"); 1059 break; 1060 } 1061 } 1062 } 1063 1064 @Override onConfigurationChanged(@onNull Configuration newConfig)1065 public void onConfigurationChanged(@NonNull Configuration newConfig) { 1066 maybeHideSfpsText(newConfig); 1067 switch(newConfig.orientation) { 1068 case Configuration.ORIENTATION_LANDSCAPE: { 1069 updateOrientation(Configuration.ORIENTATION_LANDSCAPE); 1070 break; 1071 } 1072 case Configuration.ORIENTATION_PORTRAIT: { 1073 updateOrientation(Configuration.ORIENTATION_PORTRAIT); 1074 break; 1075 } 1076 default: 1077 Log.e(TAG, "Error unhandled configuration change"); 1078 break; 1079 } 1080 } 1081 maybeHideSfpsText(@onNull Configuration newConfig)1082 private void maybeHideSfpsText(@NonNull Configuration newConfig) { 1083 final HeaderMixin headerMixin = getLayout().getMixin(HeaderMixin.class); 1084 final DescriptionMixin descriptionMixin = getLayout().getMixin(DescriptionMixin.class); 1085 final boolean isLandscape = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE; 1086 1087 if (mCanAssumeSfps) { 1088 // hide the description 1089 descriptionMixin.getTextView().setVisibility(View.GONE); 1090 headerMixin.getTextView().setHyphenationFrequency(HYPHENATION_FREQUENCY_NONE); 1091 if (isLandscape) { 1092 headerMixin.setAutoTextSizeEnabled(true); 1093 headerMixin.getTextView().setMinLines(0); 1094 headerMixin.getTextView().setMaxLines(10); 1095 } else { 1096 headerMixin.setAutoTextSizeEnabled(false); 1097 headerMixin.getTextView().setLines(4); 1098 } 1099 } 1100 } 1101 1102 public static class IconTouchDialog extends InstrumentedDialogFragment { 1103 1104 @Override onCreateDialog(Bundle savedInstanceState)1105 public Dialog onCreateDialog(Bundle savedInstanceState) { 1106 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(), 1107 R.style.Theme_AlertDialog); 1108 builder.setTitle(R.string.security_settings_fingerprint_enroll_touch_dialog_title) 1109 .setMessage(R.string.security_settings_fingerprint_enroll_touch_dialog_message) 1110 .setPositiveButton(R.string.security_settings_fingerprint_enroll_dialog_ok, 1111 new DialogInterface.OnClickListener() { 1112 @Override 1113 public void onClick(DialogInterface dialog, int which) { 1114 dialog.dismiss(); 1115 } 1116 }); 1117 return builder.create(); 1118 } 1119 1120 @Override getMetricsCategory()1121 public int getMetricsCategory() { 1122 return SettingsEnums.DIALOG_FINGERPRINT_ICON_TOUCH; 1123 } 1124 } 1125 }