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