• 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.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.view.MotionEvent;
36 import android.view.View;
37 import android.view.animation.AnimationUtils;
38 import android.view.animation.Interpolator;
39 import android.widget.ImageView;
40 import android.widget.ProgressBar;
41 import android.widget.TextView;
42 
43 import com.android.internal.logging.MetricsLogger;
44 import com.android.settings.ChooseLockSettingsHelper;
45 import com.android.settings.R;
46 
47 /**
48  * Activity which handles the actual enrolling for fingerprint.
49  */
50 public 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
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         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
onStart()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
onEnterAnimationComplete()152     public void onEnterAnimationComplete() {
153         super.onEnterAnimationComplete();
154         mAnimationCancelled = false;
155         startIconAnimation();
156     }
157 
startIconAnimation()158     private void startIconAnimation() {
159         mIconAnimationDrawable.start();
160     }
161 
stopIconAnimation()162     private void stopIconAnimation() {
163         mAnimationCancelled = true;
164         mIconAnimationDrawable.stop();
165     }
166 
167     @Override
onStop()168     protected void onStop() {
169         super.onStop();
170         mSidecar.setListener(null);
171         stopIconAnimation();
172         if (!isChangingConfigurations()) {
173             finish();
174         }
175     }
176 
animateProgress(int progress)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 
animateFlash()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 
launchFinish(byte[] token)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 
getFinishIntent()226     protected Intent getFinishIntent() {
227         return new Intent(this, FingerprintEnrollFinish.class);
228     }
229 
updateDescription()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
onEnrollmentHelp(CharSequence helpString)245     public void onEnrollmentHelp(CharSequence helpString) {
246         mErrorText.setText(helpString);
247     }
248 
249     @Override
onEnrollmentError(int errMsgId, CharSequence errString)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
onEnrollmentProgressChange(int steps, int remaining)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 
updateProgress(boolean animate)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 
getProgress(int steps, int remaining)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 
showErrorDialog(CharSequence msg, int msgId)296     private void showErrorDialog(CharSequence msg, int msgId) {
297         ErrorDialog dlg = ErrorDialog.newInstance(msg, msgId);
298         dlg.show(getFragmentManager(), ErrorDialog.class.getName());
299     }
300 
showIconTouchDialog()301     private void showIconTouchDialog() {
302         mIconTouchCount = 0;
303         new IconTouchDialog().show(getFragmentManager(), null /* tag */);
304     }
305 
showError(CharSequence error)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 
clearError()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
getMetricsCategory()405     protected int getMetricsCategory() {
406         return MetricsLogger.FINGERPRINT_ENROLLING;
407     }
408 
409     public static class IconTouchDialog extends DialogFragment {
410 
411         @Override
onCreateDialog(Bundle savedInstanceState)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          */
newInstance(CharSequence msg, int msgId)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
onCreateDialog(Bundle savedInstanceState)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