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