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