• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
onCreate(Bundle savedInstanceState)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
onStart()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
onEnterAnimationComplete()168     public void onEnterAnimationComplete() {
169         super.onEnterAnimationComplete();
170         mAnimationCancelled = false;
171         startIconAnimation();
172     }
173 
startIconAnimation()174     private void startIconAnimation() {
175         mIconAnimationDrawable.start();
176     }
177 
stopIconAnimation()178     private void stopIconAnimation() {
179         mAnimationCancelled = true;
180         mIconAnimationDrawable.stop();
181     }
182 
183     @Override
onStop()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
onBackPressed()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
onClick(View v)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 
animateProgress(int progress)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 
animateFlash()235     private void animateFlash() {
236         mIconBackgroundBlinksDrawable.start();
237     }
238 
launchFinish(byte[] token)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 
getFinishIntent()253     protected Intent getFinishIntent() {
254         return new Intent(this, FingerprintEnrollFinish.class);
255     }
256 
updateDescription()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
onEnrollmentHelp(CharSequence helpString)269     public void onEnrollmentHelp(CharSequence helpString) {
270         if (!TextUtils.isEmpty(helpString)) {
271             mErrorText.removeCallbacks(mTouchAgainRunnable);
272             showError(helpString);
273         }
274     }
275 
276     @Override
onEnrollmentError(int errMsgId, CharSequence errString)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
onEnrollmentProgressChange(int steps, int remaining)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 
updateProgress(boolean animate)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 
getProgress(int steps, int remaining)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 
showErrorDialog(CharSequence msg, int msgId)326     private void showErrorDialog(CharSequence msg, int msgId) {
327         ErrorDialog dlg = ErrorDialog.newInstance(msg, msgId);
328         dlg.show(getFragmentManager(), ErrorDialog.class.getName());
329     }
330 
showIconTouchDialog()331     private void showIconTouchDialog() {
332         mIconTouchCount = 0;
333         new IconTouchDialog().show(getFragmentManager(), null /* tag */);
334     }
335 
showError(CharSequence error)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 
clearError()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
getMetricsCategory()433     public int getMetricsCategory() {
434         return MetricsEvent.FINGERPRINT_ENROLLING;
435     }
436 
437     public static class IconTouchDialog extends InstrumentedDialogFragment {
438 
439         @Override
onCreateDialog(Bundle savedInstanceState)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
getMetricsCategory()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          */
newInstance(CharSequence msg, int msgId)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
onCreateDialog(Bundle savedInstanceState)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
getMetricsCategory()505         public int getMetricsCategory() {
506             return MetricsEvent.DIALOG_FINGERPINT_ERROR;
507         }
508     }
509 }
510