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