/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.settings.fingerprint; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.content.DialogInterface; import android.content.Intent; import android.graphics.drawable.Animatable2; import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.hardware.fingerprint.FingerprintManager; import android.media.AudioAttributes; import android.os.Bundle; import android.os.UserHandle; import android.os.VibrationEffect; import android.os.Vibrator; import android.text.TextUtils; import android.view.MotionEvent; import android.view.View; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.widget.Button; import android.widget.ProgressBar; import android.widget.TextView; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settings.password.ChooseLockSettingsHelper; /** * Activity which handles the actual enrolling for fingerprint. */ public class FingerprintEnrollEnrolling extends FingerprintEnrollBase implements FingerprintEnrollSidecar.Listener { static final String TAG_SIDECAR = "sidecar"; private static final int PROGRESS_BAR_MAX = 10000; private static final int FINISH_DELAY = 250; /** * If we don't see progress during this time, we show an error message to remind the user that * he needs to lift the finger and touch again. */ private static final int HINT_TIMEOUT_DURATION = 2500; /** * How long the user needs to touch the icon until we show the dialog. */ private static final long ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN = 500; /** * How many times the user needs to touch the icon until we show the dialog that this is not the * fingerprint sensor. */ private static final int ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN = 3; private static final VibrationEffect VIBRATE_EFFECT_ERROR = VibrationEffect.createWaveform(new long[] {0, 5, 55, 60}, -1); private static final AudioAttributes FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES = new AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) .build(); private ProgressBar mProgressBar; private ObjectAnimator mProgressAnim; private TextView mStartMessage; private TextView mRepeatMessage; private TextView mErrorText; private Interpolator mFastOutSlowInInterpolator; private Interpolator mLinearOutSlowInInterpolator; private Interpolator mFastOutLinearInInterpolator; private int mIconTouchCount; private FingerprintEnrollSidecar mSidecar; private boolean mAnimationCancelled; private AnimatedVectorDrawable mIconAnimationDrawable; private AnimatedVectorDrawable mIconBackgroundBlinksDrawable; private boolean mRestoring; private Vibrator mVibrator; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.fingerprint_enroll_enrolling); setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title); mStartMessage = (TextView) findViewById(R.id.start_message); mRepeatMessage = (TextView) findViewById(R.id.repeat_message); mErrorText = (TextView) findViewById(R.id.error_text); mProgressBar = (ProgressBar) findViewById(R.id.fingerprint_progress_bar); mVibrator = getSystemService(Vibrator.class); Button skipButton = findViewById(R.id.skip_button); skipButton.setOnClickListener(this); final LayerDrawable fingerprintDrawable = (LayerDrawable) mProgressBar.getBackground(); mIconAnimationDrawable = (AnimatedVectorDrawable) fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_animation); mIconBackgroundBlinksDrawable = (AnimatedVectorDrawable) fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_background); mIconAnimationDrawable.registerAnimationCallback(mIconAnimationCallback); mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator( this, android.R.interpolator.fast_out_slow_in); mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator( this, android.R.interpolator.linear_out_slow_in); mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator( this, android.R.interpolator.fast_out_linear_in); mProgressBar.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { mIconTouchCount++; if (mIconTouchCount == ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN) { showIconTouchDialog(); } else { mProgressBar.postDelayed(mShowDialogRunnable, ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN); } } else if (event.getActionMasked() == MotionEvent.ACTION_CANCEL || event.getActionMasked() == MotionEvent.ACTION_UP) { mProgressBar.removeCallbacks(mShowDialogRunnable); } return true; } }); mRestoring = savedInstanceState != null; } @Override protected void onStart() { super.onStart(); mSidecar = (FingerprintEnrollSidecar) getFragmentManager().findFragmentByTag(TAG_SIDECAR); if (mSidecar == null) { mSidecar = new FingerprintEnrollSidecar(); getFragmentManager().beginTransaction().add(mSidecar, TAG_SIDECAR).commit(); } mSidecar.setListener(this); updateProgress(false /* animate */); updateDescription(); if (mRestoring) { startIconAnimation(); } } @Override public void onEnterAnimationComplete() { super.onEnterAnimationComplete(); mAnimationCancelled = false; startIconAnimation(); } private void startIconAnimation() { mIconAnimationDrawable.start(); } private void stopIconAnimation() { mAnimationCancelled = true; mIconAnimationDrawable.stop(); } @Override protected void onStop() { super.onStop(); if (mSidecar != null) { mSidecar.setListener(null); } stopIconAnimation(); if (!isChangingConfigurations()) { if (mSidecar != null) { mSidecar.cancelEnrollment(); getFragmentManager().beginTransaction().remove(mSidecar).commitAllowingStateLoss(); } finish(); } } @Override public void onBackPressed() { if (mSidecar != null) { mSidecar.setListener(null); mSidecar.cancelEnrollment(); getFragmentManager().beginTransaction().remove(mSidecar).commitAllowingStateLoss(); mSidecar = null; } super.onBackPressed(); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.skip_button: setResult(RESULT_SKIP); finish(); break; default: super.onClick(v); } } private void animateProgress(int progress) { if (mProgressAnim != null) { mProgressAnim.cancel(); } ObjectAnimator anim = ObjectAnimator.ofInt(mProgressBar, "progress", mProgressBar.getProgress(), progress); anim.addListener(mProgressAnimationListener); anim.setInterpolator(mFastOutSlowInInterpolator); anim.setDuration(250); anim.start(); mProgressAnim = anim; } private void animateFlash() { mIconBackgroundBlinksDrawable.start(); } private void launchFinish(byte[] token) { Intent intent = getFinishIntent(); intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token); if (mUserId != UserHandle.USER_NULL) { intent.putExtra(Intent.EXTRA_USER_ID, mUserId); } startActivity(intent); overridePendingTransition(R.anim.suw_slide_next_in, R.anim.suw_slide_next_out); finish(); } protected Intent getFinishIntent() { return new Intent(this, FingerprintEnrollFinish.class); } private void updateDescription() { if (mSidecar.getEnrollmentSteps() == -1) { mStartMessage.setVisibility(View.VISIBLE); mRepeatMessage.setVisibility(View.INVISIBLE); } else { mStartMessage.setVisibility(View.INVISIBLE); mRepeatMessage.setVisibility(View.VISIBLE); } } @Override public void onEnrollmentHelp(CharSequence helpString) { if (!TextUtils.isEmpty(helpString)) { mErrorText.removeCallbacks(mTouchAgainRunnable); showError(helpString); } } @Override public void onEnrollmentError(int errMsgId, CharSequence errString) { int msgId; switch (errMsgId) { case FingerprintManager.FINGERPRINT_ERROR_TIMEOUT: // This message happens when the underlying crypto layer decides to revoke the // enrollment auth token. msgId = R.string.security_settings_fingerprint_enroll_error_timeout_dialog_message; break; default: // There's nothing specific to tell the user about. Ask them to try again. msgId = R.string.security_settings_fingerprint_enroll_error_generic_dialog_message; break; } showErrorDialog(getText(msgId), errMsgId); stopIconAnimation(); mErrorText.removeCallbacks(mTouchAgainRunnable); } @Override public void onEnrollmentProgressChange(int steps, int remaining) { updateProgress(true /* animate */); updateDescription(); clearError(); animateFlash(); mErrorText.removeCallbacks(mTouchAgainRunnable); mErrorText.postDelayed(mTouchAgainRunnable, HINT_TIMEOUT_DURATION); } private void updateProgress(boolean animate) { int progress = getProgress( mSidecar.getEnrollmentSteps(), mSidecar.getEnrollmentRemaining()); if (animate) { animateProgress(progress); } else { mProgressBar.setProgress(progress); if (progress >= PROGRESS_BAR_MAX) { mDelayedFinishRunnable.run(); } } } private int getProgress(int steps, int remaining) { if (steps == -1) { return 0; } int progress = Math.max(0, steps + 1 - remaining); return PROGRESS_BAR_MAX * progress / (steps + 1); } private void showErrorDialog(CharSequence msg, int msgId) { ErrorDialog dlg = ErrorDialog.newInstance(msg, msgId); dlg.show(getFragmentManager(), ErrorDialog.class.getName()); } private void showIconTouchDialog() { mIconTouchCount = 0; new IconTouchDialog().show(getFragmentManager(), null /* tag */); } private void showError(CharSequence error) { mErrorText.setText(error); if (mErrorText.getVisibility() == View.INVISIBLE) { mErrorText.setVisibility(View.VISIBLE); mErrorText.setTranslationY(getResources().getDimensionPixelSize( R.dimen.fingerprint_error_text_appear_distance)); mErrorText.setAlpha(0f); mErrorText.animate() .alpha(1f) .translationY(0f) .setDuration(200) .setInterpolator(mLinearOutSlowInInterpolator) .start(); } else { mErrorText.animate().cancel(); mErrorText.setAlpha(1f); mErrorText.setTranslationY(0f); } if (isResumed()) { mVibrator.vibrate(VIBRATE_EFFECT_ERROR, FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES); } } private void clearError() { if (mErrorText.getVisibility() == View.VISIBLE) { mErrorText.animate() .alpha(0f) .translationY(getResources().getDimensionPixelSize( R.dimen.fingerprint_error_text_disappear_distance)) .setDuration(100) .setInterpolator(mFastOutLinearInInterpolator) .withEndAction(() -> mErrorText.setVisibility(View.INVISIBLE)) .start(); } } private final Animator.AnimatorListener mProgressAnimationListener = new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { if (mProgressBar.getProgress() >= PROGRESS_BAR_MAX) { mProgressBar.postDelayed(mDelayedFinishRunnable, FINISH_DELAY); } } @Override public void onAnimationCancel(Animator animation) { } }; // Give the user a chance to see progress completed before jumping to the next stage. private final Runnable mDelayedFinishRunnable = new Runnable() { @Override public void run() { launchFinish(mToken); } }; private final Animatable2.AnimationCallback mIconAnimationCallback = new Animatable2.AnimationCallback() { @Override public void onAnimationEnd(Drawable d) { if (mAnimationCancelled) { return; } // Start animation after it has ended. mProgressBar.post(new Runnable() { @Override public void run() { startIconAnimation(); } }); } }; private final Runnable mShowDialogRunnable = new Runnable() { @Override public void run() { showIconTouchDialog(); } }; private final Runnable mTouchAgainRunnable = new Runnable() { @Override public void run() { showError(getString(R.string.security_settings_fingerprint_enroll_lift_touch_again)); } }; @Override public int getMetricsCategory() { return MetricsEvent.FINGERPRINT_ENROLLING; } public static class IconTouchDialog extends InstrumentedDialogFragment { @Override public Dialog onCreateDialog(Bundle savedInstanceState) { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setTitle(R.string.security_settings_fingerprint_enroll_touch_dialog_title) .setMessage(R.string.security_settings_fingerprint_enroll_touch_dialog_message) .setPositiveButton(R.string.security_settings_fingerprint_enroll_dialog_ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }); return builder.create(); } @Override public int getMetricsCategory() { return MetricsEvent.DIALOG_FINGERPRINT_ICON_TOUCH; } } public static class ErrorDialog extends InstrumentedDialogFragment { /** * Create a new instance of ErrorDialog. * * @param msg the string to show for message text * @param msgId the FingerprintManager error id so we know the cause * @return a new ErrorDialog */ static ErrorDialog newInstance(CharSequence msg, int msgId) { ErrorDialog dlg = new ErrorDialog(); Bundle args = new Bundle(); args.putCharSequence("error_msg", msg); args.putInt("error_id", msgId); dlg.setArguments(args); return dlg; } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); CharSequence errorString = getArguments().getCharSequence("error_msg"); final int errMsgId = getArguments().getInt("error_id"); builder.setTitle(R.string.security_settings_fingerprint_enroll_error_dialog_title) .setMessage(errorString) .setCancelable(false) .setPositiveButton(R.string.security_settings_fingerprint_enroll_dialog_ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); boolean wasTimeout = errMsgId == FingerprintManager.FINGERPRINT_ERROR_TIMEOUT; Activity activity = getActivity(); activity.setResult(wasTimeout ? RESULT_TIMEOUT : RESULT_FINISHED); activity.finish(); } }); AlertDialog dialog = builder.create(); dialog.setCanceledOnTouchOutside(false); return dialog; } @Override public int getMetricsCategory() { return MetricsEvent.DIALOG_FINGERPINT_ERROR; } } }