blob: 714e210f9f17393e789512524ef40fc862930a63 [file] [log] [blame]
/*
* 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;
}
}
}