From a4498e36ee7a9dffe154a270bb60edc1788c25f2 Mon Sep 17 00:00:00 2001 From: firewall Date: Thu, 12 Jan 2023 06:50:00 +0000 Subject: [PATCH] Refactor FingerprintEnrollEnrolling to fragment Bug: b/260957933 Test: NA Change-Id: I8f704297a2a53ddf39734e0fefe258a123255341 --- .../repository/AccessibilityRepository.java | 47 ++ .../repository/FingerprintRepository.java | 17 +- .../data/repository/VibratorRepository.java | 43 ++ .../factory/BiometricsRepositoryProvider.java | 14 + .../BiometricsRepositoryProviderImpl.java | 51 ++ .../factory/BiometricsViewModelFactory.java | 13 + ...ingerprintEnrollEnrollingRfpsFragment.java | 272 ++++++++++ ...ingerprintEnrollEnrollingSfpsFragment.java | 410 +++++++++++++++ ...ngerprintEnrollEnrollingUdfpsFragment.java | 470 ++++++++++++++++++ .../biometrics2/ui/view/GlifLayoutHelper.java | 12 + .../biometrics2/ui/view/IconTouchDialog.java | 63 +++ .../FingerprintEnrollEnrollingViewModel.java | 161 ++++++ .../FingerprintEnrollProgressViewModel.java | 32 ++ 13 files changed, 1604 insertions(+), 1 deletion(-) create mode 100644 src/com/android/settings/biometrics2/data/repository/AccessibilityRepository.java create mode 100644 src/com/android/settings/biometrics2/data/repository/VibratorRepository.java create mode 100644 src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingRfpsFragment.java create mode 100644 src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingSfpsFragment.java create mode 100644 src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingUdfpsFragment.java create mode 100644 src/com/android/settings/biometrics2/ui/view/IconTouchDialog.java create mode 100644 src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollEnrollingViewModel.java diff --git a/src/com/android/settings/biometrics2/data/repository/AccessibilityRepository.java b/src/com/android/settings/biometrics2/data/repository/AccessibilityRepository.java new file mode 100644 index 00000000000..5353f8982d7 --- /dev/null +++ b/src/com/android/settings/biometrics2/data/repository/AccessibilityRepository.java @@ -0,0 +1,47 @@ +/* + * 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.data.repository; + +import android.view.accessibility.AccessibilityManager; + +/** + * This repository is used to call all APIs in {@link AccessibilityManager} + */ +public class AccessibilityRepository { + + private final AccessibilityManager mAccessibilityManager; + + public AccessibilityRepository(AccessibilityManager accessibilityManager) { + mAccessibilityManager = accessibilityManager; + } + + /** + * Requests interruption of the accessibility feedback from all accessibility services. + */ + public void interrupt() { + mAccessibilityManager.interrupt(); + } + + /** + * Returns if the {@link AccessibilityManager} is enabled. + * + * @return True if this {@link AccessibilityManager} is enabled, false otherwise. + */ + public boolean isEnabled() { + return mAccessibilityManager.isEnabled(); + } +} diff --git a/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java b/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java index 64bf898f97f..8f432e61062 100644 --- a/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java +++ b/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java @@ -42,7 +42,8 @@ import java.util.List; public class FingerprintRepository { private static final String TAG = "FingerprintRepository"; - @NonNull private final FingerprintManager mFingerprintManager; + @NonNull + private final FingerprintManager mFingerprintManager; private List mSensorPropertiesCache; @@ -130,4 +131,18 @@ public class FingerprintRepository { return RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled( context, DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT, userId) != null; } + + /** + * Get fingerprint enroll stage threshold + */ + public float getEnrollStageThreshold(int index) { + return mFingerprintManager.getEnrollStageThreshold(index); + } + + /** + * Get fingerprint enroll stage count + */ + public int getEnrollStageCount() { + return mFingerprintManager.getEnrollStageCount(); + } } diff --git a/src/com/android/settings/biometrics2/data/repository/VibratorRepository.java b/src/com/android/settings/biometrics2/data/repository/VibratorRepository.java new file mode 100644 index 00000000000..cccafffe28f --- /dev/null +++ b/src/com/android/settings/biometrics2/data/repository/VibratorRepository.java @@ -0,0 +1,43 @@ +/* + * 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.data.repository; + +import android.annotation.NonNull; +import android.os.VibrationAttributes; +import android.os.VibrationEffect; +import android.os.Vibrator; + +/** + * This repository is used to call all APIs in {@link Vibrator} + */ +public class VibratorRepository { + + private final Vibrator mVibrator; + + public VibratorRepository(Vibrator vibrator) { + mVibrator = vibrator; + } + + /** + * Like {@link #vibrate(VibrationEffect, VibrationAttributes)}, but allows the + * caller to specify the vibration is owned by someone else and set a reason for vibration. + */ + public void vibrate(int uid, String opPkg, @NonNull VibrationEffect vibe, + String reason, @NonNull VibrationAttributes attributes) { + mVibrator.vibrate(uid, opPkg, vibe, reason, attributes); + } +} diff --git a/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProvider.java b/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProvider.java index fdc5745e926..8e17ba45d97 100644 --- a/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProvider.java +++ b/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProvider.java @@ -21,7 +21,9 @@ import android.app.Application; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.settings.biometrics2.data.repository.AccessibilityRepository; import com.android.settings.biometrics2.data.repository.FingerprintRepository; +import com.android.settings.biometrics2.data.repository.VibratorRepository; /** * Interface for BiometricsRepositoryProvider @@ -33,4 +35,16 @@ public interface BiometricsRepositoryProvider { */ @Nullable FingerprintRepository getFingerprintRepository(@NonNull Application application); + + /** + * Get VibtatorRepository + */ + @Nullable + VibratorRepository getVibratorRepository(@NonNull Application application); + + /** + * Get AccessibilityRepository + */ + @Nullable + AccessibilityRepository getAccessibilityRepository(@NonNull Application application); } diff --git a/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProviderImpl.java b/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProviderImpl.java index 22409c849d7..7b1fe162a8e 100644 --- a/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProviderImpl.java +++ b/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProviderImpl.java @@ -18,12 +18,16 @@ package com.android.settings.biometrics2.factory; import android.app.Application; import android.hardware.fingerprint.FingerprintManager; +import android.os.Vibrator; +import android.view.accessibility.AccessibilityManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.settings.Utils; +import com.android.settings.biometrics2.data.repository.AccessibilityRepository; import com.android.settings.biometrics2.data.repository.FingerprintRepository; +import com.android.settings.biometrics2.data.repository.VibratorRepository; /** * Implementation for BiometricsRepositoryProvider @@ -31,6 +35,8 @@ import com.android.settings.biometrics2.data.repository.FingerprintRepository; public class BiometricsRepositoryProviderImpl implements BiometricsRepositoryProvider { private static volatile FingerprintRepository sFingerprintRepository; + private static volatile VibratorRepository sVibratorRepository; + private static volatile AccessibilityRepository sAccessibilityRepository; /** * Get FingerprintRepository @@ -52,4 +58,49 @@ public class BiometricsRepositoryProviderImpl implements BiometricsRepositoryPro } return sFingerprintRepository; } + + /** + * Get VibratorRepository + */ + @Nullable + @Override + public VibratorRepository getVibratorRepository(@NonNull Application application) { + + final Vibrator vibrator = application.getSystemService(Vibrator.class); + if (vibrator == null) { + return null; + } + + if (sVibratorRepository == null) { + synchronized (VibratorRepository.class) { + if (sVibratorRepository == null) { + sVibratorRepository = new VibratorRepository(vibrator); + } + } + } + return sVibratorRepository; + } + + /** + * Get AccessibilityRepository + */ + @Nullable + @Override + public AccessibilityRepository getAccessibilityRepository(@NonNull Application application) { + + final AccessibilityManager accessibilityManager = application.getSystemService( + AccessibilityManager.class); + if (accessibilityManager == null) { + return null; + } + + if (sAccessibilityRepository == null) { + synchronized (AccessibilityRepository.class) { + if (sAccessibilityRepository == null) { + sAccessibilityRepository = new AccessibilityRepository(accessibilityManager); + } + } + } + return sAccessibilityRepository; + } } diff --git a/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java b/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java index 0b84f4c46c4..7bf9d538a86 100644 --- a/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java +++ b/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java @@ -28,12 +28,15 @@ import androidx.lifecycle.viewmodel.CreationExtras; import com.android.internal.widget.LockPatternUtils; import com.android.settings.biometrics.fingerprint.FingerprintUpdater; +import com.android.settings.biometrics2.data.repository.AccessibilityRepository; import com.android.settings.biometrics2.data.repository.FingerprintRepository; +import com.android.settings.biometrics2.data.repository.VibratorRepository; import com.android.settings.biometrics2.ui.model.EnrollmentRequest; import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel; import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.ChallengeGenerator; import com.android.settings.biometrics2.ui.viewmodel.DeviceFoldedViewModel; import com.android.settings.biometrics2.ui.viewmodel.DeviceRotationViewModel; +import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel; import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel; import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel; import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel; @@ -109,6 +112,16 @@ public class BiometricsViewModelFactory implements ViewModelProvider.Factory { return (T) new FingerprintEnrollProgressViewModel(application, new FingerprintUpdater(application), userId); } + } else if (modelClass.isAssignableFrom(FingerprintEnrollEnrollingViewModel.class)) { + final FingerprintRepository fingerprint = provider.getFingerprintRepository( + application); + final AccessibilityRepository accessibility = provider.getAccessibilityRepository( + application); + final VibratorRepository vibrator = provider.getVibratorRepository(application); + if (fingerprint != null && accessibility != null && vibrator != null) { + return (T) new FingerprintEnrollEnrollingViewModel(application, fingerprint, + accessibility, vibrator); + } } return create(modelClass); } diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingRfpsFragment.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingRfpsFragment.java new file mode 100644 index 00000000000..30b66a2d4d0 --- /dev/null +++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingRfpsFragment.java @@ -0,0 +1,272 @@ +/* + * 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.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.PorterDuff; +import android.graphics.drawable.Animatable2; +import android.graphics.drawable.AnimatedVectorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +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.AnimationUtils; +import android.view.animation.Interpolator; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.ViewModelProvider; + +import com.android.settings.R; +import com.android.settings.biometrics.BiometricUtils; +import com.android.settings.biometrics2.ui.model.EnrollmentProgress; +import com.android.settings.biometrics2.ui.viewmodel.DeviceRotationViewModel; +import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel; +import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel; +import com.android.settingslib.display.DisplayDensityUtils; + +import com.airbnb.lottie.LottieAnimationView; +import com.google.android.setupcompat.template.FooterBarMixin; +import com.google.android.setupcompat.template.FooterButton; +import com.google.android.setupdesign.GlifLayout; + +/** + * Fragment is used to handle enrolling process for rfps + */ +public class FingerprintEnrollEnrollingRfpsFragment extends Fragment { + + private static final String TAG = FingerprintEnrollEnrollingRfpsFragment.class.getSimpleName(); + + 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 HINT_TIMEOUT_DURATION = 2500; + + private FingerprintEnrollEnrollingViewModel mEnrollingViewModel; + private DeviceRotationViewModel mRotationViewModel; + private FingerprintEnrollProgressViewModel mProgressViewModel; + + private Interpolator mFastOutSlowInInterpolator; + private Interpolator mLinearOutSlowInInterpolator; + private Interpolator mFastOutLinearInInterpolator; + private boolean mAnimationCancelled; + + private View mView; + private ProgressBar mProgressBar; + private TextView mErrorText; + private FooterBarMixin mFooterBarMixin; + private AnimatedVectorDrawable mIconAnimationDrawable; + private AnimatedVectorDrawable mIconBackgroundBlinksDrawable; + + private LottieAnimationView mIllustrationLottie; + private boolean mShouldShowLottie; + private boolean mIsAccessibilityEnabled; + + private boolean mHaveShownSfpsNoAnimationLottie; + private boolean mHaveShownSfpsCenterLottie; + private boolean mHaveShownSfpsTipLottie; + private boolean mHaveShownSfpsLeftEdgeLottie; + private boolean mHaveShownSfpsRightEdgeLottie; + + private final View.OnClickListener mOnSkipClickListener = + (v) -> mEnrollingViewModel.onSkipButtonClick(); + + private int mIconTouchCount; + + @Override + public void onAttach(@NonNull Context context) { + final FragmentActivity activity = getActivity(); + final ViewModelProvider provider = new ViewModelProvider(activity); + mEnrollingViewModel = provider.get(FingerprintEnrollEnrollingViewModel.class); + mRotationViewModel = provider.get(DeviceRotationViewModel.class); + mProgressViewModel = provider.get(FingerprintEnrollProgressViewModel.class); + super.onAttach(context); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mIsAccessibilityEnabled = mEnrollingViewModel.isAccessibilityEnabled(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + mView = initRfpsLayout(inflater, container); + return mView; + } + + private View initRfpsLayout(LayoutInflater inflater, ViewGroup container) { + final View containView = inflater.inflate(R.layout.sfps_enroll_enrolling, container, false); + final Activity activity = getActivity(); + final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity, + (GlifLayout) containView); + glifLayoutHelper.setDescriptionText( + R.string.security_settings_fingerprint_enroll_start_message); + glifLayoutHelper.setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title); + + mShouldShowLottie = shouldShowLottie(); + boolean isLandscape = BiometricUtils.isReverseLandscape(activity) + || BiometricUtils.isLandscape(activity); + updateOrientation((isLandscape + ? Configuration.ORIENTATION_LANDSCAPE : Configuration.ORIENTATION_PORTRAIT)); + + mErrorText = containView.findViewById(R.id.error_text); + mProgressBar = containView.findViewById(R.id.fingerprint_progress_bar); + mFooterBarMixin = ((GlifLayout) containView).getMixin(FooterBarMixin.class); + mFooterBarMixin.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() + ); + + 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( + activity, android.R.interpolator.fast_out_slow_in); + mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator( + activity, android.R.interpolator.linear_out_slow_in); + mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator( + activity, 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; + }); + } + + return containView; + } + + private void updateOrientation(int orientation) { + switch (orientation) { + case Configuration.ORIENTATION_LANDSCAPE: { + mIllustrationLottie = null; + break; + } + case Configuration.ORIENTATION_PORTRAIT: { + if (mShouldShowLottie) { + mIllustrationLottie = mView.findViewById(R.id.illustration_lottie); + } + break; + } + default: + Log.e(TAG, "Error unhandled configuration change"); + break; + } + } + + private void updateTitleAndDescription() { + final Activity activity = getActivity(); + final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity, + (GlifLayout) mView); + + EnrollmentProgress progressLiveData = mProgressViewModel.getProgressLiveData().getValue(); + if (progressLiveData == null || progressLiveData.getSteps() == -1) { + glifLayoutHelper.setDescriptionText( + R.string.security_settings_fingerprint_enroll_start_message); + } else { + glifLayoutHelper.setDescriptionText( + R.string.security_settings_fingerprint_enroll_repeat_message); + } + } + + private void startIconAnimation() { + if (mIconAnimationDrawable != null) { + mIconAnimationDrawable.start(); + } + } + + private void stopIconAnimation() { + mAnimationCancelled = true; + if (mIconAnimationDrawable != null) { + mIconAnimationDrawable.stop(); + } + } + + private void showIconTouchDialog() { + mIconTouchCount = 0; + //TODO EnrollingActivity should observe live data and add dialog fragment + mEnrollingViewModel.onIconTouchDialogShow(); + } + + private boolean shouldShowLottie() { + DisplayDensityUtils displayDensity = new DisplayDensityUtils(getContext()); + int currentDensityIndex = displayDensity.getCurrentIndexForDefaultDisplay(); + final int currentDensity = displayDensity.getDefaultDisplayDensityValues() + [currentDensityIndex]; + final int defaultDensity = displayDensity.getDefaultDensityForDefaultDisplay(); + return defaultDensity == currentDensity; + } + + private final Runnable mShowDialogRunnable = new Runnable() { + @Override + public void run() { + showIconTouchDialog(); + } + }; + + 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(); + } + }); + } + }; +} diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingSfpsFragment.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingSfpsFragment.java new file mode 100644 index 00000000000..ddeb465216e --- /dev/null +++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingSfpsFragment.java @@ -0,0 +1,410 @@ +/* + * 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.annotation.RawRes; +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.PorterDuff; +import android.graphics.drawable.Animatable2; +import android.graphics.drawable.AnimatedVectorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +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.AnimationUtils; +import android.view.animation.Interpolator; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.ViewModelProvider; + +import com.android.settings.R; +import com.android.settings.biometrics.BiometricUtils; +import com.android.settings.biometrics2.ui.model.EnrollmentProgress; +import com.android.settings.biometrics2.ui.viewmodel.DeviceRotationViewModel; +import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel; +import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel; +import com.android.settingslib.display.DisplayDensityUtils; + +import com.airbnb.lottie.LottieAnimationView; +import com.airbnb.lottie.LottieCompositionFactory; +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 long ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN = 500; + private static final int ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN = 3; + private static final int HINT_TIMEOUT_DURATION = 2500; + + 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 DeviceRotationViewModel mRotationViewModel; + private FingerprintEnrollProgressViewModel mProgressViewModel; + + private Interpolator mFastOutSlowInInterpolator; + private Interpolator mLinearOutSlowInInterpolator; + private Interpolator mFastOutLinearInInterpolator; + private boolean mAnimationCancelled; + + private View mView; + private ProgressBar mProgressBar; + private TextView mErrorText; + private FooterBarMixin mFooterBarMixin; + private AnimatedVectorDrawable mIconAnimationDrawable; + private AnimatedVectorDrawable mIconBackgroundBlinksDrawable; + + private LottieAnimationView mIllustrationLottie; + private boolean mShouldShowLottie; + private boolean mIsAccessibilityEnabled; + + private boolean mHaveShownSfpsNoAnimationLottie; + private boolean mHaveShownSfpsCenterLottie; + private boolean mHaveShownSfpsTipLottie; + private boolean mHaveShownSfpsLeftEdgeLottie; + private boolean mHaveShownSfpsRightEdgeLottie; + + private final View.OnClickListener mOnSkipClickListener = + (v) -> mEnrollingViewModel.onSkipButtonClick(); + + private int mIconTouchCount; + + @Override + public void onAttach(@NonNull Context context) { + final FragmentActivity activity = getActivity(); + final ViewModelProvider provider = new ViewModelProvider(activity); + mEnrollingViewModel = provider.get(FingerprintEnrollEnrollingViewModel.class); + mRotationViewModel = provider.get(DeviceRotationViewModel.class); + mProgressViewModel = provider.get(FingerprintEnrollProgressViewModel.class); + super.onAttach(context); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mIsAccessibilityEnabled = mEnrollingViewModel.isAccessibilityEnabled(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + mView = initSfpsLayout(inflater, container); + final Configuration config = getActivity().getResources().getConfiguration(); + maybeHideSfpsText(config); + return mView; + } + + private View initSfpsLayout(LayoutInflater inflater, ViewGroup container) { + final View containView = inflater.inflate(R.layout.sfps_enroll_enrolling, container, false); + final Activity activity = getActivity(); + final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity, + (GlifLayout) containView); + glifLayoutHelper.setDescriptionText( + R.string.security_settings_fingerprint_enroll_start_message); + updateTitleAndDescription(); + + mShouldShowLottie = shouldShowLottie(); + boolean isLandscape = BiometricUtils.isReverseLandscape(activity) + || BiometricUtils.isLandscape(activity); + updateOrientation((isLandscape + ? Configuration.ORIENTATION_LANDSCAPE : Configuration.ORIENTATION_PORTRAIT)); + + mErrorText = containView.findViewById(R.id.error_text); + mProgressBar = containView.findViewById(R.id.fingerprint_progress_bar); + mFooterBarMixin = ((GlifLayout) containView).getMixin(FooterBarMixin.class); + mFooterBarMixin.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() + ); + + 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( + activity, android.R.interpolator.fast_out_slow_in); + mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator( + activity, android.R.interpolator.linear_out_slow_in); + mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator( + activity, 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; + }); + } + + return containView; + } + + private void updateTitleAndDescription() { + + final Activity activity = getActivity(); + final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity, + (GlifLayout) mView); + + if (mIsAccessibilityEnabled) { + mEnrollingViewModel.clearTalkback(); + ((GlifLayout) mView).getDescriptionTextView().setAccessibilityLiveRegion( + View.ACCESSIBILITY_LIVE_REGION_POLITE); + } + switch (getCurrentSfpsStage()) { + case SFPS_STAGE_NO_ANIMATION: + glifLayoutHelper.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( + 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 && mIllustrationLottie != null) { + 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 && mIllustrationLottie != null) { + 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 && mIllustrationLottie != null) { + 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 && 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. + glifLayoutHelper.setHeaderText( + R.string.security_settings_sfps_enroll_find_sensor_title); + glifLayoutHelper.setDescriptionText( + R.string.security_settings_sfps_enroll_start_message); + final CharSequence description = getString( + R.string.security_settings_sfps_enroll_find_sensor_message); + ((GlifLayout) mView).getHeaderTextView().setContentDescription(description); + activity.setTitle(description); + break; + + } + } + + private void maybeHideSfpsText(@android.annotation.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); + } + + } + + private int getCurrentSfpsStage() { + EnrollmentProgress progressLiveData = mProgressViewModel.getProgressLiveData().getValue(); + + if (progressLiveData == null || progressLiveData.getSteps() == -1) { + 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 int getStageThresholdSteps(int index) { + + 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 updateOrientation(int orientation) { + mIllustrationLottie = mView.findViewById(R.id.illustration_lottie); + } + + private boolean shouldShowLottie() { + DisplayDensityUtils displayDensity = new DisplayDensityUtils(getContext()); + int currentDensityIndex = displayDensity.getCurrentIndexForDefaultDisplay(); + final int currentDensity = displayDensity.getDefaultDisplayDensityValues() + [currentDensityIndex]; + final int defaultDensity = displayDensity.getDefaultDensityForDefaultDisplay(); + return defaultDensity == currentDensity; + } + + + private void startIconAnimation() { + if (mIconAnimationDrawable != null) { + mIconAnimationDrawable.start(); + } + } + + private void stopIconAnimation() { + mAnimationCancelled = true; + if (mIconAnimationDrawable != null) { + mIconAnimationDrawable.stop(); + } + } + + private void showIconTouchDialog() { + mIconTouchCount = 0; + //TODO EnrollingActivity should observe live data and add dialog fragment + mEnrollingViewModel.onIconTouchDialogShow(); + } + + private void configureEnrollmentStage(CharSequence description, @RawRes int lottie) { + final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(getActivity(), + (GlifLayout) mView); + glifLayoutHelper.setDescriptionText(description); + LottieCompositionFactory.fromRawRes(getActivity(), lottie) + .addListener((c) -> { + mIllustrationLottie.setComposition(c); + mIllustrationLottie.setVisibility(View.VISIBLE); + mIllustrationLottie.playAnimation(); + }); + } + + private final Runnable mShowDialogRunnable = new Runnable() { + @Override + public void run() { + showIconTouchDialog(); + } + }; + + 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(); + } + }); + } + }; +} diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingUdfpsFragment.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingUdfpsFragment.java new file mode 100644 index 00000000000..89b061f695f --- /dev/null +++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingUdfpsFragment.java @@ -0,0 +1,470 @@ +/* + * 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.annotation.RawRes; +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.PorterDuff; +import android.graphics.drawable.Animatable2; +import android.graphics.drawable.AnimatedVectorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.ViewModelProvider; + +import com.android.settings.R; +import com.android.settings.biometrics.BiometricUtils; +import com.android.settings.biometrics2.ui.model.EnrollmentProgress; +import com.android.settings.biometrics2.ui.viewmodel.DeviceRotationViewModel; +import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel; +import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel; +import com.android.settingslib.display.DisplayDensityUtils; + +import com.airbnb.lottie.LottieAnimationView; +import com.airbnb.lottie.LottieCompositionFactory; +import com.google.android.setupcompat.template.FooterBarMixin; +import com.google.android.setupcompat.template.FooterButton; +import com.google.android.setupdesign.GlifLayout; + +import java.util.Locale; + +/** + * Fragment is used to handle enrolling process for udfps + */ +public class FingerprintEnrollEnrollingUdfpsFragment extends Fragment { + + private static final String TAG = FingerprintEnrollEnrollingUdfpsFragment.class.getSimpleName(); + + 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 HINT_TIMEOUT_DURATION = 2500; + + 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; + + private FingerprintEnrollEnrollingViewModel mEnrollingViewModel; + private DeviceRotationViewModel mRotationViewModel; + private FingerprintEnrollProgressViewModel mProgressViewModel; + + private Interpolator mFastOutSlowInInterpolator; + private Interpolator mLinearOutSlowInInterpolator; + private Interpolator mFastOutLinearInInterpolator; + private boolean mAnimationCancelled; + + private LottieAnimationView mIllustrationLottie; + private boolean mHaveShownUdfpsTipLottie; + private boolean mHaveShownUdfpsLeftEdgeLottie; + private boolean mHaveShownUdfpsRightEdgeLottie; + private boolean mHaveShownUdfpsCenterLottie; + private boolean mHaveShownUdfpsGuideLottie; + + private View mView; + private ProgressBar mProgressBar; + private TextView mErrorText; + private FooterBarMixin mFooterBarMixin; + private AnimatedVectorDrawable mIconAnimationDrawable; + private AnimatedVectorDrawable mIconBackgroundBlinksDrawable; + + private boolean mShouldShowLottie; + private boolean mIsAccessibilityEnabled; + + private final View.OnClickListener mOnSkipClickListener = + (v) -> mEnrollingViewModel.onSkipButtonClick(); + + private int mIconTouchCount; + + @Override + public void onAttach(@NonNull Context context) { + final FragmentActivity activity = getActivity(); + final ViewModelProvider provider = new ViewModelProvider(activity); + mEnrollingViewModel = provider.get(FingerprintEnrollEnrollingViewModel.class); + mRotationViewModel = provider.get(DeviceRotationViewModel.class); + mProgressViewModel = provider.get(FingerprintEnrollProgressViewModel.class); + super.onAttach(context); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mIsAccessibilityEnabled = mEnrollingViewModel.isAccessibilityEnabled(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + mView = initUdfpsLayout(inflater, container); + return mView; + } + + private View initUdfpsLayout(LayoutInflater inflater, ViewGroup container) { + final View containView = inflater.inflate(R.layout.udfps_enroll_enrolling, container, + false); + + final Activity activity = getActivity(); + final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity, + (GlifLayout) containView); + final int rotation = mRotationViewModel.getLiveData().getValue(); + final boolean isLayoutRtl = (TextUtils.getLayoutDirectionFromLocale( + Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL); + + + //TODO implement b/20653554 + if (rotation == Surface.ROTATION_90) { + final LinearLayout layoutContainer = containView.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); + containView.setLayoutParams(lp); + } + glifLayoutHelper.setDescriptionText(R.string.security_settings_udfps_enroll_start_message); + updateTitleAndDescription(); + + mShouldShowLottie = shouldShowLottie(); + boolean isLandscape = BiometricUtils.isReverseLandscape(activity) + || BiometricUtils.isLandscape(activity); + updateOrientation((isLandscape + ? Configuration.ORIENTATION_LANDSCAPE : Configuration.ORIENTATION_PORTRAIT)); + + mErrorText = containView.findViewById(R.id.error_text); + mProgressBar = containView.findViewById(R.id.fingerprint_progress_bar); + mFooterBarMixin = ((GlifLayout) containView).getMixin(FooterBarMixin.class); + mFooterBarMixin.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() + ); + + 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( + activity, android.R.interpolator.fast_out_slow_in); + mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator( + activity, android.R.interpolator.linear_out_slow_in); + mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator( + activity, 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; + }); + } + + return containView; + } + + private void updateTitleAndDescription() { + + final Activity activity = getActivity(); + final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity, + (GlifLayout) mView); + + switch (getCurrentStage()) { + case STAGE_CENTER: + glifLayoutHelper.setHeaderText( + R.string.security_settings_fingerprint_enroll_repeat_title); + if (mIsAccessibilityEnabled || mIllustrationLottie == null) { + glifLayoutHelper.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: + glifLayoutHelper.setHeaderText( + R.string.security_settings_fingerprint_enroll_repeat_title); + if (mIsAccessibilityEnabled || mIllustrationLottie == null) { + glifLayoutHelper.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: + glifLayoutHelper.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: + glifLayoutHelper.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()) { + glifLayoutHelper.setDescriptionText( + R.string.security_settings_fingerprint_enroll_repeat_message); + } else { + glifLayoutHelper.setDescriptionText( + R.string.security_settings_udfps_enroll_edge_message); + } + } + break; + case STAGE_RIGHT_EDGE: + glifLayoutHelper.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()) { + glifLayoutHelper.setDescriptionText( + R.string.security_settings_fingerprint_enroll_repeat_message); + } else { + glifLayoutHelper.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. + glifLayoutHelper.setHeaderText( + R.string.security_settings_fingerprint_enroll_udfps_title); + glifLayoutHelper.setDescriptionText( + R.string.security_settings_udfps_enroll_start_message); + final CharSequence description = getString( + R.string.security_settings_udfps_enroll_a11y); + ((GlifLayout) mView).getHeaderTextView().setContentDescription(description); + activity.setTitle(description); + break; + + } + } + + private boolean shouldShowLottie() { + DisplayDensityUtils displayDensity = new DisplayDensityUtils(getContext()); + int currentDensityIndex = displayDensity.getCurrentIndexForDefaultDisplay(); + final int currentDensity = displayDensity.getDefaultDisplayDensityValues() + [currentDensityIndex]; + final int defaultDensity = displayDensity.getDefaultDensityForDefaultDisplay(); + return defaultDensity == currentDensity; + } + + private void updateOrientation(int orientation) { + switch (orientation) { + case Configuration.ORIENTATION_LANDSCAPE: { + mIllustrationLottie = null; + break; + } + case Configuration.ORIENTATION_PORTRAIT: { + if (mShouldShowLottie) { + mIllustrationLottie = mView.findViewById(R.id.illustration_lottie); + } + break; + } + default: + Log.e(TAG, "Error unhandled configuration change"); + break; + } + } + + private void startIconAnimation() { + if (mIconAnimationDrawable != null) { + mIconAnimationDrawable.start(); + } + } + + private void stopIconAnimation() { + mAnimationCancelled = true; + if (mIconAnimationDrawable != null) { + mIconAnimationDrawable.stop(); + } + } + + private int getCurrentStage() { + EnrollmentProgress progressLiveData = mProgressViewModel.getProgressLiveData().getValue(); + + if (progressLiveData == null || progressLiveData.getSteps() == -1) { + return STAGE_UNKNOWN; + } + + final int progressSteps = progressLiveData.getSteps() - progressLiveData.getRemaining(); + 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; + } + } + + private boolean isStageHalfCompleted() { + + EnrollmentProgress progressLiveData = mProgressViewModel.getProgressLiveData().getValue(); + if (progressLiveData == null || progressLiveData.getSteps() == -1) { + return false; + } + + final int progressSteps = progressLiveData.getSteps() - progressLiveData.getRemaining(); + int prevThresholdSteps = 0; + for (int i = 0; i < mEnrollingViewModel.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; + } + + private int getStageThresholdSteps(int index) { + + 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 showIconTouchDialog() { + mIconTouchCount = 0; + //TODO EnrollingActivity should observe live data and add dialog fragment + mEnrollingViewModel.onIconTouchDialogShow(); + } + + private void configureEnrollmentStage(CharSequence description, @RawRes int lottie) { + final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(getActivity(), + (GlifLayout) mView); + glifLayoutHelper.setDescriptionText(description); + LottieCompositionFactory.fromRawRes(getActivity(), lottie) + .addListener((c) -> { + mIllustrationLottie.setComposition(c); + mIllustrationLottie.setVisibility(View.VISIBLE); + mIllustrationLottie.playAnimation(); + }); + } + + private final Runnable mShowDialogRunnable = new Runnable() { + @Override + public void run() { + showIconTouchDialog(); + } + }; + + 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(); + } + }); + } + }; + +} diff --git a/src/com/android/settings/biometrics2/ui/view/GlifLayoutHelper.java b/src/com/android/settings/biometrics2/ui/view/GlifLayoutHelper.java index a1645d2f7ab..814f57994df 100644 --- a/src/com/android/settings/biometrics2/ui/view/GlifLayoutHelper.java +++ b/src/com/android/settings/biometrics2/ui/view/GlifLayoutHelper.java @@ -67,4 +67,16 @@ public class GlifLayoutHelper { mGlifLayout.setDescriptionText(description); } } + + /** + * Sets description resId to GlifLayout + */ + public void setDescriptionText(int resId) { + CharSequence previousDescription = mGlifLayout.getDescriptionText(); + CharSequence description = mActivity.getString(resId); + // Prevent a11y for re-reading the same string + if (!TextUtils.equals(previousDescription, description)) { + mGlifLayout.setDescriptionText(resId); + } + } } diff --git a/src/com/android/settings/biometrics2/ui/view/IconTouchDialog.java b/src/com/android/settings/biometrics2/ui/view/IconTouchDialog.java new file mode 100644 index 00000000000..38d6a5b3bde --- /dev/null +++ b/src/com/android/settings/biometrics2/ui/view/IconTouchDialog.java @@ -0,0 +1,63 @@ +/* + * 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.app.Dialog; +import android.app.settings.SettingsEnums; +import android.content.DialogInterface; +import android.os.Bundle; + +import androidx.appcompat.app.AlertDialog; + +import com.android.settings.R; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; + +/** + * Icon Touch dialog + */ +public class IconTouchDialog extends InstrumentedDialogFragment { + +// private FingerprintEnrollEnrollingViewModel mViewModel; +// +// @Override +// public void onAttach(Context context) { +// mViewModel = new ViewModelProvider(getActivity()).get( +// FingerprintEnrollEnrollingViewModel.class); +// super.onAttach(context); +// } + + @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; + } +} diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollEnrollingViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollEnrollingViewModel.java new file mode 100644 index 00000000000..058b50b95f2 --- /dev/null +++ b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollEnrollingViewModel.java @@ -0,0 +1,161 @@ +/* + * 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.viewmodel; + +import android.app.Application; +import android.os.VibrationAttributes; +import android.os.VibrationEffect; +import android.util.Log; +import android.view.accessibility.AccessibilityManager; + +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.MutableLiveData; + +import com.android.settings.biometrics2.data.repository.AccessibilityRepository; +import com.android.settings.biometrics2.data.repository.FingerprintRepository; +import com.android.settings.biometrics2.data.repository.VibratorRepository; +import com.android.settings.biometrics2.ui.model.EnrollmentRequest; + +/** + * ViewModel explaining the fingerprint enrolling page + */ +public class FingerprintEnrollEnrollingViewModel extends AndroidViewModel + implements DefaultLifecycleObserver { + + private static final String TAG = FingerprintEnrollEnrollingViewModel.class.getSimpleName(); + private static final boolean DEBUG = false; + + 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); + + //Enrolling skip + public static final int FINGERPRINT_ENROLL_ENROLLING_ACTION_SKIP = 0; + + //Icon touch dialog show + public static final int FINGERPRINT_ENROLL_ENROLLING_ACTION_SHOW_DIALOG = 0; + + //Icon touch dialog dismiss + public static final int FINGERPRINT_ENROLL_ENROLLING_ACTION_DISMISS_DIALOG = 1; + + private final FingerprintRepository mFingerprintRepository; + private final AccessibilityRepository mAccessibilityRepository; + private final VibratorRepository mVibratorRepository; + + private EnrollmentRequest mEnrollmentRequest = null; + private final MutableLiveData mEnrollingLiveData = new MutableLiveData<>(); + private final MutableLiveData mIconTouchDialogLiveData = new MutableLiveData<>(); + + + public FingerprintEnrollEnrollingViewModel(Application application, + FingerprintRepository fingerprintRepository, + AccessibilityRepository accessibilityRepository, + VibratorRepository vibratorRepository) { + super(application); + mFingerprintRepository = fingerprintRepository; + mAccessibilityRepository = accessibilityRepository; + mVibratorRepository = vibratorRepository; + } + + /** + * User clicks skip button + */ + public void onSkipButtonClick() { + final int action = FINGERPRINT_ENROLL_ENROLLING_ACTION_SKIP; + if (DEBUG) { + Log.d(TAG, "onSkipButtonClick, post action " + action); + } + mEnrollingLiveData.postValue(action); + } + + /** + * Icon touch dialog show + */ + public void onIconTouchDialogShow() { + final int action = FINGERPRINT_ENROLL_ENROLLING_ACTION_SHOW_DIALOG; + if (DEBUG) { + Log.d(TAG, "onIconTouchDialogShow, post action " + action); + } + mIconTouchDialogLiveData.postValue(action); + } + + /** + * Icon touch dialog dismiss + */ + public void onIconTouchDialogDismiss() { + final int action = FINGERPRINT_ENROLL_ENROLLING_ACTION_DISMISS_DIALOG; + if (DEBUG) { + Log.d(TAG, "onIconTouchDialogDismiss, post action " + action); + } + mIconTouchDialogLiveData.postValue(action); + } + + /** + * get enroll stage threshold + */ + public float getEnrollStageThreshold(int index) { + return mFingerprintRepository.getEnrollStageThreshold(index); + } + + /** + * Get enroll stage count + */ + public int getEnrollStageCount() { + return mFingerprintRepository.getEnrollStageCount(); + } + + /** + * The first sensor type is UDFPS sensor or not + */ + public boolean canAssumeUdfps() { + return mFingerprintRepository.canAssumeUdfps(); + } + + /** + * The first sensor type is SFPS sensor or not + */ + public boolean canAssumeSfps() { + return mFingerprintRepository.canAssumeSfps(); + } + + /** + * Requests interruption of the accessibility feedback from all accessibility services. + */ + public void clearTalkback() { + mAccessibilityRepository.interrupt(); + } + + /** + * Returns if the {@link AccessibilityManager} is enabled. + * + * @return True if this {@link AccessibilityManager} is enabled, false otherwise. + */ + public boolean isAccessibilityEnabled() { + return mAccessibilityRepository.isEnabled(); + } + + /** + * Like {@link #vibrate(VibrationEffect, VibrationAttributes)}, but allows the + * caller to specify the vibration is owned by someone else and set a reason for vibration. + */ + public void vibrateError(int uid, String opPkg, String reason) { + mVibratorRepository.vibrate(uid, opPkg, VIBRATE_EFFECT_ERROR, reason, + FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES); + } +} diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModel.java index cbc74c04b9b..532e2cce1d7 100644 --- a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModel.java +++ b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModel.java @@ -56,6 +56,10 @@ public class FingerprintEnrollProgressViewModel extends AndroidViewModel { private final MutableLiveData mErrorMessageLiveData = new MutableLiveData<>(); + private final MutableLiveData mAcquireLiveData = new MutableLiveData<>(); + private final MutableLiveData mPointerDownLiveData = new MutableLiveData<>(); + private final MutableLiveData mPointerUpLiveData = new MutableLiveData<>(); + private byte[] mToken = null; private final int mUserId; @@ -86,6 +90,21 @@ public class FingerprintEnrollProgressViewModel extends AndroidViewModel { public void onEnrollmentError(int errMsgId, CharSequence errString) { mErrorMessageLiveData.postValue(new EnrollmentStatusMessage(errMsgId, errString)); } + + @Override + public void onAcquired(boolean isAcquiredGood) { + mAcquireLiveData.postValue(isAcquiredGood); + } + + @Override + public void onPointerDown(int sensorId) { + mPointerDownLiveData.postValue(sensorId); + } + + @Override + public void onPointerUp(int sensorId) { + mPointerUpLiveData.postValue(sensorId); + } }; public FingerprintEnrollProgressViewModel(@NonNull Application application, @@ -132,6 +151,19 @@ public class FingerprintEnrollProgressViewModel extends AndroidViewModel { return mErrorMessageLiveData; } + public MutableLiveData getAcquireLiveData() { + return mAcquireLiveData; + } + + public MutableLiveData getPointerDownLiveData() { + return mPointerDownLiveData; + } + + public MutableLiveData getPointerUpLiveData() { + return mPointerUpLiveData; + } + + /** * Starts enrollment and return latest isEnrolling() result */