blob: 4cd7283eed69feea6607c19fe0740bd319307213 [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 static android.text.Layout.HYPHENATION_FREQUENCY_NONE;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RawRes;
import android.app.Dialog;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.Rect;
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.os.Bundle;
import android.os.Process;
import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.text.TextUtils;
import android.util.FeatureFlagUtils;
import android.util.Log;
import android.view.DisplayInfo;
import android.view.MotionEvent;
import android.view.OrientationEventListener;
import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.IdRes;
import androidx.appcompat.app.AlertDialog;
import com.android.internal.annotations.VisibleForTesting;
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.android.settingslib.display.DisplayDensityUtils;
import com.android.settingslib.udfps.UdfpsOverlayParams;
import com.android.settingslib.udfps.UdfpsUtils;
import com.airbnb.lottie.LottieAnimationView;
import com.airbnb.lottie.LottieCompositionFactory;
import com.airbnb.lottie.LottieProperty;
import com.airbnb.lottie.model.KeyPath;
import com.google.android.setupcompat.template.FooterActionButton;
import com.google.android.setupcompat.template.FooterBarMixin;
import com.google.android.setupcompat.template.FooterButton;
import com.google.android.setupcompat.util.WizardManagerHelper;
import com.google.android.setupdesign.GlifLayout;
import com.google.android.setupdesign.template.DescriptionMixin;
import com.google.android.setupdesign.template.HeaderMixin;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
import java.util.Locale;
/**
* 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";
static final String TAG_UDFPS_HELPER = "udfps_helper";
static final String ICON_TOUCH_DIALOG = "fps_icon_touch_dialog";
static final String KEY_STATE_CANCELED = "is_canceled";
static final String KEY_STATE_PREVIOUS_ROTATION = "previous_rotation";
private static final int PROGRESS_BAR_MAX = 10000;
private static final int STAGE_UNKNOWN = -1;
private static final int STAGE_CENTER = 0;
private static final int STAGE_GUIDED = 1;
private static final int STAGE_FINGERTIP = 2;
private static final int STAGE_LEFT_EDGE = 3;
private static final int STAGE_RIGHT_EDGE = 4;
@VisibleForTesting
protected static final int SFPS_STAGE_NO_ANIMATION = 0;
@VisibleForTesting
protected static final int SFPS_STAGE_CENTER = 1;
@VisibleForTesting
protected static final int SFPS_STAGE_FINGERTIP = 2;
@VisibleForTesting
protected static final int SFPS_STAGE_LEFT_EDGE = 3;
@VisibleForTesting
protected static final int SFPS_STAGE_RIGHT_EDGE = 4;
@IntDef({STAGE_UNKNOWN, STAGE_CENTER, STAGE_GUIDED, STAGE_FINGERTIP, STAGE_LEFT_EDGE,
STAGE_RIGHT_EDGE})
@Retention(RetentionPolicy.SOURCE)
private @interface EnrollStage {}
@VisibleForTesting
@IntDef({STAGE_UNKNOWN, SFPS_STAGE_NO_ANIMATION, SFPS_STAGE_CENTER, SFPS_STAGE_FINGERTIP,
SFPS_STAGE_LEFT_EDGE, SFPS_STAGE_RIGHT_EDGE})
@Retention(RetentionPolicy.SOURCE)
protected @interface SfpsEnrollStage {}
/**
* 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 VibrationAttributes FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES =
VibrationAttributes.createForUsage(VibrationAttributes.USAGE_ACCESSIBILITY);
private FingerprintManager mFingerprintManager;
private boolean mCanAssumeUdfps;
private boolean mCanAssumeSfps;
@Nullable private ProgressBar mProgressBar;
@VisibleForTesting
@Nullable
UdfpsEnrollHelper mUdfpsEnrollHelper;
// TODO(b/260617060): Do not hard-code mScaleFactor, referring to AuthController.
private float mScaleFactor = 1.0f;
private ObjectAnimator mProgressAnim;
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;
@VisibleForTesting
boolean mIsCanceled;
private AccessibilityManager mAccessibilityManager;
private boolean mIsAccessibilityEnabled;
private LottieAnimationView mIllustrationLottie;
private boolean mHaveShownUdfpsTipLottie;
private boolean mHaveShownUdfpsLeftEdgeLottie;
private boolean mHaveShownUdfpsRightEdgeLottie;
private boolean mHaveShownUdfpsCenterLottie;
private boolean mHaveShownUdfpsGuideLottie;
private boolean mHaveShownSfpsNoAnimationLottie;
private boolean mHaveShownSfpsCenterLottie;
private boolean mHaveShownSfpsTipLottie;
private boolean mHaveShownSfpsLeftEdgeLottie;
private boolean mHaveShownSfpsRightEdgeLottie;
private boolean mShouldShowLottie;
private UdfpsUtils mUdfpsUtils;
private ObjectAnimator mHelpAnimation;
private OrientationEventListener mOrientationEventListener;
private int mPreviousRotation = 0;
@VisibleForTesting
protected boolean shouldShowLottie() {
DisplayDensityUtils displayDensity = new DisplayDensityUtils(getApplicationContext());
int currentDensityIndex = displayDensity.getCurrentIndexForDefaultDisplay();
final int currentDensity = displayDensity.getDefaultDisplayDensityValues()
[currentDensityIndex];
final int defaultDensity = displayDensity.getDefaultDensityForDefaultDisplay();
return defaultDensity == currentDensity;
}
@Override
protected void onApplyThemeResource(Resources.Theme theme, int resid, boolean first) {
theme.applyStyle(R.style.SetupWizardPartnerResource, true);
super.onApplyThemeResource(theme, resid, first);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (isInMultiWindowMode()) {
final Toast splitUnsupportedToast = Toast.makeText(this,
R.string.dock_multi_instances_not_supported_text, Toast.LENGTH_SHORT);
splitUnsupportedToast.show();
finish();
return;
}
if (savedInstanceState != null) {
restoreSavedState(savedInstanceState);
}
mFingerprintManager = getSystemService(FingerprintManager.class);
final List<FingerprintSensorPropertiesInternal> props =
mFingerprintManager.getSensorPropertiesInternal();
mCanAssumeUdfps = props != null && props.size() == 1 && props.get(0).isAnyUdfpsType();
mCanAssumeSfps = props != null && props.size() == 1 && props.get(0).isAnySidefpsType();
mAccessibilityManager = getSystemService(AccessibilityManager.class);
mIsAccessibilityEnabled = mAccessibilityManager.isEnabled();
mUdfpsUtils = new UdfpsUtils();
final boolean isLayoutRtl = (TextUtils.getLayoutDirectionFromLocale(
Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL);
listenOrientationEvent();
if (mCanAssumeUdfps) {
int rotation = getApplicationContext().getDisplay().getRotation();
final GlifLayout layout = (GlifLayout) getLayoutInflater().inflate(
R.layout.udfps_enroll_enrolling, null, false);
switch (rotation) {
case Surface.ROTATION_90:
final LinearLayout layoutContainer = layout.findViewById(
R.id.layout_container);
final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT);
lp.setMarginEnd((int) getResources().getDimension(
R.dimen.rotation_90_enroll_margin_end));
layoutContainer.setPaddingRelative((int) getResources().getDimension(
R.dimen.rotation_90_enroll_padding_start), 0, isLayoutRtl
? 0 : (int) getResources().getDimension(
R.dimen.rotation_90_enroll_padding_end), 0);
layoutContainer.setLayoutParams(lp);
if (FeatureFlagUtils.isEnabled(getApplicationContext(),
FeatureFlagUtils.SETTINGS_SHOW_UDFPS_ENROLL_IN_SETTINGS)) {
final UdfpsEnrollView udfpsEnrollView = addUdfpsEnrollView(props.get(0));
layout.addView(udfpsEnrollView);
setOnHoverListener(true, layout, udfpsEnrollView);
}
setContentView(layout, lp);
break;
case Surface.ROTATION_0:
case Surface.ROTATION_180:
if (FeatureFlagUtils.isEnabled(getApplicationContext(),
FeatureFlagUtils.SETTINGS_SHOW_UDFPS_ENROLL_IN_SETTINGS)) {
final UdfpsEnrollView udfpsEnrollView = addUdfpsEnrollView(props.get(0));
// In the portrait mode, set layout_container's height 0, so it's
// always shown at the bottom of the screen.
// Add udfps enroll view into layout_container instead of
// udfps_enroll_enrolling, so that when the content is too long to
// make udfps_enroll_enrolling larger than the screen, udfps enroll
// view could still be set to right position by setting bottom margin to
// its parent view (layout_container) because it's always at the
// bottom of the screen.
final FrameLayout portraitLayoutContainer = layout.findViewById(
R.id.layout_container);
final ViewGroup.LayoutParams containerLp =
portraitLayoutContainer.getLayoutParams();
containerLp.height = 0;
// In the portrait mode, the title and lottie animation view may
// overlap when title needs three lines, so adding some paddings
// between them, and adjusting the fp progress view here accordingly.
final int layoutLottieAnimationPadding = (int) getResources()
.getDimension(R.dimen.udfps_lottie_padding_top);
portraitLayoutContainer.setPadding(0,
layoutLottieAnimationPadding, 0, 0);
final ImageView progressView = udfpsEnrollView.findViewById(
R.id.udfps_enroll_animation_fp_progress_view);
progressView.setPadding(0, -(layoutLottieAnimationPadding),
0, layoutLottieAnimationPadding);
final ImageView fingerprintView = udfpsEnrollView.findViewById(
R.id.udfps_enroll_animation_fp_view);
fingerprintView.setPadding(0, -layoutLottieAnimationPadding,
0, layoutLottieAnimationPadding);
portraitLayoutContainer.addView(udfpsEnrollView);
setOnHoverListener(false, layout, udfpsEnrollView);
}
setContentView(layout);
break;
case Surface.ROTATION_270:
default:
if (FeatureFlagUtils.isEnabled(getApplicationContext(),
FeatureFlagUtils.SETTINGS_SHOW_UDFPS_ENROLL_IN_SETTINGS)) {
final UdfpsEnrollView udfpsEnrollView = addUdfpsEnrollView(props.get(0));
layout.addView(udfpsEnrollView);
setOnHoverListener(true, layout, udfpsEnrollView);
}
setContentView(layout);
break;
}
setDescriptionText(R.string.security_settings_udfps_enroll_start_message);
} else if (mCanAssumeSfps) {
setContentView(R.layout.sfps_enroll_enrolling);
setHelpAnimation();
} else {
setContentView(R.layout.fingerprint_enroll_enrolling);
setDescriptionText(R.string.security_settings_fingerprint_enroll_start_message);
}
mIsSetupWizard = WizardManagerHelper.isAnySetupWizard(getIntent());
if (mCanAssumeUdfps || mCanAssumeSfps) {
updateTitleAndDescription();
} else {
setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title);
}
mShouldShowLottie = shouldShowLottie();
// On non-SFPS devices, only show the lottie if the current display density is the default
// density. Otherwise, the lottie will overlap with the settings header text.
boolean isLandscape = BiometricUtils.isReverseLandscape(getApplicationContext())
|| BiometricUtils.isLandscape(getApplicationContext());
updateOrientation((isLandscape
? Configuration.ORIENTATION_LANDSCAPE : Configuration.ORIENTATION_PORTRAIT));
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()
);
if (FeatureFlagUtils.isEnabled(getApplicationContext(),
FeatureFlagUtils.SETTINGS_SHOW_UDFPS_ENROLL_IN_SETTINGS)) {
// Remove the space view and make the width of footer button container WRAP_CONTENT
// to avoid hiding the udfps view progress bar bottom.
final LinearLayout buttonContainer = mFooterBarMixin.getButtonContainer();
View spaceView = null;
for (int i = 0; i < buttonContainer.getChildCount(); i++) {
if (!(buttonContainer.getChildAt(i) instanceof FooterActionButton)) {
spaceView = buttonContainer.getChildAt(i);
break;
}
}
if (spaceView != null) {
spaceView.setVisibility(View.GONE);
buttonContainer.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
}
}
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.setProgressBackgroundTintMode(PorterDuff.Mode.SRC);
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;
});
}
final Configuration config = getApplicationContext().getResources().getConfiguration();
maybeHideSfpsText(config);
}
private void setHelpAnimation() {
final float translationX = 40;
final int duration = 550;
final RelativeLayout progressLottieLayout = findViewById(R.id.progress_lottie);
mHelpAnimation = ObjectAnimator.ofFloat(progressLottieLayout,
"translationX" /* propertyName */,
0, translationX, -1 * translationX, translationX, 0f);
mHelpAnimation.setInterpolator(new AccelerateDecelerateInterpolator());
mHelpAnimation.setDuration(duration);
mHelpAnimation.setAutoCancel(false);
}
@Override
protected BiometricEnrollSidecar getSidecar() {
final FingerprintEnrollSidecar sidecar = new FingerprintEnrollSidecar(this,
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 && !mIsCanceled;
}
return true;
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(KEY_STATE_CANCELED, mIsCanceled);
outState.putInt(KEY_STATE_PREVIOUS_ROTATION, mPreviousRotation);
}
private void restoreSavedState(Bundle savedInstanceState) {
mRestoring = true;
mIsCanceled = savedInstanceState.getBoolean(KEY_STATE_CANCELED, false);
mPreviousRotation = savedInstanceState.getInt(KEY_STATE_PREVIOUS_ROTATION,
getDisplay().getRotation());
}
@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();
}
}
@VisibleForTesting
void onCancelEnrollment(@IdRes int errorMsgId) {
// showErrorDialog() will cause onWindowFocusChanged(false), set mIsCanceled to false
// before showErrorDialog() to prevent that another error dialog is triggered again.
mIsCanceled = true;
FingerprintErrorDialog.showErrorDialog(this, errorMsgId, mCanAssumeUdfps);
cancelEnrollment();
stopIconAnimation();
stopListenOrientationEvent();
if (!mCanAssumeUdfps) {
mErrorText.removeCallbacks(mTouchAgainRunnable);
}
}
@Override
protected void onStop() {
if (!isChangingConfigurations()) {
if (!WizardManagerHelper.isAnySetupWizard(getIntent())
&& !BiometricUtils.isAnyMultiBiometricFlow(this)
&& !mFromSettingsSummary) {
setResult(RESULT_TIMEOUT);
}
finish();
}
stopIconAnimation();
super.onStop();
}
@Override
protected boolean shouldFinishWhenBackgrounded() {
// Prevent super.onStop() from finishing, since we handle this in our onStop().
return false;
}
@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, getFinishDelay());
}
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 (mCanAssumeUdfps) {
updateTitleAndDescriptionForUdfps();
return;
} else if (mCanAssumeSfps) {
updateTitleAndDescriptionForSfps();
return;
}
if (mSidecar == null || mSidecar.getEnrollmentSteps() == -1) {
setDescriptionText(R.string.security_settings_fingerprint_enroll_start_message);
} else {
setDescriptionText(R.string.security_settings_fingerprint_enroll_repeat_message);
}
}
private void updateTitleAndDescriptionForUdfps() {
switch (getCurrentStage()) {
case STAGE_CENTER:
setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title);
if (mIsAccessibilityEnabled || mIllustrationLottie == null) {
setDescriptionText(R.string.security_settings_udfps_enroll_start_message);
} else if (!mHaveShownUdfpsCenterLottie && mIllustrationLottie != null) {
mHaveShownUdfpsCenterLottie = true;
// Note: Update string reference when differentiate in between udfps & sfps
mIllustrationLottie.setContentDescription(
getString(R.string.security_settings_sfps_enroll_finger_center_title)
);
configureEnrollmentStage(R.raw.udfps_center_hint_lottie);
}
break;
case STAGE_GUIDED:
setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title);
if (mIsAccessibilityEnabled || mIllustrationLottie == null) {
setDescriptionText(R.string.security_settings_udfps_enroll_repeat_a11y_message);
} else if (!mHaveShownUdfpsGuideLottie && mIllustrationLottie != null) {
mHaveShownUdfpsGuideLottie = true;
mIllustrationLottie.setContentDescription(
getString(R.string.security_settings_fingerprint_enroll_repeat_message)
);
// TODO(b/228100413) Could customize guided lottie animation
configureEnrollmentStage(R.raw.udfps_center_hint_lottie);
}
break;
case STAGE_FINGERTIP:
setHeaderText(R.string.security_settings_udfps_enroll_fingertip_title);
if (!mHaveShownUdfpsTipLottie && mIllustrationLottie != null) {
mHaveShownUdfpsTipLottie = true;
mIllustrationLottie.setContentDescription(
getString(R.string.security_settings_udfps_tip_fingerprint_help)
);
configureEnrollmentStage(R.raw.udfps_tip_hint_lottie);
}
break;
case STAGE_LEFT_EDGE:
setHeaderText(R.string.security_settings_udfps_enroll_left_edge_title);
if (!mHaveShownUdfpsLeftEdgeLottie && mIllustrationLottie != null) {
mHaveShownUdfpsLeftEdgeLottie = true;
mIllustrationLottie.setContentDescription(
getString(R.string.security_settings_udfps_side_fingerprint_help)
);
configureEnrollmentStage(R.raw.udfps_left_edge_hint_lottie);
} else if (mIllustrationLottie == null) {
if (isStageHalfCompleted()) {
setDescriptionText(
R.string.security_settings_fingerprint_enroll_repeat_message);
} else {
setDescriptionText(R.string.security_settings_udfps_enroll_edge_message);
}
}
break;
case STAGE_RIGHT_EDGE:
setHeaderText(R.string.security_settings_udfps_enroll_right_edge_title);
if (!mHaveShownUdfpsRightEdgeLottie && mIllustrationLottie != null) {
mHaveShownUdfpsRightEdgeLottie = true;
mIllustrationLottie.setContentDescription(
getString(R.string.security_settings_udfps_side_fingerprint_help)
);
configureEnrollmentStage(R.raw.udfps_right_edge_hint_lottie);
} else if (mIllustrationLottie == null) {
if (isStageHalfCompleted()) {
setDescriptionText(
R.string.security_settings_fingerprint_enroll_repeat_message);
} else {
setDescriptionText(R.string.security_settings_udfps_enroll_edge_message);
}
}
break;
case STAGE_UNKNOWN:
default:
// 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);
break;
}
}
// Interrupt any existing talkback speech to prevent stacking talkback messages
private void clearTalkback() {
AccessibilityManager.getInstance(getApplicationContext()).interrupt();
}
private void updateTitleAndDescriptionForSfps() {
if (mIsAccessibilityEnabled) {
clearTalkback();
getLayout().getDescriptionTextView().setAccessibilityLiveRegion(
View.ACCESSIBILITY_LIVE_REGION_POLITE);
}
switch (getCurrentSfpsStage()) {
case SFPS_STAGE_NO_ANIMATION:
setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title);
if (!mHaveShownSfpsNoAnimationLottie && mIllustrationLottie != null) {
mHaveShownSfpsNoAnimationLottie = true;
mIllustrationLottie.setContentDescription(
getString(
R.string.security_settings_sfps_animation_a11y_label,
0
)
);
configureEnrollmentStage(R.raw.sfps_lottie_no_animation);
}
break;
case SFPS_STAGE_CENTER:
setHeaderText(R.string.security_settings_sfps_enroll_finger_center_title);
if (!mHaveShownSfpsCenterLottie && mIllustrationLottie != null) {
mHaveShownSfpsCenterLottie = true;
configureEnrollmentStage(R.raw.sfps_lottie_pad_center);
}
break;
case SFPS_STAGE_FINGERTIP:
setHeaderText(R.string.security_settings_sfps_enroll_fingertip_title);
if (!mHaveShownSfpsTipLottie && mIllustrationLottie != null) {
mHaveShownSfpsTipLottie = true;
configureEnrollmentStage(R.raw.sfps_lottie_tip);
}
break;
case SFPS_STAGE_LEFT_EDGE:
setHeaderText(R.string.security_settings_sfps_enroll_left_edge_title);
if (!mHaveShownSfpsLeftEdgeLottie && mIllustrationLottie != null) {
mHaveShownSfpsLeftEdgeLottie = true;
configureEnrollmentStage(R.raw.sfps_lottie_left_edge);
}
break;
case SFPS_STAGE_RIGHT_EDGE:
setHeaderText(R.string.security_settings_sfps_enroll_right_edge_title);
if (!mHaveShownSfpsRightEdgeLottie && mIllustrationLottie != null) {
mHaveShownSfpsRightEdgeLottie = true;
configureEnrollmentStage(R.raw.sfps_lottie_right_edge);
}
break;
case STAGE_UNKNOWN:
default:
// Don't use BiometricEnrollBase#setHeaderText, since that invokes setTitle,
// which gets announced for a11y upon entering the page. For SFPS, we want to
// announce a different string for a11y upon entering the page.
getLayout().setHeaderText(
R.string.security_settings_sfps_enroll_find_sensor_title);
final CharSequence description = getString(
R.string.security_settings_sfps_enroll_find_sensor_message);
getLayout().getHeaderTextView().setContentDescription(description);
setTitle(description);
break;
}
}
@VisibleForTesting void configureEnrollmentStage(@RawRes int lottie) {
if (!mCanAssumeSfps) {
setDescriptionText("");
}
LottieCompositionFactory.fromRawRes(this, lottie)
.addListener((c) -> {
mIllustrationLottie.setComposition(c);
mIllustrationLottie.setVisibility(View.VISIBLE);
mIllustrationLottie.playAnimation();
});
}
@EnrollStage
private int getCurrentStage() {
if (mSidecar == null || mSidecar.getEnrollmentSteps() == -1) {
return STAGE_UNKNOWN;
}
final int progressSteps = mSidecar.getEnrollmentSteps() - mSidecar.getEnrollmentRemaining();
if (progressSteps < getStageThresholdSteps(0)) {
return STAGE_CENTER;
} else if (progressSteps < getStageThresholdSteps(1)) {
return STAGE_GUIDED;
} else if (progressSteps < getStageThresholdSteps(2)) {
return STAGE_FINGERTIP;
} else if (progressSteps < getStageThresholdSteps(3)) {
return STAGE_LEFT_EDGE;
} else {
return STAGE_RIGHT_EDGE;
}
}
@SfpsEnrollStage
private int getCurrentSfpsStage() {
if (mSidecar == null) {
return STAGE_UNKNOWN;
}
final int progressSteps = mSidecar.getEnrollmentSteps() - mSidecar.getEnrollmentRemaining();
if (progressSteps < getStageThresholdSteps(0)) {
return SFPS_STAGE_NO_ANIMATION;
} else if (progressSteps < getStageThresholdSteps(1)) {
return SFPS_STAGE_CENTER;
} else if (progressSteps < getStageThresholdSteps(2)) {
return SFPS_STAGE_FINGERTIP;
} else if (progressSteps < getStageThresholdSteps(3)) {
return SFPS_STAGE_LEFT_EDGE;
} else {
return SFPS_STAGE_RIGHT_EDGE;
}
}
private boolean isStageHalfCompleted() {
// Prior to first enrollment step.
if (mSidecar == null || mSidecar.getEnrollmentSteps() == -1) {
return false;
}
final int progressSteps = mSidecar.getEnrollmentSteps() - mSidecar.getEnrollmentRemaining();
int prevThresholdSteps = 0;
for (int i = 0; i < mFingerprintManager.getEnrollStageCount(); i++) {
final int thresholdSteps = getStageThresholdSteps(i);
if (progressSteps >= prevThresholdSteps && progressSteps < thresholdSteps) {
final int adjustedProgress = progressSteps - prevThresholdSteps;
final int adjustedThreshold = thresholdSteps - prevThresholdSteps;
return adjustedProgress >= adjustedThreshold / 2;
}
prevThresholdSteps = thresholdSteps;
}
// After last enrollment step.
return true;
}
@VisibleForTesting
protected int getStageThresholdSteps(int index) {
if (mSidecar == null || mSidecar.getEnrollmentSteps() == -1) {
Log.w(TAG, "getStageThresholdSteps: Enrollment not started yet");
return 1;
}
return Math.round(mSidecar.getEnrollmentSteps()
* mFingerprintManager.getEnrollStageThreshold(index));
}
@Override
public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
if (!TextUtils.isEmpty(helpString)) {
if (!(mCanAssumeUdfps || mCanAssumeSfps)) {
mErrorText.removeCallbacks(mTouchAgainRunnable);
}
showError(helpString);
if (mUdfpsEnrollHelper != null) mUdfpsEnrollHelper.onEnrollmentHelp();
}
dismissTouchDialogIfSfps();
}
@Override
public void onEnrollmentError(int errMsgId, CharSequence errString) {
onCancelEnrollment(errMsgId);
dismissTouchDialogIfSfps();
}
private void announceEnrollmentProgress(CharSequence announcement) {
AccessibilityEvent e = AccessibilityEvent.obtain();
e.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT);
e.setClassName(getClass().getName());
e.setPackageName(getPackageName());
e.getText().add(announcement);
mAccessibilityManager.sendAccessibilityEvent(e);
}
@Override
public void onEnrollmentProgressChange(int steps, int remaining) {
updateProgress(true /* animate */);
final int percent = (int) (((float) (steps - remaining) / (float) steps) * 100);
if (mCanAssumeSfps && mIsAccessibilityEnabled) {
CharSequence announcement = getString(
R.string.security_settings_sfps_enroll_progress_a11y_message, percent);
announceEnrollmentProgress(announcement);
if (mIllustrationLottie != null) {
mIllustrationLottie.setContentDescription(
getString(
R.string.security_settings_sfps_animation_a11y_label,
percent)
);
}
}
updateTitleAndDescription();
animateFlash();
if (mCanAssumeUdfps) {
if (mIsAccessibilityEnabled) {
CharSequence announcement = getString(
R.string.security_settings_udfps_enroll_progress_a11y_message, percent);
announceEnrollmentProgress(announcement);
}
} else if (!mCanAssumeSfps) {
mErrorText.removeCallbacks(mTouchAgainRunnable);
mErrorText.postDelayed(mTouchAgainRunnable, HINT_TIMEOUT_DURATION);
}
dismissTouchDialogIfSfps();
}
private void dismissTouchDialogIfSfps() {
if (!mCanAssumeSfps) {
return;
}
final IconTouchDialog dialog = (IconTouchDialog)
getSupportFragmentManager().findFragmentByTag(ICON_TOUCH_DIALOG);
if (dialog != null && dialog.isResumed()) {
dialog.dismiss();
}
}
@Override
public void onAcquired(boolean isAcquiredGood) {
if (mUdfpsEnrollHelper != null) {
mUdfpsEnrollHelper.onAcquired(isAcquiredGood);
}
}
@Override
public void onPointerDown(int sensorId) {
if (mUdfpsEnrollHelper != null) {
mUdfpsEnrollHelper.onPointerDown(sensorId);
}
}
@Override
public void onPointerUp(int sensorId) {
if (mUdfpsEnrollHelper != null) {
mUdfpsEnrollHelper.onPointerUp(sensorId);
}
}
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());
// Only clear the error when progress has been made.
// TODO (b/234772728) Add tests.
if (mProgressBar != null && mProgressBar.getProgress() < progress) {
clearError();
}
if (mUdfpsEnrollHelper != null) {
mUdfpsEnrollHelper.onEnrollmentProgress(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(), ICON_TOUCH_DIALOG);
}
private void showError(CharSequence error) {
if (mCanAssumeSfps) {
setHeaderText(error);
if (!mHelpAnimation.isRunning()) {
mHelpAnimation.start();
}
applySfpsErrorDynamicColors(getApplicationContext(), true);
} else 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() && mIsAccessibilityEnabled && !mCanAssumeUdfps) {
mVibrator.vibrate(Process.myUid(), getApplicationContext().getOpPackageName(),
VIBRATE_EFFECT_ERROR, getClass().getSimpleName() + "::showError",
FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES);
}
}
private void clearError() {
if (mCanAssumeSfps) {
applySfpsErrorDynamicColors(getApplicationContext(), false);
}
if ((!(mCanAssumeUdfps || mCanAssumeSfps)) && 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();
}
}
/**
* Applies dynamic colors corresponding to showing or clearing errors on the progress bar
* and finger lottie for SFPS
*/
private void applySfpsErrorDynamicColors(Context context, boolean isError) {
applyProgressBarDynamicColor(context, isError);
if (mIllustrationLottie != null) {
applyLottieDynamicColor(context, isError);
}
}
private void applyProgressBarDynamicColor(Context context, boolean isError) {
if (mProgressBar != null) {
int error_color = context.getColor(R.color.sfps_enrollment_progress_bar_error_color);
int progress_bar_fill_color = context.getColor(
R.color.sfps_enrollment_progress_bar_fill_color);
ColorStateList fillColor = ColorStateList.valueOf(
isError ? error_color : progress_bar_fill_color);
mProgressBar.setProgressTintList(fillColor);
mProgressBar.setProgressTintMode(PorterDuff.Mode.SRC);
mProgressBar.invalidate();
}
}
private void applyLottieDynamicColor(Context context, boolean isError) {
int error_color = context.getColor(R.color.sfps_enrollment_fp_error_color);
int fp_captured_color = context.getColor(R.color.sfps_enrollment_fp_captured_color);
int color = isError ? error_color : fp_captured_color;
mIllustrationLottie.addValueCallback(
new KeyPath(".blue100", "**"),
LottieProperty.COLOR_FILTER,
frameInfo -> new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)
);
mIllustrationLottie.invalidate();
}
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) {
startIconAnimation();
}
@Override
public void onAnimationRepeat(Animator animation) { }
@Override
public void onAnimationEnd(Animator animation) {
stopIconAnimation();
if (mProgressBar.getProgress() >= PROGRESS_BAR_MAX) {
mProgressBar.postDelayed(mDelayedFinishRunnable, getFinishDelay());
}
}
@Override
public void onAnimationCancel(Animator animation) { }
};
private long getFinishDelay() {
return mCanAssumeUdfps ? 400L : 250L;
}
// 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;
}
private void updateOrientation(int orientation) {
if (mCanAssumeSfps) {
mIllustrationLottie = findViewById(R.id.illustration_lottie);
} else {
switch(orientation) {
case Configuration.ORIENTATION_LANDSCAPE: {
mIllustrationLottie = null;
break;
}
case Configuration.ORIENTATION_PORTRAIT: {
if (mShouldShowLottie) {
mIllustrationLottie = findViewById(R.id.illustration_lottie);
}
break;
}
default:
Log.e(TAG, "Error unhandled configuration change");
break;
}
}
}
@SuppressWarnings("MissingSuperCall") // TODO: Fix me
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
maybeHideSfpsText(newConfig);
switch(newConfig.orientation) {
case Configuration.ORIENTATION_LANDSCAPE: {
updateOrientation(Configuration.ORIENTATION_LANDSCAPE);
break;
}
case Configuration.ORIENTATION_PORTRAIT: {
updateOrientation(Configuration.ORIENTATION_PORTRAIT);
break;
}
default:
Log.e(TAG, "Error unhandled configuration change");
break;
}
}
private void maybeHideSfpsText(@NonNull Configuration newConfig) {
final HeaderMixin headerMixin = getLayout().getMixin(HeaderMixin.class);
final DescriptionMixin descriptionMixin = getLayout().getMixin(DescriptionMixin.class);
final boolean isLandscape = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE;
if (mCanAssumeSfps) {
// hide the description
descriptionMixin.getTextView().setVisibility(View.GONE);
headerMixin.getTextView().setHyphenationFrequency(HYPHENATION_FREQUENCY_NONE);
if (isLandscape) {
headerMixin.setAutoTextSizeEnabled(true);
headerMixin.getTextView().setMinLines(0);
headerMixin.getTextView().setMaxLines(10);
} else {
headerMixin.setAutoTextSizeEnabled(false);
headerMixin.getTextView().setLines(4);
}
}
}
private UdfpsEnrollView addUdfpsEnrollView(FingerprintSensorPropertiesInternal udfpsProps) {
UdfpsEnrollView udfpsEnrollView = (UdfpsEnrollView) getLayoutInflater().inflate(
R.layout.udfps_enroll_view, null, false);
DisplayInfo displayInfo = new DisplayInfo();
getDisplay().getDisplayInfo(displayInfo);
mScaleFactor = mUdfpsUtils.getScaleFactor(displayInfo);
Rect udfpsBounds = udfpsProps.getLocation().getRect();
udfpsBounds.scale(mScaleFactor);
final Rect overlayBounds = new Rect(
0, /* left */
displayInfo.getNaturalHeight() / 2, /* top */
displayInfo.getNaturalWidth(), /* right */
displayInfo.getNaturalHeight() /* botom */);
UdfpsOverlayParams params = new UdfpsOverlayParams(
udfpsBounds,
overlayBounds,
displayInfo.getNaturalWidth(),
displayInfo.getNaturalHeight(),
mScaleFactor,
displayInfo.rotation);
udfpsEnrollView.setOverlayParams(params);
mUdfpsEnrollHelper = (UdfpsEnrollHelper) getSupportFragmentManager().findFragmentByTag(
FingerprintEnrollEnrolling.TAG_UDFPS_HELPER);
if (mUdfpsEnrollHelper == null) {
mUdfpsEnrollHelper = new UdfpsEnrollHelper(getApplicationContext(),
mFingerprintManager);
getSupportFragmentManager().beginTransaction()
.add(mUdfpsEnrollHelper, FingerprintEnrollEnrolling.TAG_UDFPS_HELPER)
.commitAllowingStateLoss();
}
udfpsEnrollView.setEnrollHelper(mUdfpsEnrollHelper);
return udfpsEnrollView;
}
private void setOnHoverListener(boolean isLandscape, GlifLayout enrollLayout,
UdfpsEnrollView udfpsEnrollView) {
if (!mIsAccessibilityEnabled) return;
final Context context = getApplicationContext();
final View.OnHoverListener onHoverListener = (v, event) -> {
// Map the touch to portrait mode if the device is in
// landscape mode.
final Point scaledTouch =
mUdfpsUtils.getTouchInNativeCoordinates(event.getPointerId(0),
event, udfpsEnrollView.getOverlayParams());
if (mUdfpsUtils.isWithinSensorArea(event.getPointerId(0), event,
udfpsEnrollView.getOverlayParams())) {
return false;
}
final String theStr = mUdfpsUtils.onTouchOutsideOfSensorArea(
mAccessibilityManager.isTouchExplorationEnabled(), context,
scaledTouch.x, scaledTouch.y, udfpsEnrollView.getOverlayParams());
if (theStr != null) {
v.announceForAccessibility(theStr);
}
return false;
};
enrollLayout.findManagedViewById(isLandscape ? R.id.sud_landscape_content_area
: R.id.sud_layout_content).setOnHoverListener(onHoverListener);
}
public static class IconTouchDialog extends InstrumentedDialogFragment {
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(),
R.style.Theme_AlertDialog);
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;
}
}
}