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