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