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
onCreate(Bundle savedInstanceState)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
onStart()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
onEnterAnimationComplete()153     public void onEnterAnimationComplete() {
154         super.onEnterAnimationComplete();
155         mAnimationCancelled = false;
156         startIconAnimation();
157     }
158 
startIconAnimation()159     private void startIconAnimation() {
160         mIconAnimationDrawable.start();
161     }
162 
stopIconAnimation()163     private void stopIconAnimation() {
164         mAnimationCancelled = true;
165         mIconAnimationDrawable.stop();
166     }
167 
168     @Override
onStop()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
onBackPressed()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 
animateProgress(int progress)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 
animateFlash()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 
launchFinish(byte[] token)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 
getFinishIntent()247     protected Intent getFinishIntent() {
248         return new Intent(this, FingerprintEnrollFinish.class);
249     }
250 
updateDescription()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
onEnrollmentHelp(CharSequence helpString)266     public void onEnrollmentHelp(CharSequence helpString) {
267         mErrorText.setText(helpString);
268     }
269 
270     @Override
onEnrollmentError(int errMsgId, CharSequence errString)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
onEnrollmentProgressChange(int steps, int remaining)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 
updateProgress(boolean animate)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 
getProgress(int steps, int remaining)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 
showErrorDialog(CharSequence msg, int msgId)317     private void showErrorDialog(CharSequence msg, int msgId) {
318         ErrorDialog dlg = ErrorDialog.newInstance(msg, msgId);
319         dlg.show(getFragmentManager(), ErrorDialog.class.getName());
320     }
321 
showIconTouchDialog()322     private void showIconTouchDialog() {
323         mIconTouchCount = 0;
324         new IconTouchDialog().show(getFragmentManager(), null /* tag */);
325     }
326 
showError(CharSequence error)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 
clearError()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
getMetricsCategory()426     protected int getMetricsCategory() {
427         return MetricsEvent.FINGERPRINT_ENROLLING;
428     }
429 
430     public static class IconTouchDialog extends DialogFragment {
431 
432         @Override
onCreateDialog(Bundle savedInstanceState)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          */
newInstance(CharSequence msg, int msgId)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
onCreateDialog(Bundle savedInstanceState)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