Home | History | Annotate | Download | only in fingerprint
      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
     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
    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
    153     public void onEnterAnimationComplete() {
    154         super.onEnterAnimationComplete();
    155         mAnimationCancelled = false;
    156         startIconAnimation();
    157     }
    158 
    159     private void startIconAnimation() {
    160         mIconAnimationDrawable.start();
    161     }
    162 
    163     private void stopIconAnimation() {
    164         mAnimationCancelled = true;
    165         mIconAnimationDrawable.stop();
    166     }
    167 
    168     @Override
    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
    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 
    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 
    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 
    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 
    247     protected Intent getFinishIntent() {
    248         return new Intent(this, FingerprintEnrollFinish.class);
    249     }
    250 
    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
    266     public void onEnrollmentHelp(CharSequence helpString) {
    267         mErrorText.setText(helpString);
    268     }
    269 
    270     @Override
    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
    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 
    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 
    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 
    317     private void showErrorDialog(CharSequence msg, int msgId) {
    318         ErrorDialog dlg = ErrorDialog.newInstance(msg, msgId);
    319         dlg.show(getFragmentManager(), ErrorDialog.class.getName());
    320     }
    321 
    322     private void showIconTouchDialog() {
    323         mIconTouchCount = 0;
    324         new IconTouchDialog().show(getFragmentManager(), null /* tag */);
    325     }
    326 
    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 
    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
    426     protected int getMetricsCategory() {
    427         return MetricsEvent.FINGERPRINT_ENROLLING;
    428     }
    429 
    430     public static class IconTouchDialog extends DialogFragment {
    431 
    432         @Override
    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          */
    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
    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