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.fingerprint; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.ObjectAnimator; 22 import android.animation.ValueAnimator; 23 import android.app.Activity; 24 import android.app.AlertDialog; 25 import android.app.Dialog; 26 import android.content.DialogInterface; 27 import android.content.Intent; 28 import android.graphics.drawable.Animatable2; 29 import android.graphics.drawable.AnimatedVectorDrawable; 30 import android.graphics.drawable.Drawable; 31 import android.graphics.drawable.LayerDrawable; 32 import android.hardware.fingerprint.FingerprintManager; 33 import android.media.AudioAttributes; 34 import android.os.Bundle; 35 import android.os.UserHandle; 36 import android.os.VibrationEffect; 37 import android.os.Vibrator; 38 import android.text.TextUtils; 39 import android.view.MotionEvent; 40 import android.view.View; 41 import android.view.animation.AnimationUtils; 42 import android.view.animation.Interpolator; 43 import android.widget.Button; 44 import android.widget.ProgressBar; 45 import android.widget.TextView; 46 47 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 48 import com.android.settings.R; 49 import com.android.settings.core.instrumentation.InstrumentedDialogFragment; 50 import com.android.settings.password.ChooseLockSettingsHelper; 51 52 /** 53 * Activity which handles the actual enrolling for fingerprint. 54 */ 55 public class FingerprintEnrollEnrolling extends FingerprintEnrollBase 56 implements FingerprintEnrollSidecar.Listener { 57 58 static final String TAG_SIDECAR = "sidecar"; 59 60 private static final int PROGRESS_BAR_MAX = 10000; 61 private static final int FINISH_DELAY = 250; 62 63 /** 64 * If we don't see progress during this time, we show an error message to remind the user that 65 * he needs to lift the finger and touch again. 66 */ 67 private static final int HINT_TIMEOUT_DURATION = 2500; 68 69 /** 70 * How long the user needs to touch the icon until we show the dialog. 71 */ 72 private static final long ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN = 500; 73 74 /** 75 * How many times the user needs to touch the icon until we show the dialog that this is not the 76 * fingerprint sensor. 77 */ 78 private static final int ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN = 3; 79 80 private static final VibrationEffect VIBRATE_EFFECT_ERROR = 81 VibrationEffect.createWaveform(new long[] {0, 5, 55, 60}, -1); 82 private static final AudioAttributes FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES = 83 new AudioAttributes.Builder() 84 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) 85 .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) 86 .build(); 87 88 private ProgressBar mProgressBar; 89 private ObjectAnimator mProgressAnim; 90 private TextView mStartMessage; 91 private TextView mRepeatMessage; 92 private TextView mErrorText; 93 private Interpolator mFastOutSlowInInterpolator; 94 private Interpolator mLinearOutSlowInInterpolator; 95 private Interpolator mFastOutLinearInInterpolator; 96 private int mIconTouchCount; 97 private FingerprintEnrollSidecar mSidecar; 98 private boolean mAnimationCancelled; 99 private AnimatedVectorDrawable mIconAnimationDrawable; 100 private AnimatedVectorDrawable mIconBackgroundBlinksDrawable; 101 private boolean mRestoring; 102 private Vibrator mVibrator; 103 104 @Override 105 protected void onCreate(Bundle savedInstanceState) { 106 super.onCreate(savedInstanceState); 107 setContentView(R.layout.fingerprint_enroll_enrolling); 108 setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title); 109 mStartMessage = (TextView) findViewById(R.id.start_message); 110 mRepeatMessage = (TextView) findViewById(R.id.repeat_message); 111 mErrorText = (TextView) findViewById(R.id.error_text); 112 mProgressBar = (ProgressBar) findViewById(R.id.fingerprint_progress_bar); 113 mVibrator = getSystemService(Vibrator.class); 114 115 Button skipButton = findViewById(R.id.skip_button); 116 skipButton.setOnClickListener(this); 117 118 final LayerDrawable fingerprintDrawable = (LayerDrawable) mProgressBar.getBackground(); 119 mIconAnimationDrawable = (AnimatedVectorDrawable) 120 fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_animation); 121 mIconBackgroundBlinksDrawable = (AnimatedVectorDrawable) 122 fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_background); 123 mIconAnimationDrawable.registerAnimationCallback(mIconAnimationCallback); 124 mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator( 125 this, android.R.interpolator.fast_out_slow_in); 126 mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator( 127 this, android.R.interpolator.linear_out_slow_in); 128 mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator( 129 this, android.R.interpolator.fast_out_linear_in); 130 mProgressBar.setOnTouchListener(new View.OnTouchListener() { 131 @Override 132 public boolean onTouch(View v, MotionEvent event) { 133 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { 134 mIconTouchCount++; 135 if (mIconTouchCount == ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN) { 136 showIconTouchDialog(); 137 } else { 138 mProgressBar.postDelayed(mShowDialogRunnable, 139 ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN); 140 } 141 } else if (event.getActionMasked() == MotionEvent.ACTION_CANCEL 142 || event.getActionMasked() == MotionEvent.ACTION_UP) { 143 mProgressBar.removeCallbacks(mShowDialogRunnable); 144 } 145 return true; 146 } 147 }); 148 mRestoring = savedInstanceState != null; 149 } 150 151 @Override 152 protected void onStart() { 153 super.onStart(); 154 mSidecar = (FingerprintEnrollSidecar) getFragmentManager().findFragmentByTag(TAG_SIDECAR); 155 if (mSidecar == null) { 156 mSidecar = new FingerprintEnrollSidecar(); 157 getFragmentManager().beginTransaction().add(mSidecar, TAG_SIDECAR).commit(); 158 } 159 mSidecar.setListener(this); 160 updateProgress(false /* animate */); 161 updateDescription(); 162 if (mRestoring) { 163 startIconAnimation(); 164 } 165 } 166 167 @Override 168 public void onEnterAnimationComplete() { 169 super.onEnterAnimationComplete(); 170 mAnimationCancelled = false; 171 startIconAnimation(); 172 } 173 174 private void startIconAnimation() { 175 mIconAnimationDrawable.start(); 176 } 177 178 private void stopIconAnimation() { 179 mAnimationCancelled = true; 180 mIconAnimationDrawable.stop(); 181 } 182 183 @Override 184 protected void onStop() { 185 super.onStop(); 186 if (mSidecar != null) { 187 mSidecar.setListener(null); 188 } 189 stopIconAnimation(); 190 if (!isChangingConfigurations()) { 191 if (mSidecar != null) { 192 mSidecar.cancelEnrollment(); 193 getFragmentManager().beginTransaction().remove(mSidecar).commitAllowingStateLoss(); 194 } 195 finish(); 196 } 197 } 198 199 @Override 200 public void onBackPressed() { 201 if (mSidecar != null) { 202 mSidecar.setListener(null); 203 mSidecar.cancelEnrollment(); 204 getFragmentManager().beginTransaction().remove(mSidecar).commitAllowingStateLoss(); 205 mSidecar = null; 206 } 207 super.onBackPressed(); 208 } 209 210 @Override 211 public void onClick(View v) { 212 switch (v.getId()) { 213 case R.id.skip_button: 214 setResult(RESULT_SKIP); 215 finish(); 216 break; 217 default: 218 super.onClick(v); 219 } 220 } 221 222 private void animateProgress(int progress) { 223 if (mProgressAnim != null) { 224 mProgressAnim.cancel(); 225 } 226 ObjectAnimator anim = ObjectAnimator.ofInt(mProgressBar, "progress", 227 mProgressBar.getProgress(), progress); 228 anim.addListener(mProgressAnimationListener); 229 anim.setInterpolator(mFastOutSlowInInterpolator); 230 anim.setDuration(250); 231 anim.start(); 232 mProgressAnim = anim; 233 } 234 235 private void animateFlash() { 236 mIconBackgroundBlinksDrawable.start(); 237 } 238 239 private void launchFinish(byte[] token) { 240 Intent intent = getFinishIntent(); 241 intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT 242 | Intent.FLAG_ACTIVITY_CLEAR_TOP 243 | Intent.FLAG_ACTIVITY_SINGLE_TOP); 244 intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token); 245 if (mUserId != UserHandle.USER_NULL) { 246 intent.putExtra(Intent.EXTRA_USER_ID, mUserId); 247 } 248 startActivity(intent); 249 overridePendingTransition(R.anim.suw_slide_next_in, R.anim.suw_slide_next_out); 250 finish(); 251 } 252 253 protected Intent getFinishIntent() { 254 return new Intent(this, FingerprintEnrollFinish.class); 255 } 256 257 private void updateDescription() { 258 if (mSidecar.getEnrollmentSteps() == -1) { 259 mStartMessage.setVisibility(View.VISIBLE); 260 mRepeatMessage.setVisibility(View.INVISIBLE); 261 } else { 262 mStartMessage.setVisibility(View.INVISIBLE); 263 mRepeatMessage.setVisibility(View.VISIBLE); 264 } 265 } 266 267 268 @Override 269 public void onEnrollmentHelp(CharSequence helpString) { 270 if (!TextUtils.isEmpty(helpString)) { 271 mErrorText.removeCallbacks(mTouchAgainRunnable); 272 showError(helpString); 273 } 274 } 275 276 @Override 277 public void onEnrollmentError(int errMsgId, CharSequence errString) { 278 int msgId; 279 switch (errMsgId) { 280 case FingerprintManager.FINGERPRINT_ERROR_TIMEOUT: 281 // This message happens when the underlying crypto layer decides to revoke the 282 // enrollment auth token. 283 msgId = R.string.security_settings_fingerprint_enroll_error_timeout_dialog_message; 284 break; 285 default: 286 // There's nothing specific to tell the user about. Ask them to try again. 287 msgId = R.string.security_settings_fingerprint_enroll_error_generic_dialog_message; 288 break; 289 } 290 showErrorDialog(getText(msgId), errMsgId); 291 stopIconAnimation(); 292 mErrorText.removeCallbacks(mTouchAgainRunnable); 293 } 294 295 @Override 296 public void onEnrollmentProgressChange(int steps, int remaining) { 297 updateProgress(true /* animate */); 298 updateDescription(); 299 clearError(); 300 animateFlash(); 301 mErrorText.removeCallbacks(mTouchAgainRunnable); 302 mErrorText.postDelayed(mTouchAgainRunnable, HINT_TIMEOUT_DURATION); 303 } 304 305 private void updateProgress(boolean animate) { 306 int progress = getProgress( 307 mSidecar.getEnrollmentSteps(), mSidecar.getEnrollmentRemaining()); 308 if (animate) { 309 animateProgress(progress); 310 } else { 311 mProgressBar.setProgress(progress); 312 if (progress >= PROGRESS_BAR_MAX) { 313 mDelayedFinishRunnable.run(); 314 } 315 } 316 } 317 318 private int getProgress(int steps, int remaining) { 319 if (steps == -1) { 320 return 0; 321 } 322 int progress = Math.max(0, steps + 1 - remaining); 323 return PROGRESS_BAR_MAX * progress / (steps + 1); 324 } 325 326 private void showErrorDialog(CharSequence msg, int msgId) { 327 ErrorDialog dlg = ErrorDialog.newInstance(msg, msgId); 328 dlg.show(getFragmentManager(), ErrorDialog.class.getName()); 329 } 330 331 private void showIconTouchDialog() { 332 mIconTouchCount = 0; 333 new IconTouchDialog().show(getFragmentManager(), null /* tag */); 334 } 335 336 private void showError(CharSequence error) { 337 mErrorText.setText(error); 338 if (mErrorText.getVisibility() == View.INVISIBLE) { 339 mErrorText.setVisibility(View.VISIBLE); 340 mErrorText.setTranslationY(getResources().getDimensionPixelSize( 341 R.dimen.fingerprint_error_text_appear_distance)); 342 mErrorText.setAlpha(0f); 343 mErrorText.animate() 344 .alpha(1f) 345 .translationY(0f) 346 .setDuration(200) 347 .setInterpolator(mLinearOutSlowInInterpolator) 348 .start(); 349 } else { 350 mErrorText.animate().cancel(); 351 mErrorText.setAlpha(1f); 352 mErrorText.setTranslationY(0f); 353 } 354 if (isResumed()) { 355 mVibrator.vibrate(VIBRATE_EFFECT_ERROR, FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES); 356 } 357 } 358 359 private void clearError() { 360 if (mErrorText.getVisibility() == View.VISIBLE) { 361 mErrorText.animate() 362 .alpha(0f) 363 .translationY(getResources().getDimensionPixelSize( 364 R.dimen.fingerprint_error_text_disappear_distance)) 365 .setDuration(100) 366 .setInterpolator(mFastOutLinearInInterpolator) 367 .withEndAction(() -> mErrorText.setVisibility(View.INVISIBLE)) 368 .start(); 369 } 370 } 371 372 private final Animator.AnimatorListener mProgressAnimationListener 373 = new Animator.AnimatorListener() { 374 375 @Override 376 public void onAnimationStart(Animator animation) { } 377 378 @Override 379 public void onAnimationRepeat(Animator animation) { } 380 381 @Override 382 public void onAnimationEnd(Animator animation) { 383 if (mProgressBar.getProgress() >= PROGRESS_BAR_MAX) { 384 mProgressBar.postDelayed(mDelayedFinishRunnable, FINISH_DELAY); 385 } 386 } 387 388 @Override 389 public void onAnimationCancel(Animator animation) { } 390 }; 391 392 // Give the user a chance to see progress completed before jumping to the next stage. 393 private final Runnable mDelayedFinishRunnable = new Runnable() { 394 @Override 395 public void run() { 396 launchFinish(mToken); 397 } 398 }; 399 400 private final Animatable2.AnimationCallback mIconAnimationCallback = 401 new Animatable2.AnimationCallback() { 402 @Override 403 public void onAnimationEnd(Drawable d) { 404 if (mAnimationCancelled) { 405 return; 406 } 407 408 // Start animation after it has ended. 409 mProgressBar.post(new Runnable() { 410 @Override 411 public void run() { 412 startIconAnimation(); 413 } 414 }); 415 } 416 }; 417 418 private final Runnable mShowDialogRunnable = new Runnable() { 419 @Override 420 public void run() { 421 showIconTouchDialog(); 422 } 423 }; 424 425 private final Runnable mTouchAgainRunnable = new Runnable() { 426 @Override 427 public void run() { 428 showError(getString(R.string.security_settings_fingerprint_enroll_lift_touch_again)); 429 } 430 }; 431 432 @Override 433 public int getMetricsCategory() { 434 return MetricsEvent.FINGERPRINT_ENROLLING; 435 } 436 437 public static class IconTouchDialog extends InstrumentedDialogFragment { 438 439 @Override 440 public Dialog onCreateDialog(Bundle savedInstanceState) { 441 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); 442 builder.setTitle(R.string.security_settings_fingerprint_enroll_touch_dialog_title) 443 .setMessage(R.string.security_settings_fingerprint_enroll_touch_dialog_message) 444 .setPositiveButton(R.string.security_settings_fingerprint_enroll_dialog_ok, 445 new DialogInterface.OnClickListener() { 446 @Override 447 public void onClick(DialogInterface dialog, int which) { 448 dialog.dismiss(); 449 } 450 }); 451 return builder.create(); 452 } 453 454 @Override 455 public int getMetricsCategory() { 456 return MetricsEvent.DIALOG_FINGERPRINT_ICON_TOUCH; 457 } 458 } 459 460 public static class ErrorDialog extends InstrumentedDialogFragment { 461 462 /** 463 * Create a new instance of ErrorDialog. 464 * 465 * @param msg the string to show for message text 466 * @param msgId the FingerprintManager error id so we know the cause 467 * @return a new ErrorDialog 468 */ 469 static ErrorDialog newInstance(CharSequence msg, int msgId) { 470 ErrorDialog dlg = new ErrorDialog(); 471 Bundle args = new Bundle(); 472 args.putCharSequence("error_msg", msg); 473 args.putInt("error_id", msgId); 474 dlg.setArguments(args); 475 return dlg; 476 } 477 478 @Override 479 public Dialog onCreateDialog(Bundle savedInstanceState) { 480 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); 481 CharSequence errorString = getArguments().getCharSequence("error_msg"); 482 final int errMsgId = getArguments().getInt("error_id"); 483 builder.setTitle(R.string.security_settings_fingerprint_enroll_error_dialog_title) 484 .setMessage(errorString) 485 .setCancelable(false) 486 .setPositiveButton(R.string.security_settings_fingerprint_enroll_dialog_ok, 487 new DialogInterface.OnClickListener() { 488 @Override 489 public void onClick(DialogInterface dialog, int which) { 490 dialog.dismiss(); 491 boolean wasTimeout = 492 errMsgId == FingerprintManager.FINGERPRINT_ERROR_TIMEOUT; 493 Activity activity = getActivity(); 494 activity.setResult(wasTimeout ? 495 RESULT_TIMEOUT : RESULT_FINISHED); 496 activity.finish(); 497 } 498 }); 499 AlertDialog dialog = builder.create(); 500 dialog.setCanceledOnTouchOutside(false); 501 return dialog; 502 } 503 504 @Override 505 public int getMetricsCategory() { 506 return MetricsEvent.DIALOG_FINGERPINT_ERROR; 507 } 508 } 509 } 510