Merge "[BiometricsV2] Refactor EnrollingSfpsFragment"
This commit is contained in:
committed by
Android (Google) Code Review
commit
dfe6fac149
@@ -1,599 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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.biometrics2.ui.view;
|
||||
|
||||
import static android.hardware.fingerprint.FingerprintManager.ENROLL_ENROLL;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.annotation.RawRes;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.AccelerateDecelerateInterpolator;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.view.animation.Interpolator;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.biometrics.fingerprint.FingerprintErrorDialog;
|
||||
import com.android.settings.biometrics2.ui.model.EnrollmentProgress;
|
||||
import com.android.settings.biometrics2.ui.model.EnrollmentStatusMessage;
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel;
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel;
|
||||
|
||||
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.FooterBarMixin;
|
||||
import com.google.android.setupcompat.template.FooterButton;
|
||||
import com.google.android.setupdesign.GlifLayout;
|
||||
import com.google.android.setupdesign.template.DescriptionMixin;
|
||||
import com.google.android.setupdesign.template.HeaderMixin;
|
||||
|
||||
/**
|
||||
* Fragment is used to handle enrolling process for sfps
|
||||
*/
|
||||
public class FingerprintEnrollEnrollingSfpsFragment extends Fragment {
|
||||
|
||||
private static final String TAG = FingerprintEnrollEnrollingSfpsFragment.class.getSimpleName();
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private static final int PROGRESS_BAR_MAX = 10000;
|
||||
private static final long ANIMATION_DURATION = 250L;
|
||||
private static final long ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN = 500;
|
||||
private static final int ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN = 3;
|
||||
|
||||
private static final int STAGE_UNKNOWN = -1;
|
||||
private static final int SFPS_STAGE_NO_ANIMATION = 0;
|
||||
private static final int SFPS_STAGE_CENTER = 1;
|
||||
private static final int SFPS_STAGE_FINGERTIP = 2;
|
||||
private static final int SFPS_STAGE_LEFT_EDGE = 3;
|
||||
private static final int SFPS_STAGE_RIGHT_EDGE = 4;
|
||||
|
||||
private FingerprintEnrollEnrollingViewModel mEnrollingViewModel;
|
||||
private FingerprintEnrollProgressViewModel mProgressViewModel;
|
||||
|
||||
private Interpolator mFastOutSlowInInterpolator;
|
||||
|
||||
private GlifLayout mView;
|
||||
private ProgressBar mProgressBar;
|
||||
private ObjectAnimator mProgressAnim;
|
||||
|
||||
private LottieAnimationView mIllustrationLottie;
|
||||
|
||||
private boolean mHaveShownSfpsNoAnimationLottie;
|
||||
private boolean mHaveShownSfpsCenterLottie;
|
||||
private boolean mHaveShownSfpsTipLottie;
|
||||
private boolean mHaveShownSfpsLeftEdgeLottie;
|
||||
private boolean mHaveShownSfpsRightEdgeLottie;
|
||||
private ObjectAnimator mHelpAnimation;
|
||||
private int mIconTouchCount;
|
||||
|
||||
private final View.OnClickListener mOnSkipClickListener = v -> {
|
||||
mEnrollingViewModel.setOnSkipPressed();
|
||||
cancelEnrollment();
|
||||
};
|
||||
|
||||
private final Observer<EnrollmentProgress> mProgressObserver = progress -> {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "mProgressObserver(" + progress + ")");
|
||||
}
|
||||
if (progress != null && progress.getSteps() >= 0) {
|
||||
onEnrollmentProgressChange(progress);
|
||||
}
|
||||
};
|
||||
|
||||
private final Observer<EnrollmentStatusMessage> mHelpMessageObserver = helpMessage -> {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "mHelpMessageObserver(" + helpMessage + ")");
|
||||
}
|
||||
if (helpMessage != null) {
|
||||
onEnrollmentHelp(helpMessage);
|
||||
}
|
||||
};
|
||||
|
||||
private final Observer<EnrollmentStatusMessage> mErrorMessageObserver = errorMessage -> {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "mErrorMessageObserver(" + errorMessage + ")");
|
||||
}
|
||||
if (errorMessage != null) {
|
||||
onEnrollmentError(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
final FragmentActivity activity = getActivity();
|
||||
final ViewModelProvider provider = new ViewModelProvider(activity);
|
||||
mEnrollingViewModel = provider.get(FingerprintEnrollEnrollingViewModel.class);
|
||||
mProgressViewModel = provider.get(FingerprintEnrollProgressViewModel.class);
|
||||
super.onAttach(context);
|
||||
requireActivity().getOnBackPressedDispatcher().addCallback(new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
setEnabled(false);
|
||||
mEnrollingViewModel.setOnBackPressed();
|
||||
cancelEnrollment();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
mView = initSfpsLayout(inflater, container);
|
||||
maybeHideSfpsText(getActivity().getResources().getConfiguration());
|
||||
return mView;
|
||||
}
|
||||
|
||||
private GlifLayout initSfpsLayout(LayoutInflater inflater, ViewGroup container) {
|
||||
final GlifLayout containView = (GlifLayout) inflater.inflate(R.layout.sfps_enroll_enrolling,
|
||||
container, false);
|
||||
final Activity activity = getActivity();
|
||||
|
||||
new GlifLayoutHelper(activity, containView).setDescriptionText(
|
||||
getString(R.string.security_settings_fingerprint_enroll_start_message));
|
||||
|
||||
// setHelpAnimation()
|
||||
final float translationX = 40;
|
||||
final int duration = 550;
|
||||
final RelativeLayout progressLottieLayout = containView.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);
|
||||
|
||||
mIllustrationLottie = containView.findViewById(R.id.illustration_lottie);
|
||||
|
||||
mProgressBar = containView.findViewById(R.id.fingerprint_progress_bar);
|
||||
final FooterBarMixin footerBarMixin = containView.getMixin(FooterBarMixin.class);
|
||||
footerBarMixin.setSecondaryButton(
|
||||
new FooterButton.Builder(activity)
|
||||
.setText(R.string.security_settings_fingerprint_enroll_enrolling_skip)
|
||||
.setListener(mOnSkipClickListener)
|
||||
.setButtonType(FooterButton.ButtonType.SKIP)
|
||||
.setTheme(R.style.SudGlifButton_Secondary)
|
||||
.build()
|
||||
);
|
||||
|
||||
mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(
|
||||
activity, android.R.interpolator.fast_out_slow_in);
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
return containView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
startEnrollment();
|
||||
updateProgress(false /* animate */, mProgressViewModel.getProgressLiveData().getValue());
|
||||
final EnrollmentStatusMessage msg = mProgressViewModel.getHelpMessageLiveData().getValue();
|
||||
if (msg != null) {
|
||||
onEnrollmentHelp(msg);
|
||||
} else {
|
||||
clearError();
|
||||
updateTitleAndDescription();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
removeEnrollmentObservers();
|
||||
if (!getActivity().isChangingConfigurations() && mProgressViewModel.isEnrolling()) {
|
||||
mProgressViewModel.cancelEnrollment();
|
||||
}
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
private void removeEnrollmentObservers() {
|
||||
preRemoveEnrollmentObservers();
|
||||
mProgressViewModel.getErrorMessageLiveData().removeObserver(mErrorMessageObserver);
|
||||
}
|
||||
|
||||
private void preRemoveEnrollmentObservers() {
|
||||
mProgressViewModel.getProgressLiveData().removeObserver(mProgressObserver);
|
||||
mProgressViewModel.getHelpMessageLiveData().removeObserver(mHelpMessageObserver);
|
||||
}
|
||||
|
||||
private void cancelEnrollment() {
|
||||
preRemoveEnrollmentObservers();
|
||||
mProgressViewModel.cancelEnrollment();
|
||||
}
|
||||
|
||||
private void startEnrollment() {
|
||||
final boolean startResult = mProgressViewModel.startEnrollment(ENROLL_ENROLL);
|
||||
if (!startResult) {
|
||||
Log.e(TAG, "startEnrollment(), failed");
|
||||
}
|
||||
mProgressViewModel.getProgressLiveData().observe(this, mProgressObserver);
|
||||
mProgressViewModel.getHelpMessageLiveData().observe(this, mHelpMessageObserver);
|
||||
mProgressViewModel.getErrorMessageLiveData().observe(this, mErrorMessageObserver);
|
||||
}
|
||||
|
||||
private void configureEnrollmentStage(CharSequence description, @RawRes int lottie) {
|
||||
new GlifLayoutHelper(getActivity(), mView).setDescriptionText(description);
|
||||
LottieCompositionFactory.fromRawRes(getActivity(), lottie)
|
||||
.addListener((c) -> {
|
||||
mIllustrationLottie.setComposition(c);
|
||||
mIllustrationLottie.setVisibility(View.VISIBLE);
|
||||
mIllustrationLottie.playAnimation();
|
||||
});
|
||||
}
|
||||
|
||||
private int getCurrentSfpsStage() {
|
||||
EnrollmentProgress progressLiveData = mProgressViewModel.getProgressLiveData().getValue();
|
||||
|
||||
if (progressLiveData == null) {
|
||||
return STAGE_UNKNOWN;
|
||||
}
|
||||
|
||||
final int progressSteps = progressLiveData.getSteps() - progressLiveData.getRemaining();
|
||||
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 void onEnrollmentHelp(@NonNull EnrollmentStatusMessage helpMessage) {
|
||||
final CharSequence helpStr = helpMessage.getStr();
|
||||
if (!TextUtils.isEmpty(helpStr)) {
|
||||
showError(helpStr);
|
||||
}
|
||||
}
|
||||
|
||||
private void onEnrollmentError(@NonNull EnrollmentStatusMessage errorMessage) {
|
||||
removeEnrollmentObservers();
|
||||
|
||||
if (mEnrollingViewModel.getOnBackPressed()
|
||||
&& errorMessage.getMsgId() == FingerprintManager.FINGERPRINT_ERROR_CANCELED) {
|
||||
mEnrollingViewModel.onCancelledDueToOnBackPressed();
|
||||
} else if (mEnrollingViewModel.getOnSkipPressed()
|
||||
&& errorMessage.getMsgId() == FingerprintManager.FINGERPRINT_ERROR_CANCELED) {
|
||||
mEnrollingViewModel.onCancelledDueToOnSkipPressed();
|
||||
} else {
|
||||
final int errMsgId = errorMessage.getMsgId();
|
||||
mEnrollingViewModel.showErrorDialog(
|
||||
new FingerprintEnrollEnrollingViewModel.ErrorDialogData(
|
||||
getString(FingerprintErrorDialog.getErrorMessage(errMsgId)),
|
||||
getString(FingerprintErrorDialog.getErrorTitle(errMsgId)),
|
||||
errMsgId
|
||||
));
|
||||
mProgressViewModel.cancelEnrollment();
|
||||
}
|
||||
}
|
||||
|
||||
private void announceEnrollmentProgress(CharSequence announcement) {
|
||||
mEnrollingViewModel.sendAccessibilityEvent(announcement);
|
||||
}
|
||||
|
||||
private void onEnrollmentProgressChange(@NonNull EnrollmentProgress progress) {
|
||||
updateProgress(true /* animate */, progress);
|
||||
if (mEnrollingViewModel.isAccessibilityEnabled()) {
|
||||
final int percent = (int) (((float) (progress.getSteps() - progress.getRemaining())
|
||||
/ (float) progress.getSteps()) * 100);
|
||||
|
||||
CharSequence announcement = getString(
|
||||
R.string.security_settings_sfps_enroll_progress_a11y_message, percent);
|
||||
announceEnrollmentProgress(announcement);
|
||||
|
||||
mIllustrationLottie.setContentDescription(
|
||||
getString(R.string.security_settings_sfps_animation_a11y_label, percent)
|
||||
);
|
||||
}
|
||||
updateTitleAndDescription();
|
||||
}
|
||||
|
||||
private void updateProgress(boolean animate, @NonNull EnrollmentProgress enrollmentProgress) {
|
||||
if (!mProgressViewModel.isEnrolling()) {
|
||||
Log.d(TAG, "Enrollment not started yet");
|
||||
return;
|
||||
}
|
||||
|
||||
final int progress = getProgress(enrollmentProgress);
|
||||
// Only clear the error when progress has been made.
|
||||
// TODO (b/234772728) Add tests.
|
||||
if (mProgressBar != null && mProgressBar.getProgress() < progress) {
|
||||
clearError();
|
||||
}
|
||||
|
||||
if (animate) {
|
||||
animateProgress(progress);
|
||||
} else {
|
||||
if (mProgressBar != null) {
|
||||
mProgressBar.setProgress(progress);
|
||||
}
|
||||
if (progress >= PROGRESS_BAR_MAX) {
|
||||
mDelayedFinishRunnable.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int getProgress(@NonNull EnrollmentProgress progress) {
|
||||
if (progress.getSteps() == -1) {
|
||||
return 0;
|
||||
}
|
||||
int displayProgress = Math.max(0, progress.getSteps() + 1 - progress.getRemaining());
|
||||
return PROGRESS_BAR_MAX * displayProgress / (progress.getSteps() + 1);
|
||||
}
|
||||
|
||||
private void showError(CharSequence error) {
|
||||
mView.setHeaderText(error);
|
||||
mView.getHeaderTextView().setContentDescription(error);
|
||||
new GlifLayoutHelper(getActivity(), mView).setDescriptionText("");
|
||||
if (isResumed() && !mHelpAnimation.isRunning()) {
|
||||
mHelpAnimation.start();
|
||||
}
|
||||
applySfpsErrorDynamicColors(true);
|
||||
if (isResumed() && mEnrollingViewModel.isAccessibilityEnabled()) {
|
||||
mEnrollingViewModel.vibrateError(getClass().getSimpleName() + "::showError");
|
||||
}
|
||||
}
|
||||
|
||||
private void clearError() {
|
||||
applySfpsErrorDynamicColors(false);
|
||||
}
|
||||
|
||||
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(ANIMATION_DURATION);
|
||||
anim.start();
|
||||
mProgressAnim = anim;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies dynamic colors corresponding to showing or clearing errors on the progress bar
|
||||
* and finger lottie for SFPS
|
||||
*/
|
||||
private void applySfpsErrorDynamicColors(boolean isError) {
|
||||
applyProgressBarDynamicColor(isError);
|
||||
applyLottieDynamicColor(isError);
|
||||
}
|
||||
|
||||
private void applyProgressBarDynamicColor(boolean isError) {
|
||||
final Context context = getActivity().getApplicationContext();
|
||||
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(boolean isError) {
|
||||
final Context context = getActivity().getApplicationContext();
|
||||
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 int getStageThresholdSteps(int index) {
|
||||
final EnrollmentProgress progressLiveData =
|
||||
mProgressViewModel.getProgressLiveData().getValue();
|
||||
|
||||
if (progressLiveData == null || progressLiveData.getSteps() == -1) {
|
||||
Log.w(TAG, "getStageThresholdSteps: Enrollment not started yet");
|
||||
return 1;
|
||||
}
|
||||
return Math.round(progressLiveData.getSteps()
|
||||
* mEnrollingViewModel.getEnrollStageThreshold(index));
|
||||
}
|
||||
|
||||
private void updateTitleAndDescription() {
|
||||
final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(getActivity(), mView);
|
||||
if (mEnrollingViewModel.isAccessibilityEnabled()) {
|
||||
mEnrollingViewModel.clearTalkback();
|
||||
glifLayoutHelper.getGlifLayout().getDescriptionTextView().setAccessibilityLiveRegion(
|
||||
View.ACCESSIBILITY_LIVE_REGION_POLITE);
|
||||
}
|
||||
final int stage = getCurrentSfpsStage();
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "updateTitleAndDescription, stage:" + stage
|
||||
+ ", noAnimation:" + mHaveShownSfpsNoAnimationLottie
|
||||
+ ", center:" + mHaveShownSfpsCenterLottie
|
||||
+ ", tip:" + mHaveShownSfpsTipLottie
|
||||
+ ", leftEdge:" + mHaveShownSfpsLeftEdgeLottie
|
||||
+ ", rightEdge:" + mHaveShownSfpsRightEdgeLottie);
|
||||
}
|
||||
switch (stage) {
|
||||
case SFPS_STAGE_NO_ANIMATION:
|
||||
glifLayoutHelper.setHeaderText(
|
||||
R.string.security_settings_fingerprint_enroll_repeat_title);
|
||||
if (!mHaveShownSfpsNoAnimationLottie) {
|
||||
mHaveShownSfpsNoAnimationLottie = true;
|
||||
mIllustrationLottie.setContentDescription(
|
||||
getString(
|
||||
R.string.security_settings_sfps_animation_a11y_label,
|
||||
0
|
||||
)
|
||||
);
|
||||
configureEnrollmentStage(
|
||||
getString(R.string.security_settings_sfps_enroll_start_message),
|
||||
R.raw.sfps_lottie_no_animation
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case SFPS_STAGE_CENTER:
|
||||
glifLayoutHelper.setHeaderText(
|
||||
R.string.security_settings_sfps_enroll_finger_center_title);
|
||||
if (!mHaveShownSfpsCenterLottie) {
|
||||
mHaveShownSfpsCenterLottie = true;
|
||||
configureEnrollmentStage(
|
||||
getString(R.string.security_settings_sfps_enroll_start_message),
|
||||
R.raw.sfps_lottie_pad_center
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case SFPS_STAGE_FINGERTIP:
|
||||
glifLayoutHelper.setHeaderText(
|
||||
R.string.security_settings_sfps_enroll_fingertip_title);
|
||||
if (!mHaveShownSfpsTipLottie) {
|
||||
mHaveShownSfpsTipLottie = true;
|
||||
configureEnrollmentStage("", R.raw.sfps_lottie_tip);
|
||||
}
|
||||
break;
|
||||
|
||||
case SFPS_STAGE_LEFT_EDGE:
|
||||
glifLayoutHelper.setHeaderText(
|
||||
R.string.security_settings_sfps_enroll_left_edge_title);
|
||||
if (!mHaveShownSfpsLeftEdgeLottie) {
|
||||
mHaveShownSfpsLeftEdgeLottie = true;
|
||||
configureEnrollmentStage("", R.raw.sfps_lottie_left_edge);
|
||||
}
|
||||
break;
|
||||
|
||||
case SFPS_STAGE_RIGHT_EDGE:
|
||||
glifLayoutHelper.setHeaderText(
|
||||
R.string.security_settings_sfps_enroll_right_edge_title);
|
||||
if (!mHaveShownSfpsRightEdgeLottie) {
|
||||
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.
|
||||
glifLayoutHelper.setHeaderText(
|
||||
R.string.security_settings_sfps_enroll_find_sensor_title);
|
||||
glifLayoutHelper.setDescriptionText(getString(
|
||||
R.string.security_settings_sfps_enroll_start_message));
|
||||
final CharSequence description = getString(
|
||||
R.string.security_settings_sfps_enroll_find_sensor_message);
|
||||
glifLayoutHelper.getGlifLayout().getHeaderTextView().setContentDescription(
|
||||
description);
|
||||
glifLayoutHelper.getActivity().setTitle(description);
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void showIconTouchDialog() {
|
||||
mIconTouchCount = 0;
|
||||
mEnrollingViewModel.showIconTouchDialog();
|
||||
}
|
||||
|
||||
private final Runnable mShowDialogRunnable = () -> showIconTouchDialog();
|
||||
|
||||
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, ANIMATION_DURATION);
|
||||
}
|
||||
}
|
||||
|
||||
@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 = () -> mEnrollingViewModel.onEnrollingDone();
|
||||
|
||||
private void maybeHideSfpsText(@NonNull Configuration newConfig) {
|
||||
final HeaderMixin headerMixin = ((GlifLayout) mView).getMixin(HeaderMixin.class);
|
||||
final DescriptionMixin descriptionMixin = ((GlifLayout) mView).getMixin(
|
||||
DescriptionMixin.class);
|
||||
final boolean isLandscape = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE;
|
||||
|
||||
if (isLandscape) {
|
||||
headerMixin.setAutoTextSizeEnabled(true);
|
||||
headerMixin.getTextView().setMinLines(0);
|
||||
headerMixin.getTextView().setMaxLines(10);
|
||||
descriptionMixin.getTextView().setMinLines(0);
|
||||
descriptionMixin.getTextView().setMaxLines(10);
|
||||
} else {
|
||||
headerMixin.setAutoTextSizeEnabled(false);
|
||||
headerMixin.getTextView().setLines(4);
|
||||
// hide the description
|
||||
descriptionMixin.getTextView().setLines(0);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,620 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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.biometrics2.ui.view
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.RawRes
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.hardware.fingerprint.FingerprintManager
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.view.animation.AnimationUtils
|
||||
import android.view.animation.Interpolator
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.airbnb.lottie.LottieAnimationView
|
||||
import com.airbnb.lottie.LottieComposition
|
||||
import com.airbnb.lottie.LottieCompositionFactory
|
||||
import com.airbnb.lottie.LottieProperty
|
||||
import com.airbnb.lottie.model.KeyPath
|
||||
import com.android.settings.R
|
||||
import com.android.settings.biometrics.fingerprint.FingerprintErrorDialog
|
||||
import com.android.settings.biometrics2.ui.model.EnrollmentProgress
|
||||
import com.android.settings.biometrics2.ui.model.EnrollmentStatusMessage
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel
|
||||
import com.google.android.setupcompat.template.FooterBarMixin
|
||||
import com.google.android.setupcompat.template.FooterButton
|
||||
import com.google.android.setupdesign.GlifLayout
|
||||
import com.google.android.setupdesign.template.DescriptionMixin
|
||||
import com.google.android.setupdesign.template.HeaderMixin
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Fragment is used to handle enrolling process for sfps
|
||||
*/
|
||||
class FingerprintEnrollEnrollingSfpsFragment : Fragment() {
|
||||
|
||||
private var _enrollingViewModel: FingerprintEnrollEnrollingViewModel? = null
|
||||
private val enrollingViewModel: FingerprintEnrollEnrollingViewModel
|
||||
get() = _enrollingViewModel!!
|
||||
|
||||
private var _progressViewModel: FingerprintEnrollProgressViewModel? = null
|
||||
private val progressViewModel: FingerprintEnrollProgressViewModel
|
||||
get() = _progressViewModel!!
|
||||
|
||||
private val fastOutSlowInInterpolator: Interpolator
|
||||
get() = AnimationUtils.loadInterpolator(activity, R.interpolator.fast_out_slow_in)
|
||||
|
||||
private var enrollingSfpsView: GlifLayout? = null
|
||||
|
||||
private val progressBar: ProgressBar
|
||||
get() = enrollingSfpsView!!.findViewById<ProgressBar>(R.id.fingerprint_progress_bar)!!
|
||||
|
||||
private var progressAnim: ObjectAnimator? = null
|
||||
|
||||
private val progressAnimationListener: Animator.AnimatorListener =
|
||||
object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(animation: Animator) {}
|
||||
override fun onAnimationRepeat(animation: Animator) {}
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
if (progressBar.progress >= PROGRESS_BAR_MAX) {
|
||||
progressBar.postDelayed(delayedFinishRunnable, PROGRESS_ANIMATION_DURATION)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(animation: Animator) {}
|
||||
}
|
||||
|
||||
private val illustrationLottie: LottieAnimationView
|
||||
get() = enrollingSfpsView!!.findViewById<LottieAnimationView>(R.id.illustration_lottie)!!
|
||||
|
||||
private var haveShownSfpsNoAnimationLottie = false
|
||||
private var haveShownSfpsCenterLottie = false
|
||||
private var haveShownSfpsTipLottie = false
|
||||
private var haveShownSfpsLeftEdgeLottie = false
|
||||
private var haveShownSfpsRightEdgeLottie = false
|
||||
|
||||
private var helpAnimation: ObjectAnimator? = null
|
||||
|
||||
private var iconTouchCount = 0
|
||||
|
||||
private val showIconTouchDialogRunnable = Runnable { showIconTouchDialog() }
|
||||
|
||||
// Give the user a chance to see progress completed before jumping to the next stage.
|
||||
private val delayedFinishRunnable = Runnable { enrollingViewModel.onEnrollingDone() }
|
||||
|
||||
private val onSkipClickListener = View.OnClickListener { _: View? ->
|
||||
enrollingViewModel.setOnSkipPressed()
|
||||
cancelEnrollment()
|
||||
}
|
||||
|
||||
private val progressObserver: Observer<EnrollmentProgress> =
|
||||
Observer<EnrollmentProgress> { progress: EnrollmentProgress? ->
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "progressObserver($progress)")
|
||||
}
|
||||
if (progress != null && progress.steps >= 0) {
|
||||
onEnrollmentProgressChange(progress)
|
||||
}
|
||||
}
|
||||
|
||||
private val helpMessageObserver: Observer<EnrollmentStatusMessage> =
|
||||
Observer<EnrollmentStatusMessage> { helpMessage: EnrollmentStatusMessage? ->
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "helpMessageObserver($helpMessage)")
|
||||
}
|
||||
helpMessage?.let { onEnrollmentHelp(it) }
|
||||
}
|
||||
|
||||
private val errorMessageObserver: Observer<EnrollmentStatusMessage> =
|
||||
Observer<EnrollmentStatusMessage> { errorMessage: EnrollmentStatusMessage? ->
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "errorMessageObserver($errorMessage)")
|
||||
}
|
||||
errorMessage?.let { onEnrollmentError(it) }
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
ViewModelProvider(requireActivity()).let { provider ->
|
||||
_enrollingViewModel = provider[FingerprintEnrollEnrollingViewModel::class.java]
|
||||
_progressViewModel = provider[FingerprintEnrollProgressViewModel::class.java]
|
||||
}
|
||||
super.onAttach(context)
|
||||
requireActivity().onBackPressedDispatcher.addCallback(
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
isEnabled = false
|
||||
enrollingViewModel.setOnBackPressed()
|
||||
cancelEnrollment()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
enrollingSfpsView = inflater.inflate(
|
||||
R.layout.sfps_enroll_enrolling,
|
||||
container, false
|
||||
) as GlifLayout
|
||||
return enrollingSfpsView
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
requireActivity().bindFingerprintEnrollEnrollingSfpsView(
|
||||
view = enrollingSfpsView!!,
|
||||
onSkipClickListener = onSkipClickListener
|
||||
)
|
||||
|
||||
// setHelpAnimation()
|
||||
helpAnimation = ObjectAnimator.ofFloat(
|
||||
enrollingSfpsView!!.findViewById<RelativeLayout>(R.id.progress_lottie)!!,
|
||||
"translationX" /* propertyName */,
|
||||
0f,
|
||||
HELP_ANIMATION_TRANSLATION_X,
|
||||
-1 * HELP_ANIMATION_TRANSLATION_X,
|
||||
HELP_ANIMATION_TRANSLATION_X,
|
||||
0f
|
||||
).also {
|
||||
it.interpolator = AccelerateDecelerateInterpolator()
|
||||
it.setDuration(HELP_ANIMATION_DURATION)
|
||||
it.setAutoCancel(false)
|
||||
}
|
||||
|
||||
progressBar.setOnTouchListener { _: View?, event: MotionEvent ->
|
||||
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||
iconTouchCount++
|
||||
if (iconTouchCount == ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN) {
|
||||
showIconTouchDialog()
|
||||
} else {
|
||||
progressBar.postDelayed(
|
||||
showIconTouchDialogRunnable,
|
||||
ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN
|
||||
)
|
||||
}
|
||||
} else if (event.actionMasked == MotionEvent.ACTION_CANCEL
|
||||
|| event.actionMasked == MotionEvent.ACTION_UP
|
||||
) {
|
||||
progressBar.removeCallbacks(showIconTouchDialogRunnable)
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
startEnrollment()
|
||||
updateProgress(false /* animate */, progressViewModel.progressLiveData.value!!)
|
||||
progressViewModel.helpMessageLiveData.value?.let {
|
||||
onEnrollmentHelp(it)
|
||||
} ?: run {
|
||||
clearError()
|
||||
updateTitleAndDescription()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
removeEnrollmentObservers()
|
||||
if (!activity!!.isChangingConfigurations && progressViewModel.isEnrolling) {
|
||||
progressViewModel.cancelEnrollment()
|
||||
}
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
private fun removeEnrollmentObservers() {
|
||||
preRemoveEnrollmentObservers()
|
||||
progressViewModel.errorMessageLiveData.removeObserver(errorMessageObserver)
|
||||
}
|
||||
|
||||
private fun preRemoveEnrollmentObservers() {
|
||||
progressViewModel.progressLiveData.removeObserver(progressObserver)
|
||||
progressViewModel.helpMessageLiveData.removeObserver(helpMessageObserver)
|
||||
}
|
||||
|
||||
private fun cancelEnrollment() {
|
||||
preRemoveEnrollmentObservers()
|
||||
progressViewModel.cancelEnrollment()
|
||||
}
|
||||
|
||||
private fun startEnrollment() {
|
||||
val startResult: Boolean =
|
||||
progressViewModel.startEnrollment(FingerprintManager.ENROLL_ENROLL)
|
||||
if (!startResult) {
|
||||
Log.e(TAG, "startEnrollment(), failed")
|
||||
}
|
||||
progressViewModel.progressLiveData.observe(this, progressObserver)
|
||||
progressViewModel.helpMessageLiveData.observe(this, helpMessageObserver)
|
||||
progressViewModel.errorMessageLiveData.observe(this, errorMessageObserver)
|
||||
}
|
||||
|
||||
private fun configureEnrollmentStage(description: CharSequence, @RawRes lottie: Int) {
|
||||
GlifLayoutHelper(requireActivity(), enrollingSfpsView!!).setDescriptionText(description)
|
||||
LottieCompositionFactory.fromRawRes(activity, lottie)
|
||||
.addListener { c: LottieComposition ->
|
||||
illustrationLottie.setComposition(c)
|
||||
illustrationLottie.visibility = View.VISIBLE
|
||||
illustrationLottie.playAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
private val currentSfpsStage: Int
|
||||
get() {
|
||||
val progressLiveData: EnrollmentProgress =
|
||||
progressViewModel.progressLiveData.value
|
||||
?: return STAGE_UNKNOWN
|
||||
val progressSteps: Int = progressLiveData.steps - progressLiveData.remaining
|
||||
return if (progressSteps < getStageThresholdSteps(0)) {
|
||||
SFPS_STAGE_NO_ANIMATION
|
||||
} else if (progressSteps < getStageThresholdSteps(1)) {
|
||||
SFPS_STAGE_CENTER
|
||||
} else if (progressSteps < getStageThresholdSteps(2)) {
|
||||
SFPS_STAGE_FINGERTIP
|
||||
} else if (progressSteps < getStageThresholdSteps(3)) {
|
||||
SFPS_STAGE_LEFT_EDGE
|
||||
} else {
|
||||
SFPS_STAGE_RIGHT_EDGE
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEnrollmentHelp(helpMessage: EnrollmentStatusMessage) {
|
||||
val helpStr: CharSequence = helpMessage.str
|
||||
if (helpStr.isNotEmpty()) {
|
||||
showError(helpStr)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEnrollmentError(errorMessage: EnrollmentStatusMessage) {
|
||||
removeEnrollmentObservers()
|
||||
if (enrollingViewModel.onBackPressed
|
||||
&& errorMessage.msgId == FingerprintManager.FINGERPRINT_ERROR_CANCELED
|
||||
) {
|
||||
enrollingViewModel.onCancelledDueToOnBackPressed()
|
||||
} else if (enrollingViewModel.onSkipPressed
|
||||
&& errorMessage.msgId == FingerprintManager.FINGERPRINT_ERROR_CANCELED
|
||||
) {
|
||||
enrollingViewModel.onCancelledDueToOnSkipPressed()
|
||||
} else {
|
||||
val errMsgId: Int = errorMessage.msgId
|
||||
enrollingViewModel.showErrorDialog(
|
||||
FingerprintEnrollEnrollingViewModel.ErrorDialogData(
|
||||
getString(FingerprintErrorDialog.getErrorMessage(errMsgId)),
|
||||
getString(FingerprintErrorDialog.getErrorTitle(errMsgId)),
|
||||
errMsgId
|
||||
)
|
||||
)
|
||||
progressViewModel.cancelEnrollment()
|
||||
}
|
||||
}
|
||||
|
||||
private fun announceEnrollmentProgress(announcement: CharSequence) {
|
||||
enrollingViewModel.sendAccessibilityEvent(announcement)
|
||||
}
|
||||
|
||||
private fun onEnrollmentProgressChange(progress: EnrollmentProgress) {
|
||||
updateProgress(true /* animate */, progress)
|
||||
if (enrollingViewModel.isAccessibilityEnabled) {
|
||||
val percent: Int =
|
||||
((progress.steps - progress.remaining).toFloat() / progress.steps.toFloat() * 100).toInt()
|
||||
val announcement: CharSequence = getString(
|
||||
R.string.security_settings_sfps_enroll_progress_a11y_message, percent
|
||||
)
|
||||
announceEnrollmentProgress(announcement)
|
||||
illustrationLottie.contentDescription =
|
||||
getString(R.string.security_settings_sfps_animation_a11y_label, percent)
|
||||
}
|
||||
updateTitleAndDescription()
|
||||
}
|
||||
|
||||
private fun updateProgress(animate: Boolean, enrollmentProgress: EnrollmentProgress) {
|
||||
if (!progressViewModel.isEnrolling) {
|
||||
Log.d(TAG, "Enrollment not started yet")
|
||||
return
|
||||
}
|
||||
|
||||
val progress = getProgress(enrollmentProgress)
|
||||
|
||||
// Only clear the error when progress has been made.
|
||||
// TODO (b/234772728) Add tests.
|
||||
if (progressBar.progress < progress) {
|
||||
clearError()
|
||||
}
|
||||
if (animate) {
|
||||
animateProgress(progress)
|
||||
} else {
|
||||
progressBar.progress = progress
|
||||
if (progress >= PROGRESS_BAR_MAX) {
|
||||
delayedFinishRunnable.run()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getProgress(progress: EnrollmentProgress): Int {
|
||||
if (progress.steps == -1) {
|
||||
return 0
|
||||
}
|
||||
val displayProgress = Math.max(0, progress.steps + 1 - progress.remaining)
|
||||
return PROGRESS_BAR_MAX * displayProgress / (progress.steps + 1)
|
||||
}
|
||||
|
||||
private fun showError(error: CharSequence) {
|
||||
enrollingSfpsView!!.let {
|
||||
it.headerText = error
|
||||
it.headerTextView.contentDescription = error
|
||||
GlifLayoutHelper(requireActivity(), it).setDescriptionText("")
|
||||
}
|
||||
|
||||
if (isResumed && !helpAnimation!!.isRunning) {
|
||||
helpAnimation!!.start()
|
||||
}
|
||||
applySfpsErrorDynamicColors(true)
|
||||
if (isResumed && enrollingViewModel.isAccessibilityEnabled) {
|
||||
enrollingViewModel.vibrateError(javaClass.simpleName + "::showError")
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearError() {
|
||||
applySfpsErrorDynamicColors(false)
|
||||
}
|
||||
|
||||
private fun animateProgress(progress: Int) {
|
||||
progressAnim?.cancel()
|
||||
progressAnim = ObjectAnimator.ofInt(
|
||||
progressBar,
|
||||
"progress",
|
||||
progressBar.progress,
|
||||
progress
|
||||
).also {
|
||||
it.addListener(progressAnimationListener)
|
||||
it.interpolator = fastOutSlowInInterpolator
|
||||
it.setDuration(PROGRESS_ANIMATION_DURATION)
|
||||
it.start()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies dynamic colors corresponding to showing or clearing errors on the progress bar
|
||||
* and finger lottie for SFPS
|
||||
*/
|
||||
private fun applySfpsErrorDynamicColors(isError: Boolean) {
|
||||
progressBar.applyProgressBarDynamicColor(requireContext(), isError)
|
||||
illustrationLottie.applyLottieDynamicColor(requireContext(), isError)
|
||||
}
|
||||
|
||||
private fun getStageThresholdSteps(index: Int): Int {
|
||||
val progressLiveData: EnrollmentProgress? =
|
||||
progressViewModel.progressLiveData.value
|
||||
if (progressLiveData == null || progressLiveData.steps == -1) {
|
||||
Log.w(TAG, "getStageThresholdSteps: Enrollment not started yet")
|
||||
return 1
|
||||
}
|
||||
return (progressLiveData.steps
|
||||
* enrollingViewModel.getEnrollStageThreshold(index)).roundToInt()
|
||||
}
|
||||
|
||||
private fun updateTitleAndDescription() {
|
||||
val helper = GlifLayoutHelper(requireActivity(), enrollingSfpsView!!)
|
||||
if (enrollingViewModel.isAccessibilityEnabled) {
|
||||
enrollingViewModel.clearTalkback()
|
||||
helper.glifLayout.descriptionTextView.accessibilityLiveRegion =
|
||||
View.ACCESSIBILITY_LIVE_REGION_POLITE
|
||||
}
|
||||
val stage = currentSfpsStage
|
||||
if (DEBUG) {
|
||||
Log.d(
|
||||
TAG, "updateTitleAndDescription, stage:" + stage
|
||||
+ ", noAnimation:" + haveShownSfpsNoAnimationLottie
|
||||
+ ", center:" + haveShownSfpsCenterLottie
|
||||
+ ", tip:" + haveShownSfpsTipLottie
|
||||
+ ", leftEdge:" + haveShownSfpsLeftEdgeLottie
|
||||
+ ", rightEdge:" + haveShownSfpsRightEdgeLottie
|
||||
)
|
||||
}
|
||||
when (stage) {
|
||||
SFPS_STAGE_NO_ANIMATION -> {
|
||||
helper.setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title)
|
||||
if (!haveShownSfpsNoAnimationLottie) {
|
||||
haveShownSfpsNoAnimationLottie = true
|
||||
illustrationLottie.contentDescription =
|
||||
getString(R.string.security_settings_sfps_animation_a11y_label, 0)
|
||||
configureEnrollmentStage(
|
||||
getString(R.string.security_settings_sfps_enroll_start_message),
|
||||
R.raw.sfps_lottie_no_animation
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SFPS_STAGE_CENTER -> {
|
||||
helper.setHeaderText(R.string.security_settings_sfps_enroll_finger_center_title)
|
||||
if (!haveShownSfpsCenterLottie) {
|
||||
haveShownSfpsCenterLottie = true
|
||||
configureEnrollmentStage(
|
||||
getString(R.string.security_settings_sfps_enroll_start_message),
|
||||
R.raw.sfps_lottie_pad_center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SFPS_STAGE_FINGERTIP -> {
|
||||
helper.setHeaderText(R.string.security_settings_sfps_enroll_fingertip_title)
|
||||
if (!haveShownSfpsTipLottie) {
|
||||
haveShownSfpsTipLottie = true
|
||||
configureEnrollmentStage("", R.raw.sfps_lottie_tip)
|
||||
}
|
||||
}
|
||||
|
||||
SFPS_STAGE_LEFT_EDGE -> {
|
||||
helper.setHeaderText(R.string.security_settings_sfps_enroll_left_edge_title)
|
||||
if (!haveShownSfpsLeftEdgeLottie) {
|
||||
haveShownSfpsLeftEdgeLottie = true
|
||||
configureEnrollmentStage("", R.raw.sfps_lottie_left_edge)
|
||||
}
|
||||
}
|
||||
|
||||
SFPS_STAGE_RIGHT_EDGE -> {
|
||||
helper.setHeaderText(R.string.security_settings_sfps_enroll_right_edge_title)
|
||||
if (!haveShownSfpsRightEdgeLottie) {
|
||||
haveShownSfpsRightEdgeLottie = true
|
||||
configureEnrollmentStage("", R.raw.sfps_lottie_right_edge)
|
||||
}
|
||||
}
|
||||
|
||||
STAGE_UNKNOWN -> {
|
||||
// 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.
|
||||
helper.setHeaderText(R.string.security_settings_sfps_enroll_find_sensor_title)
|
||||
helper.setDescriptionText(
|
||||
getString(R.string.security_settings_sfps_enroll_start_message)
|
||||
)
|
||||
val description: CharSequence = getString(
|
||||
R.string.security_settings_sfps_enroll_find_sensor_message
|
||||
)
|
||||
helper.glifLayout.headerTextView.contentDescription = description
|
||||
helper.activity.title = description
|
||||
}
|
||||
|
||||
else -> {
|
||||
helper.setHeaderText(R.string.security_settings_sfps_enroll_find_sensor_title)
|
||||
helper.setDescriptionText(
|
||||
getString(R.string.security_settings_sfps_enroll_start_message)
|
||||
)
|
||||
val description: CharSequence = getString(
|
||||
R.string.security_settings_sfps_enroll_find_sensor_message
|
||||
)
|
||||
helper.glifLayout.headerTextView.contentDescription = description
|
||||
helper.activity.title = description
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showIconTouchDialog() {
|
||||
iconTouchCount = 0
|
||||
enrollingViewModel.showIconTouchDialog()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = FingerprintEnrollEnrollingSfpsFragment::class.java.simpleName
|
||||
private const val DEBUG = false
|
||||
private const val PROGRESS_BAR_MAX = 10000
|
||||
private const val HELP_ANIMATION_DURATION = 550L
|
||||
private const val HELP_ANIMATION_TRANSLATION_X = 40f
|
||||
private const val PROGRESS_ANIMATION_DURATION = 250L
|
||||
private const val ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN: Long = 500
|
||||
private const val ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN = 3
|
||||
private const val STAGE_UNKNOWN = -1
|
||||
private const val SFPS_STAGE_NO_ANIMATION = 0
|
||||
private const val SFPS_STAGE_CENTER = 1
|
||||
private const val SFPS_STAGE_FINGERTIP = 2
|
||||
private const val SFPS_STAGE_LEFT_EDGE = 3
|
||||
private const val SFPS_STAGE_RIGHT_EDGE = 4
|
||||
}
|
||||
}
|
||||
|
||||
fun FragmentActivity.bindFingerprintEnrollEnrollingSfpsView(
|
||||
view: GlifLayout,
|
||||
onSkipClickListener: View.OnClickListener
|
||||
) {
|
||||
GlifLayoutHelper(this, view).setDescriptionText(
|
||||
getString(R.string.security_settings_fingerprint_enroll_start_message)
|
||||
)
|
||||
|
||||
view.getMixin(FooterBarMixin::class.java).secondaryButton = FooterButton.Builder(this)
|
||||
.setText(R.string.security_settings_fingerprint_enroll_enrolling_skip)
|
||||
.setListener(onSkipClickListener)
|
||||
.setButtonType(FooterButton.ButtonType.SKIP)
|
||||
.setTheme(R.style.SudGlifButton_Secondary)
|
||||
.build()
|
||||
|
||||
view.findViewById<ProgressBar>(R.id.fingerprint_progress_bar)!!.progressBackgroundTintMode =
|
||||
PorterDuff.Mode.SRC
|
||||
|
||||
view.findViewById<ProgressBar>(R.id.fingerprint_progress_bar)!!
|
||||
.applyProgressBarDynamicColor(this, false)
|
||||
|
||||
view.findViewById<LottieAnimationView>(R.id.illustration_lottie)!!
|
||||
.applyLottieDynamicColor(this, false)
|
||||
|
||||
view.maybeHideSfpsText(resources.configuration.orientation)
|
||||
}
|
||||
|
||||
private fun ProgressBar.applyProgressBarDynamicColor(context: Context, isError: Boolean) {
|
||||
progressTintList = ColorStateList.valueOf(
|
||||
context.getColor(
|
||||
if (isError)
|
||||
R.color.sfps_enrollment_progress_bar_error_color
|
||||
else
|
||||
R.color.sfps_enrollment_progress_bar_fill_color
|
||||
)
|
||||
)
|
||||
progressTintMode = PorterDuff.Mode.SRC
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun LottieAnimationView.applyLottieDynamicColor(context: Context, isError: Boolean) {
|
||||
addValueCallback<ColorFilter>(
|
||||
KeyPath(".blue100", "**"),
|
||||
LottieProperty.COLOR_FILTER
|
||||
) {
|
||||
PorterDuffColorFilter(
|
||||
context.getColor(
|
||||
if (isError)
|
||||
R.color.sfps_enrollment_fp_error_color
|
||||
else
|
||||
R.color.sfps_enrollment_fp_captured_color
|
||||
),
|
||||
PorterDuff.Mode.SRC_ATOP
|
||||
)
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun GlifLayout.maybeHideSfpsText(@Configuration.Orientation orientation: Int) {
|
||||
val headerMixin: HeaderMixin = getMixin(HeaderMixin::class.java)
|
||||
val descriptionMixin: DescriptionMixin = getMixin(DescriptionMixin::class.java)
|
||||
|
||||
val isLandscape = (orientation == Configuration.ORIENTATION_LANDSCAPE)
|
||||
headerMixin.setAutoTextSizeEnabled(isLandscape)
|
||||
if (isLandscape) {
|
||||
headerMixin.textView.minLines = 0
|
||||
headerMixin.textView.maxLines = 10
|
||||
descriptionMixin.textView.minLines = 0
|
||||
descriptionMixin.textView.maxLines = 10
|
||||
} else {
|
||||
headerMixin.textView.setLines(4)
|
||||
// hide the description
|
||||
descriptionMixin.textView.setLines(0)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user