| /* |
| * 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.biometrics.fingerprint; |
| |
| import android.animation.Animator; |
| import android.animation.ObjectAnimator; |
| import android.annotation.Nullable; |
| import android.app.Dialog; |
| import android.app.settings.SettingsEnums; |
| 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.hardware.fingerprint.FingerprintSensorPropertiesInternal; |
| import android.media.AudioAttributes; |
| import android.os.Bundle; |
| import android.os.VibrationEffect; |
| import android.os.Vibrator; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.view.MotionEvent; |
| import android.view.OrientationEventListener; |
| import android.view.Surface; |
| import android.view.View; |
| import android.view.accessibility.AccessibilityEvent; |
| import android.view.accessibility.AccessibilityManager; |
| import android.view.animation.AnimationUtils; |
| import android.view.animation.Interpolator; |
| import android.widget.ProgressBar; |
| import android.widget.TextView; |
| |
| import androidx.appcompat.app.AlertDialog; |
| |
| import com.android.settings.R; |
| import com.android.settings.biometrics.BiometricEnrollSidecar; |
| import com.android.settings.biometrics.BiometricUtils; |
| import com.android.settings.biometrics.BiometricsEnrollEnrolling; |
| import com.android.settings.core.instrumentation.InstrumentedDialogFragment; |
| |
| import com.google.android.setupcompat.template.FooterBarMixin; |
| import com.google.android.setupcompat.template.FooterButton; |
| import com.google.android.setupcompat.util.WizardManagerHelper; |
| |
| import java.util.List; |
| |
| /** |
| * Activity which handles the actual enrolling for fingerprint. |
| */ |
| public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling { |
| |
| private static final String TAG = "FingerprintEnrollEnrolling"; |
| static final String TAG_SIDECAR = "sidecar"; |
| |
| private static final int PROGRESS_BAR_MAX = 10000; |
| private static final int FINISH_DELAY = 250; |
| /** |
| * Enroll with two center touches before going to guided enrollment. |
| */ |
| private static final int NUM_CENTER_TOUCHES = 2; |
| |
| /** |
| * If we don't see progress during this time, we show an error message to remind the users that |
| * they need 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 boolean mCanAssumeUdfps; |
| @Nullable private ProgressBar mProgressBar; |
| private ObjectAnimator mProgressAnim; |
| private TextView mDescriptionText; |
| private TextView mErrorText; |
| private Interpolator mFastOutSlowInInterpolator; |
| private Interpolator mLinearOutSlowInInterpolator; |
| private Interpolator mFastOutLinearInInterpolator; |
| private int mIconTouchCount; |
| private boolean mAnimationCancelled; |
| @Nullable private AnimatedVectorDrawable mIconAnimationDrawable; |
| @Nullable private AnimatedVectorDrawable mIconBackgroundBlinksDrawable; |
| private boolean mRestoring; |
| private Vibrator mVibrator; |
| private boolean mIsSetupWizard; |
| private AccessibilityManager mAccessibilityManager; |
| private boolean mIsAccessibilityEnabled; |
| |
| private OrientationEventListener mOrientationEventListener; |
| private int mPreviousRotation = 0; |
| |
| @Override |
| protected void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| |
| final FingerprintManager fingerprintManager = getSystemService(FingerprintManager.class); |
| final List<FingerprintSensorPropertiesInternal> props = |
| fingerprintManager.getSensorPropertiesInternal(); |
| mCanAssumeUdfps = props.size() == 1 && props.get(0).isAnyUdfpsType(); |
| |
| mAccessibilityManager = getSystemService(AccessibilityManager.class); |
| mIsAccessibilityEnabled = mAccessibilityManager.isEnabled(); |
| |
| listenOrientationEvent(); |
| |
| if (mCanAssumeUdfps) { |
| if (BiometricUtils.isReverseLandscape(getApplicationContext())) { |
| setContentView(R.layout.udfps_enroll_enrolling_land); |
| } else { |
| setContentView(R.layout.udfps_enroll_enrolling); |
| } |
| setDescriptionText(R.string.security_settings_udfps_enroll_start_message); |
| } else { |
| setContentView(R.layout.fingerprint_enroll_enrolling); |
| setDescriptionText(R.string.security_settings_fingerprint_enroll_start_message); |
| } |
| |
| mIsSetupWizard = WizardManagerHelper.isAnySetupWizard(getIntent()); |
| if (mCanAssumeUdfps) { |
| updateTitleAndDescription(); |
| } else { |
| setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title); |
| } |
| |
| mErrorText = findViewById(R.id.error_text); |
| mProgressBar = findViewById(R.id.fingerprint_progress_bar); |
| mVibrator = getSystemService(Vibrator.class); |
| |
| mFooterBarMixin = getLayout().getMixin(FooterBarMixin.class); |
| mFooterBarMixin.setSecondaryButton( |
| new FooterButton.Builder(this) |
| .setText(R.string.security_settings_fingerprint_enroll_enrolling_skip) |
| .setListener(this::onSkipButtonClick) |
| .setButtonType(FooterButton.ButtonType.SKIP) |
| .setTheme(R.style.SudGlifButton_Secondary) |
| .build() |
| ); |
| |
| final LayerDrawable fingerprintDrawable = mProgressBar != null |
| ? (LayerDrawable) mProgressBar.getBackground() : null; |
| if (fingerprintDrawable != null) { |
| 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); |
| if (mProgressBar != null) { |
| mProgressBar.setOnTouchListener((v, 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 BiometricEnrollSidecar getSidecar() { |
| final FingerprintEnrollSidecar sidecar = new FingerprintEnrollSidecar(); |
| sidecar.setEnrollReason(FingerprintManager.ENROLL_ENROLL); |
| return sidecar; |
| } |
| |
| @Override |
| protected boolean shouldStartAutomatically() { |
| if (mCanAssumeUdfps) { |
| // Continue enrollment if restoring (e.g. configuration changed). Otherwise, wait |
| // for the entry animation to complete before starting. |
| return mRestoring; |
| } |
| return true; |
| } |
| |
| @Override |
| protected void onStart() { |
| super.onStart(); |
| updateProgress(false /* animate */); |
| updateTitleAndDescription(); |
| if (mRestoring) { |
| startIconAnimation(); |
| } |
| } |
| |
| @Override |
| public void onEnterAnimationComplete() { |
| super.onEnterAnimationComplete(); |
| |
| if (mCanAssumeUdfps) { |
| startEnrollment(); |
| } |
| |
| mAnimationCancelled = false; |
| startIconAnimation(); |
| } |
| |
| private void startIconAnimation() { |
| if (mIconAnimationDrawable != null) { |
| mIconAnimationDrawable.start(); |
| } |
| } |
| |
| private void stopIconAnimation() { |
| mAnimationCancelled = true; |
| if (mIconAnimationDrawable != null) { |
| mIconAnimationDrawable.stop(); |
| } |
| } |
| |
| @Override |
| protected void onStop() { |
| super.onStop(); |
| stopIconAnimation(); |
| } |
| |
| @Override |
| protected void onDestroy() { |
| stopListenOrientationEvent(); |
| super.onDestroy(); |
| } |
| |
| private void animateProgress(int progress) { |
| if (mCanAssumeUdfps) { |
| // UDFPS animations are owned by SystemUI |
| if (progress >= PROGRESS_BAR_MAX) { |
| // Wait for any animations in SysUI to finish, then proceed to next page |
| getMainThreadHandler().postDelayed(mDelayedFinishRunnable, FINISH_DELAY); |
| } |
| return; |
| } |
| 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() { |
| if (mIconBackgroundBlinksDrawable != null) { |
| mIconBackgroundBlinksDrawable.start(); |
| } |
| } |
| |
| protected Intent getFinishIntent() { |
| return new Intent(this, FingerprintEnrollFinish.class); |
| } |
| |
| private void updateTitleAndDescription() { |
| if (mSidecar == null || mSidecar.getEnrollmentSteps() == -1) { |
| if (mCanAssumeUdfps) { |
| // setHeaderText(R.string.security_settings_fingerprint_enroll_udfps_title); |
| // Don't use BiometricEnrollBase#setHeaderText, since that invokes setTitle, |
| // which gets announced for a11y upon entering the page. For UDFPS, we want to |
| // announce a different string for a11y upon entering the page. |
| getLayout().setHeaderText( |
| R.string.security_settings_fingerprint_enroll_udfps_title); |
| setDescriptionText(R.string.security_settings_udfps_enroll_start_message); |
| |
| final CharSequence description = getString( |
| R.string.security_settings_udfps_enroll_a11y); |
| getLayout().getHeaderTextView().setContentDescription(description); |
| setTitle(description); |
| } else { |
| setDescriptionText(R.string.security_settings_fingerprint_enroll_start_message); |
| } |
| } else if (mCanAssumeUdfps && !isCenterEnrollmentComplete()) { |
| if (mIsSetupWizard) { |
| setHeaderText(R.string.security_settings_udfps_enroll_title_one_more_time); |
| } else { |
| setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title); |
| } |
| setDescriptionText(R.string.security_settings_udfps_enroll_start_message); |
| } else { |
| if (mCanAssumeUdfps) { |
| setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title); |
| if (mIsAccessibilityEnabled) { |
| setDescriptionText(R.string.security_settings_udfps_enroll_repeat_a11y_message); |
| } else { |
| setDescriptionText(R.string.security_settings_udfps_enroll_repeat_message); |
| } |
| } else { |
| setDescriptionText(R.string.security_settings_fingerprint_enroll_repeat_message); |
| } |
| } |
| } |
| |
| private boolean isCenterEnrollmentComplete() { |
| if (mSidecar == null || mSidecar.getEnrollmentSteps() == -1) { |
| return false; |
| } |
| final int stepsEnrolled = mSidecar.getEnrollmentSteps() - mSidecar.getEnrollmentRemaining(); |
| return stepsEnrolled >= NUM_CENTER_TOUCHES; |
| } |
| |
| @Override |
| public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) { |
| if (!TextUtils.isEmpty(helpString)) { |
| if (!mCanAssumeUdfps) { |
| mErrorText.removeCallbacks(mTouchAgainRunnable); |
| } |
| showError(helpString); |
| } |
| } |
| |
| @Override |
| public void onEnrollmentError(int errMsgId, CharSequence errString) { |
| FingerprintErrorDialog.showErrorDialog(this, errMsgId); |
| stopIconAnimation(); |
| if (!mCanAssumeUdfps) { |
| mErrorText.removeCallbacks(mTouchAgainRunnable); |
| } |
| } |
| |
| @Override |
| public void onEnrollmentProgressChange(int steps, int remaining) { |
| updateProgress(true /* animate */); |
| updateTitleAndDescription(); |
| clearError(); |
| animateFlash(); |
| if (!mCanAssumeUdfps) { |
| mErrorText.removeCallbacks(mTouchAgainRunnable); |
| mErrorText.postDelayed(mTouchAgainRunnable, HINT_TIMEOUT_DURATION); |
| } else { |
| if (mIsAccessibilityEnabled) { |
| final int percent = (int) (((float)(steps - remaining) / (float) steps) * 100); |
| CharSequence cs = getString( |
| R.string.security_settings_udfps_enroll_progress_a11y_message, percent); |
| AccessibilityEvent e = AccessibilityEvent.obtain(); |
| e.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT); |
| e.setClassName(getClass().getName()); |
| e.setPackageName(getPackageName()); |
| e.getText().add(cs); |
| mAccessibilityManager.sendAccessibilityEvent(e); |
| } |
| } |
| } |
| |
| private void updateProgress(boolean animate) { |
| if (mSidecar == null || !mSidecar.isEnrolling()) { |
| Log.d(TAG, "Enrollment not started yet"); |
| return; |
| } |
| |
| int progress = getProgress( |
| mSidecar.getEnrollmentSteps(), mSidecar.getEnrollmentRemaining()); |
| if (animate) { |
| animateProgress(progress); |
| } else { |
| if (mProgressBar != null) { |
| 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 showIconTouchDialog() { |
| mIconTouchCount = 0; |
| new IconTouchDialog().show(getSupportFragmentManager(), null /* tag */); |
| } |
| |
| private void showError(CharSequence error) { |
| if (mCanAssumeUdfps) { |
| setHeaderText(error); |
| // Show nothing for subtitle when getting an error message. |
| setDescriptionText(""); |
| } else { |
| 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 (!mCanAssumeUdfps && 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 void listenOrientationEvent() { |
| mOrientationEventListener = new OrientationEventListener(this) { |
| @Override |
| public void onOrientationChanged(int orientation) { |
| final int currentRotation = getDisplay().getRotation(); |
| if ((mPreviousRotation == Surface.ROTATION_90 |
| && currentRotation == Surface.ROTATION_270) || ( |
| mPreviousRotation == Surface.ROTATION_270 |
| && currentRotation == Surface.ROTATION_90)) { |
| mPreviousRotation = currentRotation; |
| recreate(); |
| } |
| } |
| }; |
| mOrientationEventListener.enable(); |
| mPreviousRotation = getDisplay().getRotation(); |
| } |
| |
| private void stopListenOrientationEvent() { |
| if (mOrientationEventListener != null) { |
| mOrientationEventListener.disable(); |
| } |
| mOrientationEventListener = null; |
| } |
| |
| 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 SettingsEnums.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 SettingsEnums.DIALOG_FINGERPRINT_ICON_TOUCH; |
| } |
| } |
| } |