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