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