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