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 android.animation.Animator; 20 import android.animation.ObjectAnimator; 21 import android.annotation.Nullable; 22 import android.app.Dialog; 23 import android.app.settings.SettingsEnums; 24 import android.content.DialogInterface; 25 import android.content.Intent; 26 import android.graphics.drawable.Animatable2; 27 import android.graphics.drawable.AnimatedVectorDrawable; 28 import android.graphics.drawable.Drawable; 29 import android.graphics.drawable.LayerDrawable; 30 import android.hardware.fingerprint.FingerprintManager; 31 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; 32 import android.media.AudioAttributes; 33 import android.os.Bundle; 34 import android.os.VibrationEffect; 35 import android.os.Vibrator; 36 import android.text.TextUtils; 37 import android.util.Log; 38 import android.view.MotionEvent; 39 import android.view.OrientationEventListener; 40 import android.view.Surface; 41 import android.view.View; 42 import android.view.accessibility.AccessibilityEvent; 43 import android.view.accessibility.AccessibilityManager; 44 import android.view.animation.AnimationUtils; 45 import android.view.animation.Interpolator; 46 import android.widget.ProgressBar; 47 import android.widget.TextView; 48 49 import androidx.appcompat.app.AlertDialog; 50 51 import com.android.settings.R; 52 import com.android.settings.biometrics.BiometricEnrollSidecar; 53 import com.android.settings.biometrics.BiometricUtils; 54 import com.android.settings.biometrics.BiometricsEnrollEnrolling; 55 import com.android.settings.core.instrumentation.InstrumentedDialogFragment; 56 57 import com.google.android.setupcompat.template.FooterBarMixin; 58 import com.google.android.setupcompat.template.FooterButton; 59 import com.google.android.setupcompat.util.WizardManagerHelper; 60 61 import java.util.List; 62 63 /** 64 * Activity which handles the actual enrolling for fingerprint. 65 */ 66 public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling { 67 68 private static final String TAG = "FingerprintEnrollEnrolling"; 69 static final String TAG_SIDECAR = "sidecar"; 70 71 private static final int PROGRESS_BAR_MAX = 10000; 72 private static final int FINISH_DELAY = 250; 73 /** 74 * Enroll with two center touches before going to guided enrollment. 75 */ 76 private static final int NUM_CENTER_TOUCHES = 2; 77 78 /** 79 * If we don't see progress during this time, we show an error message to remind the users that 80 * they need to lift the finger and touch again. 81 */ 82 private static final int HINT_TIMEOUT_DURATION = 2500; 83 84 /** 85 * How long the user needs to touch the icon until we show the dialog. 86 */ 87 private static final long ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN = 500; 88 89 /** 90 * How many times the user needs to touch the icon until we show the dialog that this is not the 91 * fingerprint sensor. 92 */ 93 private static final int ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN = 3; 94 95 private static final VibrationEffect VIBRATE_EFFECT_ERROR = 96 VibrationEffect.createWaveform(new long[] {0, 5, 55, 60}, -1); 97 private static final AudioAttributes FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES = 98 new AudioAttributes.Builder() 99 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) 100 .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) 101 .build(); 102 103 private boolean mCanAssumeUdfps; 104 @Nullable private ProgressBar mProgressBar; 105 private ObjectAnimator mProgressAnim; 106 private TextView mDescriptionText; 107 private TextView mErrorText; 108 private Interpolator mFastOutSlowInInterpolator; 109 private Interpolator mLinearOutSlowInInterpolator; 110 private Interpolator mFastOutLinearInInterpolator; 111 private int mIconTouchCount; 112 private boolean mAnimationCancelled; 113 @Nullable private AnimatedVectorDrawable mIconAnimationDrawable; 114 @Nullable private AnimatedVectorDrawable mIconBackgroundBlinksDrawable; 115 private boolean mRestoring; 116 private Vibrator mVibrator; 117 private boolean mIsSetupWizard; 118 private AccessibilityManager mAccessibilityManager; 119 private boolean mIsAccessibilityEnabled; 120 121 private OrientationEventListener mOrientationEventListener; 122 private int mPreviousRotation = 0; 123 124 @Override onCreate(Bundle savedInstanceState)125 protected void onCreate(Bundle savedInstanceState) { 126 super.onCreate(savedInstanceState); 127 128 final FingerprintManager fingerprintManager = getSystemService(FingerprintManager.class); 129 final List<FingerprintSensorPropertiesInternal> props = 130 fingerprintManager.getSensorPropertiesInternal(); 131 mCanAssumeUdfps = props.size() == 1 && props.get(0).isAnyUdfpsType(); 132 133 mAccessibilityManager = getSystemService(AccessibilityManager.class); 134 mIsAccessibilityEnabled = mAccessibilityManager.isEnabled(); 135 136 listenOrientationEvent(); 137 138 if (mCanAssumeUdfps) { 139 if (BiometricUtils.isReverseLandscape(getApplicationContext())) { 140 setContentView(R.layout.udfps_enroll_enrolling_land); 141 } else { 142 setContentView(R.layout.udfps_enroll_enrolling); 143 } 144 setDescriptionText(R.string.security_settings_udfps_enroll_start_message); 145 } else { 146 setContentView(R.layout.fingerprint_enroll_enrolling); 147 setDescriptionText(R.string.security_settings_fingerprint_enroll_start_message); 148 } 149 150 mIsSetupWizard = WizardManagerHelper.isAnySetupWizard(getIntent()); 151 if (mCanAssumeUdfps) { 152 updateTitleAndDescription(); 153 } else { 154 setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title); 155 } 156 157 mErrorText = findViewById(R.id.error_text); 158 mProgressBar = findViewById(R.id.fingerprint_progress_bar); 159 mVibrator = getSystemService(Vibrator.class); 160 161 mFooterBarMixin = getLayout().getMixin(FooterBarMixin.class); 162 mFooterBarMixin.setSecondaryButton( 163 new FooterButton.Builder(this) 164 .setText(R.string.security_settings_fingerprint_enroll_enrolling_skip) 165 .setListener(this::onSkipButtonClick) 166 .setButtonType(FooterButton.ButtonType.SKIP) 167 .setTheme(R.style.SudGlifButton_Secondary) 168 .build() 169 ); 170 171 final LayerDrawable fingerprintDrawable = mProgressBar != null 172 ? (LayerDrawable) mProgressBar.getBackground() : null; 173 if (fingerprintDrawable != null) { 174 mIconAnimationDrawable = (AnimatedVectorDrawable) 175 fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_animation); 176 mIconBackgroundBlinksDrawable = (AnimatedVectorDrawable) 177 fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_background); 178 mIconAnimationDrawable.registerAnimationCallback(mIconAnimationCallback); 179 } 180 181 mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator( 182 this, android.R.interpolator.fast_out_slow_in); 183 mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator( 184 this, android.R.interpolator.linear_out_slow_in); 185 mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator( 186 this, android.R.interpolator.fast_out_linear_in); 187 if (mProgressBar != null) { 188 mProgressBar.setOnTouchListener((v, event) -> { 189 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { 190 mIconTouchCount++; 191 if (mIconTouchCount == ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN) { 192 showIconTouchDialog(); 193 } else { 194 mProgressBar.postDelayed(mShowDialogRunnable, 195 ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN); 196 } 197 } else if (event.getActionMasked() == MotionEvent.ACTION_CANCEL 198 || event.getActionMasked() == MotionEvent.ACTION_UP) { 199 mProgressBar.removeCallbacks(mShowDialogRunnable); 200 } 201 return true; 202 }); 203 } 204 mRestoring = savedInstanceState != null; 205 } 206 207 @Override getSidecar()208 protected BiometricEnrollSidecar getSidecar() { 209 final FingerprintEnrollSidecar sidecar = new FingerprintEnrollSidecar(); 210 sidecar.setEnrollReason(FingerprintManager.ENROLL_ENROLL); 211 return sidecar; 212 } 213 214 @Override shouldStartAutomatically()215 protected boolean shouldStartAutomatically() { 216 if (mCanAssumeUdfps) { 217 // Continue enrollment if restoring (e.g. configuration changed). Otherwise, wait 218 // for the entry animation to complete before starting. 219 return mRestoring; 220 } 221 return true; 222 } 223 224 @Override onStart()225 protected void onStart() { 226 super.onStart(); 227 updateProgress(false /* animate */); 228 updateTitleAndDescription(); 229 if (mRestoring) { 230 startIconAnimation(); 231 } 232 } 233 234 @Override onEnterAnimationComplete()235 public void onEnterAnimationComplete() { 236 super.onEnterAnimationComplete(); 237 238 if (mCanAssumeUdfps) { 239 startEnrollment(); 240 } 241 242 mAnimationCancelled = false; 243 startIconAnimation(); 244 } 245 startIconAnimation()246 private void startIconAnimation() { 247 if (mIconAnimationDrawable != null) { 248 mIconAnimationDrawable.start(); 249 } 250 } 251 stopIconAnimation()252 private void stopIconAnimation() { 253 mAnimationCancelled = true; 254 if (mIconAnimationDrawable != null) { 255 mIconAnimationDrawable.stop(); 256 } 257 } 258 259 @Override onStop()260 protected void onStop() { 261 super.onStop(); 262 stopIconAnimation(); 263 } 264 265 @Override onDestroy()266 protected void onDestroy() { 267 stopListenOrientationEvent(); 268 super.onDestroy(); 269 } 270 animateProgress(int progress)271 private void animateProgress(int progress) { 272 if (mCanAssumeUdfps) { 273 // UDFPS animations are owned by SystemUI 274 if (progress >= PROGRESS_BAR_MAX) { 275 // Wait for any animations in SysUI to finish, then proceed to next page 276 getMainThreadHandler().postDelayed(mDelayedFinishRunnable, FINISH_DELAY); 277 } 278 return; 279 } 280 if (mProgressAnim != null) { 281 mProgressAnim.cancel(); 282 } 283 ObjectAnimator anim = ObjectAnimator.ofInt(mProgressBar, "progress", 284 mProgressBar.getProgress(), progress); 285 anim.addListener(mProgressAnimationListener); 286 anim.setInterpolator(mFastOutSlowInInterpolator); 287 anim.setDuration(250); 288 anim.start(); 289 mProgressAnim = anim; 290 } 291 animateFlash()292 private void animateFlash() { 293 if (mIconBackgroundBlinksDrawable != null) { 294 mIconBackgroundBlinksDrawable.start(); 295 } 296 } 297 getFinishIntent()298 protected Intent getFinishIntent() { 299 return new Intent(this, FingerprintEnrollFinish.class); 300 } 301 updateTitleAndDescription()302 private void updateTitleAndDescription() { 303 if (mSidecar == null || mSidecar.getEnrollmentSteps() == -1) { 304 if (mCanAssumeUdfps) { 305 // setHeaderText(R.string.security_settings_fingerprint_enroll_udfps_title); 306 // Don't use BiometricEnrollBase#setHeaderText, since that invokes setTitle, 307 // which gets announced for a11y upon entering the page. For UDFPS, we want to 308 // announce a different string for a11y upon entering the page. 309 getLayout().setHeaderText( 310 R.string.security_settings_fingerprint_enroll_udfps_title); 311 setDescriptionText(R.string.security_settings_udfps_enroll_start_message); 312 313 final CharSequence description = getString( 314 R.string.security_settings_udfps_enroll_a11y); 315 getLayout().getHeaderTextView().setContentDescription(description); 316 setTitle(description); 317 } else { 318 setDescriptionText(R.string.security_settings_fingerprint_enroll_start_message); 319 } 320 } else if (mCanAssumeUdfps && !isCenterEnrollmentComplete()) { 321 if (mIsSetupWizard) { 322 setHeaderText(R.string.security_settings_udfps_enroll_title_one_more_time); 323 } else { 324 setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title); 325 } 326 setDescriptionText(R.string.security_settings_udfps_enroll_start_message); 327 } else { 328 if (mCanAssumeUdfps) { 329 setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title); 330 if (mIsAccessibilityEnabled) { 331 setDescriptionText(R.string.security_settings_udfps_enroll_repeat_a11y_message); 332 } else { 333 setDescriptionText(R.string.security_settings_udfps_enroll_repeat_message); 334 } 335 } else { 336 setDescriptionText(R.string.security_settings_fingerprint_enroll_repeat_message); 337 } 338 } 339 } 340 isCenterEnrollmentComplete()341 private boolean isCenterEnrollmentComplete() { 342 if (mSidecar == null || mSidecar.getEnrollmentSteps() == -1) { 343 return false; 344 } 345 final int stepsEnrolled = mSidecar.getEnrollmentSteps() - mSidecar.getEnrollmentRemaining(); 346 return stepsEnrolled >= NUM_CENTER_TOUCHES; 347 } 348 349 @Override onEnrollmentHelp(int helpMsgId, CharSequence helpString)350 public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) { 351 if (!TextUtils.isEmpty(helpString)) { 352 if (!mCanAssumeUdfps) { 353 mErrorText.removeCallbacks(mTouchAgainRunnable); 354 } 355 showError(helpString); 356 } 357 } 358 359 @Override onEnrollmentError(int errMsgId, CharSequence errString)360 public void onEnrollmentError(int errMsgId, CharSequence errString) { 361 FingerprintErrorDialog.showErrorDialog(this, errMsgId); 362 stopIconAnimation(); 363 if (!mCanAssumeUdfps) { 364 mErrorText.removeCallbacks(mTouchAgainRunnable); 365 } 366 } 367 368 @Override onEnrollmentProgressChange(int steps, int remaining)369 public void onEnrollmentProgressChange(int steps, int remaining) { 370 updateProgress(true /* animate */); 371 updateTitleAndDescription(); 372 clearError(); 373 animateFlash(); 374 if (!mCanAssumeUdfps) { 375 mErrorText.removeCallbacks(mTouchAgainRunnable); 376 mErrorText.postDelayed(mTouchAgainRunnable, HINT_TIMEOUT_DURATION); 377 } else { 378 if (mIsAccessibilityEnabled) { 379 final int percent = (int) (((float)(steps - remaining) / (float) steps) * 100); 380 CharSequence cs = getString( 381 R.string.security_settings_udfps_enroll_progress_a11y_message, percent); 382 AccessibilityEvent e = AccessibilityEvent.obtain(); 383 e.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT); 384 e.setClassName(getClass().getName()); 385 e.setPackageName(getPackageName()); 386 e.getText().add(cs); 387 mAccessibilityManager.sendAccessibilityEvent(e); 388 } 389 } 390 } 391 updateProgress(boolean animate)392 private void updateProgress(boolean animate) { 393 if (mSidecar == null || !mSidecar.isEnrolling()) { 394 Log.d(TAG, "Enrollment not started yet"); 395 return; 396 } 397 398 int progress = getProgress( 399 mSidecar.getEnrollmentSteps(), mSidecar.getEnrollmentRemaining()); 400 if (animate) { 401 animateProgress(progress); 402 } else { 403 if (mProgressBar != null) { 404 mProgressBar.setProgress(progress); 405 } 406 if (progress >= PROGRESS_BAR_MAX) { 407 mDelayedFinishRunnable.run(); 408 } 409 } 410 } 411 getProgress(int steps, int remaining)412 private int getProgress(int steps, int remaining) { 413 if (steps == -1) { 414 return 0; 415 } 416 int progress = Math.max(0, steps + 1 - remaining); 417 return PROGRESS_BAR_MAX * progress / (steps + 1); 418 } 419 showIconTouchDialog()420 private void showIconTouchDialog() { 421 mIconTouchCount = 0; 422 new IconTouchDialog().show(getSupportFragmentManager(), null /* tag */); 423 } 424 showError(CharSequence error)425 private void showError(CharSequence error) { 426 if (mCanAssumeUdfps) { 427 setHeaderText(error); 428 // Show nothing for subtitle when getting an error message. 429 setDescriptionText(""); 430 } else { 431 mErrorText.setText(error); 432 if (mErrorText.getVisibility() == View.INVISIBLE) { 433 mErrorText.setVisibility(View.VISIBLE); 434 mErrorText.setTranslationY(getResources().getDimensionPixelSize( 435 R.dimen.fingerprint_error_text_appear_distance)); 436 mErrorText.setAlpha(0f); 437 mErrorText.animate() 438 .alpha(1f) 439 .translationY(0f) 440 .setDuration(200) 441 .setInterpolator(mLinearOutSlowInInterpolator) 442 .start(); 443 } else { 444 mErrorText.animate().cancel(); 445 mErrorText.setAlpha(1f); 446 mErrorText.setTranslationY(0f); 447 } 448 } 449 if (isResumed()) { 450 mVibrator.vibrate(VIBRATE_EFFECT_ERROR, FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES); 451 } 452 } 453 clearError()454 private void clearError() { 455 if (!mCanAssumeUdfps && mErrorText.getVisibility() == View.VISIBLE) { 456 mErrorText.animate() 457 .alpha(0f) 458 .translationY(getResources().getDimensionPixelSize( 459 R.dimen.fingerprint_error_text_disappear_distance)) 460 .setDuration(100) 461 .setInterpolator(mFastOutLinearInInterpolator) 462 .withEndAction(() -> mErrorText.setVisibility(View.INVISIBLE)) 463 .start(); 464 } 465 } 466 listenOrientationEvent()467 private void listenOrientationEvent() { 468 mOrientationEventListener = new OrientationEventListener(this) { 469 @Override 470 public void onOrientationChanged(int orientation) { 471 final int currentRotation = getDisplay().getRotation(); 472 if ((mPreviousRotation == Surface.ROTATION_90 473 && currentRotation == Surface.ROTATION_270) || ( 474 mPreviousRotation == Surface.ROTATION_270 475 && currentRotation == Surface.ROTATION_90)) { 476 mPreviousRotation = currentRotation; 477 recreate(); 478 } 479 } 480 }; 481 mOrientationEventListener.enable(); 482 mPreviousRotation = getDisplay().getRotation(); 483 } 484 stopListenOrientationEvent()485 private void stopListenOrientationEvent() { 486 if (mOrientationEventListener != null) { 487 mOrientationEventListener.disable(); 488 } 489 mOrientationEventListener = null; 490 } 491 492 private final Animator.AnimatorListener mProgressAnimationListener 493 = new Animator.AnimatorListener() { 494 495 @Override 496 public void onAnimationStart(Animator animation) { } 497 498 @Override 499 public void onAnimationRepeat(Animator animation) { } 500 501 @Override 502 public void onAnimationEnd(Animator animation) { 503 if (mProgressBar.getProgress() >= PROGRESS_BAR_MAX) { 504 mProgressBar.postDelayed(mDelayedFinishRunnable, FINISH_DELAY); 505 } 506 } 507 508 @Override 509 public void onAnimationCancel(Animator animation) { } 510 }; 511 512 // Give the user a chance to see progress completed before jumping to the next stage. 513 private final Runnable mDelayedFinishRunnable = new Runnable() { 514 @Override 515 public void run() { 516 launchFinish(mToken); 517 } 518 }; 519 520 private final Animatable2.AnimationCallback mIconAnimationCallback = 521 new Animatable2.AnimationCallback() { 522 @Override 523 public void onAnimationEnd(Drawable d) { 524 if (mAnimationCancelled) { 525 return; 526 } 527 528 // Start animation after it has ended. 529 mProgressBar.post(new Runnable() { 530 @Override 531 public void run() { 532 startIconAnimation(); 533 } 534 }); 535 } 536 }; 537 538 private final Runnable mShowDialogRunnable = new Runnable() { 539 @Override 540 public void run() { 541 showIconTouchDialog(); 542 } 543 }; 544 545 private final Runnable mTouchAgainRunnable = new Runnable() { 546 @Override 547 public void run() { 548 showError(getString(R.string.security_settings_fingerprint_enroll_lift_touch_again)); 549 } 550 }; 551 552 @Override getMetricsCategory()553 public int getMetricsCategory() { 554 return SettingsEnums.FINGERPRINT_ENROLLING; 555 } 556 557 public static class IconTouchDialog extends InstrumentedDialogFragment { 558 559 @Override onCreateDialog(Bundle savedInstanceState)560 public Dialog onCreateDialog(Bundle savedInstanceState) { 561 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); 562 builder.setTitle(R.string.security_settings_fingerprint_enroll_touch_dialog_title) 563 .setMessage(R.string.security_settings_fingerprint_enroll_touch_dialog_message) 564 .setPositiveButton(R.string.security_settings_fingerprint_enroll_dialog_ok, 565 new DialogInterface.OnClickListener() { 566 @Override 567 public void onClick(DialogInterface dialog, int which) { 568 dialog.dismiss(); 569 } 570 }); 571 return builder.create(); 572 } 573 574 @Override getMetricsCategory()575 public int getMetricsCategory() { 576 return SettingsEnums.DIALOG_FINGERPRINT_ICON_TOUCH; 577 } 578 } 579 } 580