diff --git a/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java b/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java index f58175af520..c7229e3eabe 100644 --- a/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java +++ b/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java @@ -53,6 +53,14 @@ public class FingerprintRepository { return prop != null && prop.isAnyUdfpsType(); } + /** + * The first sensor type is Side fps sensor or not + */ + public boolean canAssumeSfps() { + FingerprintSensorPropertiesInternal prop = getFirstFingerprintSensorPropertiesInternal(); + return prop != null && prop.isAnySidefpsType(); + } + /** * Get max possible number of fingerprints for a user */ @@ -78,6 +86,7 @@ public class FingerprintRepository { @Nullable private FingerprintSensorPropertiesInternal getFirstFingerprintSensorPropertiesInternal() { + // TODO(b/264827022) use API addAuthenticatorsRegisteredCallback final List props = mFingerprintManager.getSensorPropertiesInternal(); return props.size() > 0 ? props.get(0) : null; diff --git a/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProviderImpl.java b/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProviderImpl.java index 87b41e93080..22409c849d7 100644 --- a/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProviderImpl.java +++ b/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProviderImpl.java @@ -30,6 +30,8 @@ import com.android.settings.biometrics2.data.repository.FingerprintRepository; */ public class BiometricsRepositoryProviderImpl implements BiometricsRepositoryProvider { + private static volatile FingerprintRepository sFingerprintRepository; + /** * Get FingerprintRepository */ @@ -41,6 +43,13 @@ public class BiometricsRepositoryProviderImpl implements BiometricsRepositoryPro if (fingerprintManager == null) { return null; } - return new FingerprintRepository(fingerprintManager); + if (sFingerprintRepository == null) { + synchronized (FingerprintRepository.class) { + if (sFingerprintRepository == null) { + sFingerprintRepository = new FingerprintRepository(fingerprintManager); + } + } + } + return sFingerprintRepository; } } diff --git a/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java b/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java index a9f43561ed8..15b187d9e0a 100644 --- a/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java +++ b/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java @@ -27,12 +27,18 @@ import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory; 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.FingerprintRepository; 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.FingerprintEnrollFindSensorViewModel; import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel; +import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel; import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollmentViewModel; import com.android.settings.overlay.FeatureFactory; +import com.android.systemui.unfold.compat.ScreenSizeFoldProvider; /** * View model factory for biometric enrollment fragment @@ -42,7 +48,7 @@ public class BiometricsViewModelFactory implements ViewModelProvider.Factory { private static final String TAG = "BiometricsViewModelFact"; public static final CreationExtras.Key CHALLENGE_GENERATOR = - new CreationExtras.Key() {}; + new CreationExtras.Key<>() {}; @NonNull @Override @@ -59,7 +65,22 @@ public class BiometricsViewModelFactory implements ViewModelProvider.Factory { final BiometricsRepositoryProvider provider = FeatureFactory.getFactory(application) .getBiometricsRepositoryProvider(); - if (modelClass.isAssignableFrom(FingerprintEnrollIntroViewModel.class)) { + if (modelClass.isAssignableFrom(AutoCredentialViewModel.class)) { + final LockPatternUtils lockPatternUtils = + featureFactory.getSecurityFeatureProvider().getLockPatternUtils(application); + final ChallengeGenerator challengeGenerator = extras.get(CHALLENGE_GENERATOR); + if (challengeGenerator != null) { + return (T) new AutoCredentialViewModel(application, lockPatternUtils, + challengeGenerator); + } + } else if (modelClass.isAssignableFrom(DeviceFoldedViewModel.class)) { + return (T) new DeviceFoldedViewModel(new ScreenSizeFoldProvider(application), + application.getMainExecutor()); + } else if (modelClass.isAssignableFrom(DeviceRotationViewModel.class)) { + return (T) new DeviceRotationViewModel(application); + } else if (modelClass.isAssignableFrom(FingerprintEnrollFindSensorViewModel.class)) { + return (T) new FingerprintEnrollFindSensorViewModel(application); + } else if (modelClass.isAssignableFrom(FingerprintEnrollIntroViewModel.class)) { final FingerprintRepository repository = provider.getFingerprintRepository(application); if (repository != null) { return (T) new FingerprintEnrollIntroViewModel(application, repository); @@ -70,14 +91,9 @@ public class BiometricsViewModelFactory implements ViewModelProvider.Factory { return (T) new FingerprintEnrollmentViewModel(application, repository, application.getSystemService(KeyguardManager.class)); } - } else if (modelClass.isAssignableFrom(AutoCredentialViewModel.class)) { - final LockPatternUtils lockPatternUtils = - featureFactory.getSecurityFeatureProvider().getLockPatternUtils(application); - final ChallengeGenerator challengeGenerator = extras.get(CHALLENGE_GENERATOR); - if (challengeGenerator != null) { - return (T) new AutoCredentialViewModel(application, lockPatternUtils, - challengeGenerator); - } + } else if (modelClass.isAssignableFrom(FingerprintEnrollProgressViewModel.class)) { + return (T) new FingerprintEnrollProgressViewModel(application, + new FingerprintUpdater(application)); } return create(modelClass); } diff --git a/src/com/android/settings/biometrics2/ui/model/EnrollmentProgress.java b/src/com/android/settings/biometrics2/ui/model/EnrollmentProgress.java new file mode 100644 index 00000000000..c62d670283a --- /dev/null +++ b/src/com/android/settings/biometrics2/ui/model/EnrollmentProgress.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2022 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.model; + +/** + * Biometric Enrollment progress + */ +public final class EnrollmentProgress { + + public static final int INITIAL_STEPS = -1; + public static final int INITIAL_REMAINING = 0; + + private final int mSteps; + private final int mRemaining; + + public EnrollmentProgress(int steps, int remaining) { + mSteps = steps; + mRemaining = remaining; + } + + public int getSteps() { + return mSteps; + } + + public int getRemaining() { + return mRemaining; + } + + public boolean isInitialStep() { + return mSteps == INITIAL_STEPS; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()) + + "{steps:" + mSteps + ", remaining:" + mRemaining + "}"; + } +} diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFindRfpsFragment.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFindRfpsFragment.java new file mode 100644 index 00000000000..d7537847f44 --- /dev/null +++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFindRfpsFragment.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2022 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_FIND_SENSOR; +import static android.view.View.OnClickListener; + +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +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.FingerprintFindSensorAnimation; +import com.android.settings.biometrics2.ui.model.EnrollmentProgress; +import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel; +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; + +/** + * Fragment explaining the side fingerprint sensor location for fingerprint enrollment. + * It interacts with ProgressViewModel, and FingerprintFindSensorAnimation. + *
+ | Has                 | UDFPS | SFPS | Other (Rear FPS) |
+ |---------------------|-------|------|------------------|
+ | Primary button      | Yes   | No   | No               |
+ | Illustration Lottie | Yes   | Yes  | No               |
+ | Animation           | No    | No   | Depend on layout |
+ | Progress ViewModel  | No    | Yes  | Yes              |
+ | Orientation detect  | No    | Yes  | No               |
+ | Foldable detect     | No    | Yes  | No               |
+ 
+ */ +public class FingerprintEnrollFindRfpsFragment extends Fragment { + + private static final boolean DEBUG = false; + private static final String TAG = "FingerprintEnrollFindRfpsFragment"; + + private FingerprintEnrollFindSensorViewModel mViewModel; + private FingerprintEnrollProgressViewModel mPorgressViewModel; + + private View mView; + private GlifLayout mGlifLayout; + private FooterBarMixin mFooterBarMixin; + private final OnClickListener mOnSkipClickListener = (v) -> mViewModel.onSkipButtonClick(); + @Nullable private FingerprintFindSensorAnimation mAnimation; + + private final Observer mProgressObserver = progress -> { + if (DEBUG) { + Log.d(TAG, "mProgressObserver(" + progress + ")"); + } + if (progress != null && !progress.isInitialStep()) { + mViewModel.onStartButtonClick(); + } + }; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + final Context context = inflater.getContext(); + mView = inflater.inflate(R.layout.fingerprint_enroll_find_sensor, container, false); + mGlifLayout = mView.findViewById(R.id.setup_wizard_layout); + mFooterBarMixin = mGlifLayout.getMixin(FooterBarMixin.class); + mFooterBarMixin.setSecondaryButton( + new FooterButton.Builder(context) + .setText(R.string.security_settings_fingerprint_enroll_enrolling_skip) + .setButtonType(FooterButton.ButtonType.SKIP) + .setTheme(R.style.SudGlifButton_Secondary) + .build() + ); + View animationView = mView.findViewById(R.id.fingerprint_sensor_location_animation); + if (animationView instanceof FingerprintFindSensorAnimation) { + mAnimation = (FingerprintFindSensorAnimation) animationView; + } + return mView; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + final Activity activity = getActivity(); + final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity, mGlifLayout); + glifLayoutHelper.setHeaderText( + R.string.security_settings_fingerprint_enroll_find_sensor_title); + glifLayoutHelper.setDescriptionText( + getText(R.string.security_settings_fingerprint_enroll_find_sensor_message)); + mFooterBarMixin.getSecondaryButton().setOnClickListener(mOnSkipClickListener); + } + + @Override + public void onStart() { + super.onStart(); + + if (DEBUG) { + Log.d(TAG, "onStart(), start looking for fingerprint, animation exist:" + + (mAnimation != null)); + } + startLookingForFingerprint(); + } + + @Override + public void onResume() { + if (mAnimation != null) { + if (DEBUG) { + Log.d(TAG, "onResume(), start animation"); + } + mAnimation.startAnimation(); + } + super.onResume(); + } + + @Override + public void onPause() { + if (mAnimation != null) { + if (DEBUG) { + Log.d(TAG, "onPause(), pause animation"); + } + mAnimation.pauseAnimation(); + } + super.onPause(); + } + + @Override + public void onStop() { + super.onStop(); + if (DEBUG) { + Log.d(TAG, "onStop(), stop looking for fingerprint, animation exist:" + + (mAnimation != null)); + } + stopLookingForFingerprint(); + } + + private void startLookingForFingerprint() { + if (mPorgressViewModel.isEnrolling()) { + Log.d(TAG, "startLookingForFingerprint(), failed because isEnrolling is true before" + + " starting"); + return; + } + + mPorgressViewModel.clearProgressLiveData(); + mPorgressViewModel.getProgressLiveData().observe(this, mProgressObserver); + final boolean startResult = mPorgressViewModel.startEnrollment(ENROLL_FIND_SENSOR); + if (!startResult) { + Log.e(TAG, "startLookingForFingerprint(), failed to start enrollment"); + } + } + + private void stopLookingForFingerprint() { + if (!mPorgressViewModel.isEnrolling()) { + Log.d(TAG, "stopLookingForFingerprint(), failed because isEnrolling is false before" + + " stopping"); + return; + } + + mPorgressViewModel.getProgressLiveData().removeObserver(mProgressObserver); + final boolean cancelResult = mPorgressViewModel.cancelEnrollment(); + if (!cancelResult) { + Log.e(TAG, "stopLookingForFingerprint(), failed to cancel enrollment"); + } + } + + @Override + public void onDestroy() { + if (mAnimation != null) { + if (DEBUG) { + Log.d(TAG, "onDestroy(), stop animation"); + } + mAnimation.stopAnimation(); + } + super.onDestroy(); + } + + @Override + public void onAttach(@NonNull Context context) { + final FragmentActivity activity = getActivity(); + final ViewModelProvider provider = new ViewModelProvider(activity); + mViewModel = provider.get(FingerprintEnrollFindSensorViewModel.class); + mPorgressViewModel = provider.get(FingerprintEnrollProgressViewModel.class); + super.onAttach(context); + } +} diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFindSfpsFragment.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFindSfpsFragment.java new file mode 100644 index 00000000000..c363f04e1e5 --- /dev/null +++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFindSfpsFragment.java @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2022 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_FIND_SENSOR; + +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Surface; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RawRes; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModelProvider; + +import com.android.settings.R; +import com.android.settings.biometrics2.ui.model.EnrollmentProgress; +import com.android.settings.biometrics2.ui.viewmodel.DeviceFoldedViewModel; +import com.android.settings.biometrics2.ui.viewmodel.DeviceRotationViewModel; +import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel; +import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel; +import com.android.settingslib.widget.LottieColorUtils; + +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 explaining the side fingerprint sensor location for fingerprint enrollment. + * It interacts with ProgressViewModel, FoldCallback (for different lottie), and + * LottieAnimationView. + *
+ | Has                 | UDFPS | SFPS | Other (Rear FPS) |
+ |---------------------|-------|------|------------------|
+ | Primary button      | Yes   | No   | No               |
+ | Illustration Lottie | Yes   | Yes  | No               |
+ | Animation           | No    | No   | Depend on layout |
+ | Progress ViewModel  | No    | Yes  | Yes              |
+ | Orientation detect  | No    | Yes  | No               |
+ | Foldable detect     | No    | Yes  | No               |
+ 
+ */ +public class FingerprintEnrollFindSfpsFragment extends Fragment { + + private static final boolean DEBUG = false; + private static final String TAG = "FingerprintEnrollFindSfpsFragment"; + + private FingerprintEnrollFindSensorViewModel mViewModel; + private FingerprintEnrollProgressViewModel mProgressViewModel; + private DeviceRotationViewModel mRotationViewModel; + private DeviceFoldedViewModel mFoldedViewModel; + + private final Observer mRotationObserver = rotation -> { + if (DEBUG) { + Log.d(TAG, "rotationObserver " + rotation); + } + if (rotation == null) { + return; + } + onRotationChanged(rotation); + }; + + @Surface.Rotation private int mAnimationRotation = -1; + + private View mView; + private GlifLayout mGlifLayout; + private FooterBarMixin mFooterBarMixin; + private final OnClickListener mOnSkipClickListener = (v) -> mViewModel.onSkipButtonClick(); + private LottieAnimationView mIllustrationLottie; + + private final Observer mProgressObserver = progress -> { + if (DEBUG) { + Log.d(TAG, "mProgressObserver(" + progress + ")"); + } + if (progress != null && !progress.isInitialStep()) { + mViewModel.onStartButtonClick(); + } + }; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + final Context context = inflater.getContext(); + mView = inflater.inflate(R.layout.sfps_enroll_find_sensor_layout, container, false); + mGlifLayout = mView.findViewById(R.id.setup_wizard_layout); + mIllustrationLottie = mView.findViewById(R.id.illustration_lottie); + mFooterBarMixin = mGlifLayout.getMixin(FooterBarMixin.class); + mFooterBarMixin.setSecondaryButton( + new FooterButton.Builder(context) + .setText(R.string.security_settings_fingerprint_enroll_enrolling_skip) + .setButtonType(FooterButton.ButtonType.SKIP) + .setTheme(R.style.SudGlifButton_Secondary) + .build() + ); + return mView; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + final Activity activity = getActivity(); + final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity, mGlifLayout); + glifLayoutHelper.setHeaderText(R.string.security_settings_sfps_enroll_find_sensor_title); + glifLayoutHelper.setDescriptionText( + getText(R.string.security_settings_sfps_enroll_find_sensor_message)); + mFooterBarMixin.getSecondaryButton().setOnClickListener(mOnSkipClickListener); + } + + @Override + public void onStart() { + super.onStart(); + + if (DEBUG) { + Log.d(TAG, "onStart(), start looking for fingerprint"); + } + startLookingForFingerprint(); + } + + @Override + public void onResume() { + super.onResume(); + final LiveData rotationLiveData = mRotationViewModel.getLiveData(); + playLottieAnimation(rotationLiveData.getValue()); + rotationLiveData.observe(this, mRotationObserver); + } + + @Override + public void onPause() { + mRotationViewModel.getLiveData().removeObserver(mRotationObserver); + super.onPause(); + } + + @Override + public void onStop() { + super.onStop(); + if (DEBUG) { + Log.d(TAG, "onStop(), stop looking for fingerprint"); + } + stopLookingForFingerprint(); + } + + private void startLookingForFingerprint() { + if (mProgressViewModel.isEnrolling()) { + Log.d(TAG, "startLookingForFingerprint(), failed because isEnrolling is true before" + + " starting"); + return; + } + + mProgressViewModel.clearProgressLiveData(); + mProgressViewModel.getProgressLiveData().observe(this, mProgressObserver); + final boolean startResult = mProgressViewModel.startEnrollment(ENROLL_FIND_SENSOR); + if (!startResult) { + Log.e(TAG, "startLookingForFingerprint(), failed to start enrollment"); + } + } + + private void stopLookingForFingerprint() { + if (!mProgressViewModel.isEnrolling()) { + Log.d(TAG, "stopLookingForFingerprint(), failed because isEnrolling is false before" + + " stopping"); + return; + } + + mProgressViewModel.getProgressLiveData().removeObserver(mProgressObserver); + final boolean cancelResult = mProgressViewModel.cancelEnrollment(); + if (!cancelResult) { + Log.e(TAG, "stopLookingForFingerprint(), failed to cancel enrollment"); + } + } + + private void onRotationChanged(@Surface.Rotation int newRotation) { + if (DEBUG) { + Log.d(TAG, "onRotationChanged() from " + mAnimationRotation + " to " + newRotation); + } + if ((newRotation + 2) % 4 == mAnimationRotation) { + playLottieAnimation(newRotation); + } + // Fragment will be re-created if it's changed between landscape and portrait, so no need to + // handle other cases. + } + + private void playLottieAnimation(@Surface.Rotation int rotation) { + @RawRes final int animationRawRes = getSfpsLottieAnimationRawRes(rotation); + if (DEBUG) { + Log.d(TAG, "play lottie animation " + animationRawRes + + ", previous rotation:" + mAnimationRotation + ", new rotation:" + rotation); + } + + mAnimationRotation = rotation; + mIllustrationLottie.setAnimation(animationRawRes); + LottieColorUtils.applyDynamicColors(getActivity(), mIllustrationLottie); + mIllustrationLottie.setVisibility(View.VISIBLE); + mIllustrationLottie.playAnimation(); + } + + @RawRes + private int getSfpsLottieAnimationRawRes(@Surface.Rotation int rotation) { + final boolean isFolded = !Boolean.FALSE.equals(mFoldedViewModel.getLiveData().getValue()); + switch (rotation) { + case Surface.ROTATION_90: + return isFolded ? R.raw.fingerprint_edu_lottie_folded_top_left + : R.raw.fingerprint_edu_lottie_portrait_top_left; + case Surface.ROTATION_180 : + return isFolded ? R.raw.fingerprint_edu_lottie_folded_bottom_left + : R.raw.fingerprint_edu_lottie_landscape_bottom_left; + case Surface.ROTATION_270 : + return isFolded ? R.raw.fingerprint_edu_lottie_folded_bottom_right + : R.raw.fingerprint_edu_lottie_portrait_bottom_right; + default : + return isFolded ? R.raw.fingerprint_edu_lottie_folded_top_right + : R.raw.fingerprint_edu_lottie_landscape_top_right; + } + } + + @Override + public void onAttach(@NonNull Context context) { + final FragmentActivity activity = getActivity(); + final ViewModelProvider provider = new ViewModelProvider(activity); + mViewModel = provider.get(FingerprintEnrollFindSensorViewModel.class); + mProgressViewModel = provider.get(FingerprintEnrollProgressViewModel.class); + mRotationViewModel = provider.get(DeviceRotationViewModel.class); + mFoldedViewModel = provider.get(DeviceFoldedViewModel.class); + super.onAttach(context); + } +} diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFindUdfpsFragment.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFindUdfpsFragment.java new file mode 100644 index 00000000000..565f017b703 --- /dev/null +++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFindUdfpsFragment.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2022 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.view.View.OnClickListener; + +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; + +import com.android.settings.R; +import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel; + +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 explaining the under-display fingerprint sensor location for fingerprint enrollment. + * It interacts with Primary button, and LottieAnimationView. + *
+ | Has                 | UDFPS | SFPS | Other (Rear FPS) |
+ |---------------------|-------|------|------------------|
+ | Primary button      | Yes   | No   | No               |
+ | Illustration Lottie | Yes   | Yes  | No               |
+ | Animation           | No    | No   | Depend on layout |
+ | Progress ViewModel  | No    | Yes  | Yes              |
+ | Orientation detect  | No    | Yes  | No               |
+ | Foldable detect     | No    | Yes  | No               |
+ 
+ */ +public class FingerprintEnrollFindUdfpsFragment extends Fragment { + + private FingerprintEnrollFindSensorViewModel mViewModel; + + private View mView; + private GlifLayout mGlifLayout; + private FooterBarMixin mFooterBarMixin; + private final OnClickListener mOnSkipClickListener = (v) -> mViewModel.onSkipButtonClick(); + private final OnClickListener mOnStartClickListener = (v) -> mViewModel.onStartButtonClick(); + private LottieAnimationView mIllustrationLottie; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + final Context context = inflater.getContext(); + mView = inflater.inflate(R.layout.udfps_enroll_find_sensor_layout, container, false); + mGlifLayout = mView.findViewById(R.id.setup_wizard_layout); + mIllustrationLottie = mView.findViewById(R.id.illustration_lottie); + mFooterBarMixin = mGlifLayout.getMixin(FooterBarMixin.class); + mFooterBarMixin.setSecondaryButton( + new FooterButton.Builder(context) + .setText(R.string.security_settings_fingerprint_enroll_enrolling_skip) + .setButtonType(FooterButton.ButtonType.SKIP) + .setTheme(R.style.SudGlifButton_Secondary) + .build() + ); + mFooterBarMixin.setPrimaryButton( + new FooterButton.Builder(context) + .setText(R.string.security_settings_udfps_enroll_find_sensor_start_button) + .setButtonType(FooterButton.ButtonType.NEXT) + .setTheme(R.style.SudGlifButton_Primary) + .build() + ); + return mView; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + final Activity activity = getActivity(); + final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity, mGlifLayout); + glifLayoutHelper.setHeaderText(R.string.security_settings_udfps_enroll_find_sensor_title); + glifLayoutHelper.setDescriptionText( + getText(R.string.security_settings_udfps_enroll_find_sensor_message)); + mFooterBarMixin.getSecondaryButton().setOnClickListener(mOnSkipClickListener); + mFooterBarMixin.getPrimaryButton().setOnClickListener(mOnStartClickListener); + mIllustrationLottie.setOnClickListener(mOnStartClickListener); + + if (mViewModel.isAccessibilityEnabled()) { + mIllustrationLottie.setAnimation(R.raw.udfps_edu_a11y_lottie); + } + } + + @Override + public void onAttach(@NonNull Context context) { + mViewModel = new ViewModelProvider(getActivity()).get( + FingerprintEnrollFindSensorViewModel.class); + super.onAttach(context); + } +} diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollIntroFragment.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollIntroFragment.java index ecec36f50de..f6e06f9d938 100644 --- a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollIntroFragment.java +++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollIntroFragment.java @@ -24,18 +24,17 @@ import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroSt import static com.google.android.setupdesign.util.DynamicColorPalette.ColorType.ACCENT; -import android.app.Activity; import android.app.admin.DevicePolicyManager; import android.content.Context; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.os.Bundle; import android.text.Html; -import android.text.TextUtils; import android.text.method.LinkMovementMethod; import android.util.Log; import android.view.LayoutInflater; import android.view.View; +import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; @@ -69,6 +68,9 @@ public class FingerprintEnrollIntroFragment extends Fragment { private View mView = null; private FooterButton mPrimaryFooterButton = null; private FooterButton mSecondaryFooterButton = null; + private final OnClickListener mOnNextClickListener = (v) -> mViewModel.onNextButtonClick(); + private final OnClickListener mOnSkipOrCancelClickListener = + (v) -> mViewModel.onSkipOrCancelButtonClick(); private ImageView mIconShield = null; private TextView mFooterMessage6 = null; @Nullable private PorterDuffColorFilter mIconColorFilter; @@ -150,8 +152,8 @@ public class FingerprintEnrollIntroFragment extends Fragment { final Context context = view.getContext(); - mPrimaryFooterButton.setOnClickListener(mViewModel::onNextButtonClick); - mSecondaryFooterButton.setOnClickListener(mViewModel::onSkipOrCancelButtonClick); + mPrimaryFooterButton.setOnClickListener(mOnNextClickListener); + mSecondaryFooterButton.setOnClickListener(mOnSkipOrCancelClickListener); if (mViewModel.canAssumeUdfps()) { mFooterMessage6.setVisibility(View.VISIBLE); @@ -165,15 +167,15 @@ public class FingerprintEnrollIntroFragment extends Fragment { ? R.string.security_settings_fingerprint_enroll_introduction_cancel : R.string.security_settings_fingerprint_enroll_introduction_no_thanks); + final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(getActivity(), getLayout()); if (mViewModel.isBiometricUnlockDisabledByAdmin() && !mViewModel.isParentalConsentRequired()) { - setHeaderText( - getActivity(), + glifLayoutHelper.setHeaderText( R.string.security_settings_fingerprint_enroll_introduction_title_unlock_disabled ); - getLayout().setDescriptionText(getDescriptionDisabledByAdmin(context)); + glifLayoutHelper.setDescriptionText(getDescriptionDisabledByAdmin(context)); } else { - setHeaderText(getActivity(), + glifLayoutHelper.setHeaderText( R.string.security_settings_fingerprint_enroll_introduction_title); } observePageStatusLiveDataIfNeed(); @@ -192,7 +194,7 @@ public class FingerprintEnrollIntroFragment extends Fragment { final RequireScrollMixin requireScrollMixin = getLayout() .getMixin(RequireScrollMixin.class); requireScrollMixin.requireScrollWithButton(getActivity(), mPrimaryFooterButton, - getMoreButtonTextRes(), mViewModel::onNextButtonClick); + getMoreButtonTextRes(), mOnNextClickListener); // Always set true to setHasScrolledToBottom() before registering listener through // setOnRequireScrollStateChangedListener(), because listener will not be called if first @@ -253,21 +255,6 @@ public class FingerprintEnrollIntroFragment extends Fragment { } } - private void setHeaderText(@NonNull Activity activity, int resId) { - TextView layoutTitle = getLayout().getHeaderTextView(); - CharSequence previousTitle = layoutTitle.getText(); - CharSequence title = activity.getText(resId); - if (previousTitle != title) { - if (!TextUtils.isEmpty(previousTitle)) { - layoutTitle.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); - } - getLayout().setHeaderText(title); - getLayout().getHeaderTextView().setContentDescription(title); - activity.setTitle(title); - } - getLayout().getHeaderTextView().setContentDescription(activity.getText(resId)); - } - void updateFooterButtons(@NonNull FingerprintEnrollIntroStatus status) { @StringRes final int scrollToBottomPrimaryResId = status.getEnrollableStatus() == FINGERPRINT_ENROLLABLE_OK diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.java index cd4c539b361..366b7acef18 100644 --- a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.java +++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.java @@ -16,6 +16,7 @@ package com.android.settings.biometrics2.ui.view; +import static androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; import static androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY; import static com.android.settings.biometrics2.factory.BiometricsViewModelFactory.CHALLENGE_GENERATOR; @@ -23,24 +24,34 @@ import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewMo import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK; import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_IS_GENERATING_CHALLENGE; import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_VALID; +import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel.FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_DIALOG; +import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel.FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_SKIP; +import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel.FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_START; +import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel.FingerprintEnrollFindSensorAction; import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL; import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH; import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL; +import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FingerprintEnrollIntroAction; +import android.annotation.StyleRes; import android.app.Application; import android.content.Intent; import android.content.res.ColorStateList; +import android.content.res.Resources; import android.graphics.Color; import android.os.Bundle; import android.util.Log; import androidx.activity.result.ActivityResult; +import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; +import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.viewmodel.CreationExtras; import androidx.lifecycle.viewmodel.MutableCreationExtras; @@ -48,14 +59,16 @@ import androidx.lifecycle.viewmodel.MutableCreationExtras; import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.biometrics.BiometricEnrollBase; -import com.android.settings.biometrics.fingerprint.FingerprintEnrollFindSensor; -import com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollFindSensor; +import com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling; +import com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollEnrolling; import com.android.settings.biometrics2.data.repository.FingerprintRepository; import com.android.settings.biometrics2.factory.BiometricsViewModelFactory; import com.android.settings.biometrics2.ui.model.EnrollmentRequest; import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel; import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.FingerprintChallengeGenerator; +import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel; import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel; +import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel; import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollmentViewModel; import com.android.settings.overlay.FeatureFactory; @@ -66,37 +79,57 @@ import com.google.android.setupdesign.util.ThemeHelper; */ public class FingerprintEnrollmentActivity extends FragmentActivity { + private static final boolean DEBUG = false; private static final String TAG = "FingerprintEnrollmentActivity"; + private static final String INTRO_TAG = "enroll-intro"; + private static final String FIND_UDFPS_TAG = "enroll-find-udfps"; + private static final String FIND_SFPS_TAG = "enroll-find-sfps"; + private static final String FIND_RFPS_TAG = "enroll-find-rfps"; + private static final String SKIP_SETUP_FIND_FPS_DIALOG_TAG = "skip-setup-dialog"; + protected static final int LAUNCH_CONFIRM_LOCK_ACTIVITY = 1; + private ViewModelProvider mViewModelProvider; private FingerprintEnrollmentViewModel mViewModel; private AutoCredentialViewModel mAutoCredentialViewModel; - private ActivityResultLauncher mNextActivityLauncher; - private ActivityResultLauncher mChooseLockLauncher; + private final Observer mIntroActionObserver = action -> { + if (DEBUG) { + Log.d(TAG, "mIntroActionObserver(" + action + ")"); + } + if (action != null) { + onIntroAction(action); + } + }; + private final Observer mFindSensorActionObserver = action -> { + if (DEBUG) { + Log.d(TAG, "mFindSensorActionObserver(" + action + ")"); + } + if (action != null) { + onFindSensorAction(action); + } + }; + private final ActivityResultCallback mNextActivityResultCallback = + result -> mViewModel.onContinueEnrollActivityResult(result, + mAutoCredentialViewModel.getUserId()); + private final ActivityResultLauncher mNextActivityLauncher = + registerForActivityResult(new StartActivityForResult(), mNextActivityResultCallback); + private final ActivityResultCallback mChooseLockResultCallback = + result -> onChooseOrConfirmLockResult(true /* isChooseLock */, result); + private final ActivityResultLauncher mChooseLockLauncher = + registerForActivityResult(new StartActivityForResult(), mChooseLockResultCallback); @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mNextActivityLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - (it) -> mViewModel.onContinueEnrollActivityResult( - it, - mAutoCredentialViewModel.getUserId()) - ); - mChooseLockLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - (it) -> onChooseOrConfirmLockResult(true, it) - ); + mViewModelProvider = new ViewModelProvider(this); - ViewModelProvider viewModelProvider = new ViewModelProvider(this); - - mViewModel = viewModelProvider.get(FingerprintEnrollmentViewModel.class); + mViewModel = mViewModelProvider.get(FingerprintEnrollmentViewModel.class); mViewModel.setRequest(new EnrollmentRequest(getIntent(), getApplicationContext())); mViewModel.setSavedInstanceState(savedInstanceState); - mAutoCredentialViewModel = viewModelProvider.get(AutoCredentialViewModel.class); + mAutoCredentialViewModel = mViewModelProvider.get(AutoCredentialViewModel.class); mAutoCredentialViewModel.setCredentialModel(savedInstanceState, getIntent()); // Theme @@ -106,20 +139,37 @@ public class FingerprintEnrollmentActivity extends FragmentActivity { // fragment setContentView(R.layout.biometric_enrollment_container); - final FingerprintEnrollIntroViewModel introViewModel = - viewModelProvider.get(FingerprintEnrollIntroViewModel.class); - introViewModel.setEnrollmentRequest(mViewModel.getRequest()); - introViewModel.setUserId(mAutoCredentialViewModel.getUserId()); + if (DEBUG) { + Log.e(TAG, "onCreate() has savedInstance:" + (savedInstanceState != null)); + } if (savedInstanceState == null) { checkCredential(); - - final String tag = "FingerprintEnrollIntroFragment"; - getSupportFragmentManager().beginTransaction() - .setReorderingAllowed(true) - .add(R.id.fragment_container_view, FingerprintEnrollIntroFragment.class, null, - tag) - .commit(); + startIntroFragment(); + } else { + final FragmentManager manager = getSupportFragmentManager(); + String[] tags = new String[] { + FIND_UDFPS_TAG, + FIND_SFPS_TAG, + FIND_RFPS_TAG, + INTRO_TAG + }; + for (String tag: tags) { + final Fragment fragment = manager.findFragmentByTag(tag); + if (fragment == null) { + continue; + } + if (DEBUG) { + Log.e(TAG, "onCreate() currentFragment:" + tag); + } + if (tag.equals(INTRO_TAG)) { + attachIntroViewModel(); + } else { // FIND_UDFPS_TAG, FIND_SFPS_TAG, FIND_RFPS_TAG + attachFindSensorViewModel(); + attachIntroViewModel(); + } + break; + } } // observe LiveData @@ -128,11 +178,80 @@ public class FingerprintEnrollmentActivity extends FragmentActivity { mAutoCredentialViewModel.getGenerateChallengeFailedLiveData().observe(this, this::onGenerateChallengeFailed); + } + + private void startIntroFragment() { + attachIntroViewModel(); + getSupportFragmentManager().beginTransaction() + .setReorderingAllowed(true) + .replace(R.id.fragment_container_view, FingerprintEnrollIntroFragment.class, null, + INTRO_TAG) + .commit(); + } + + private void attachIntroViewModel() { + final FingerprintEnrollIntroViewModel introViewModel = + mViewModelProvider.get(FingerprintEnrollIntroViewModel.class); + + introViewModel.setEnrollmentRequest(mViewModel.getRequest()); + introViewModel.setUserId(mAutoCredentialViewModel.getUserId()); // Clear ActionLiveData in FragmentViewModel to prevent getting previous action during - // recreate, like press 'I agree' then press 'back' in FingerprintEnrollFindSensor activity. + // recreate, like press 'Agree' then press 'back' in FingerprintEnrollFindSensor activity. introViewModel.clearActionLiveData(); - introViewModel.getActionLiveData().observe(this, this::observeIntroAction); + introViewModel.getActionLiveData().observe(this, mIntroActionObserver); + } + + // We need to make sure token is valid before entering find sensor page + private void startFindSensorFragment() { + attachFindSensorViewModel(); + if (mViewModel.canAssumeUdfps()) { + // UDFPS does not need to start real fingerprint enrolling during finding sensor + startFindFpsFragmentWithProgressViewModel(FingerprintEnrollFindUdfpsFragment.class, + FIND_UDFPS_TAG, false /* initProgressViewModel */); + } else if (mViewModel.canAssumeSfps()) { + startFindFpsFragmentWithProgressViewModel(FingerprintEnrollFindSfpsFragment.class, + FIND_SFPS_TAG, true /* initProgressViewModel */); + } else { + startFindFpsFragmentWithProgressViewModel(FingerprintEnrollFindRfpsFragment.class, + FIND_RFPS_TAG, true /* initProgressViewModel */); + } + } + + private void startFindFpsFragmentWithProgressViewModel( + @NonNull Class findFpsClass, @NonNull String tag, + boolean initProgressViewModel) { + if (initProgressViewModel) { + final FingerprintEnrollProgressViewModel progressViewModel = + mViewModelProvider.get(FingerprintEnrollProgressViewModel.class); + progressViewModel.setUserId(mAutoCredentialViewModel.getUserId()); + progressViewModel.setToken(mAutoCredentialViewModel.getToken()); + } + final FingerprintEnrollFindSensorViewModel findSensorViewModel = + mViewModelProvider.get(FingerprintEnrollFindSensorViewModel.class); + findSensorViewModel.setIsSuw(mViewModel.getRequest().isSuw()); + getSupportFragmentManager().beginTransaction() + .setReorderingAllowed(true) + .setCustomAnimations(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out, + R.anim.sud_slide_back_in, R.anim.sud_slide_back_out) + .replace(R.id.fragment_container_view, findFpsClass, null, tag) + .addToBackStack(tag) + .commit(); + } + + private void attachFindSensorViewModel() { + final FingerprintEnrollFindSensorViewModel findSensorViewModel = + mViewModelProvider.get(FingerprintEnrollFindSensorViewModel.class); + + // Clear ActionLiveData in FragmentViewModel to prevent getting previous action during + // recreate, like press 'Start' then press 'back' in FingerprintEnrollEnrolling activity. + findSensorViewModel.clearActionLiveData(); + findSensorViewModel.getActionLiveData().observe(this, mFindSensorActionObserver); + } + + private void startSkipSetupFindFpsDialog() { + new SkipSetupFindFpsDialog().show(getSupportFragmentManager(), + SKIP_SETUP_FIND_FPS_DIALOG_TAG); } private void onGenerateChallengeFailed(@NonNull Boolean ignoredBoolean) { @@ -217,10 +336,7 @@ public class FingerprintEnrollmentActivity extends FragmentActivity { } } - private void observeIntroAction(@Nullable Integer action) { - if (action == null) { - return; - } + private void onIntroAction(@FingerprintEnrollIntroAction int action) { switch (action) { case FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH: { onSetActivityResult( @@ -233,13 +349,30 @@ public class FingerprintEnrollmentActivity extends FragmentActivity { return; } case FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL: { + startFindSensorFragment(); + } + } + } + + private void onFindSensorAction(@FingerprintEnrollFindSensorAction int action) { + switch (action) { + case FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_SKIP: { + onSetActivityResult( + new ActivityResult(BiometricEnrollBase.RESULT_SKIP, null)); + return; + } + case FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_DIALOG: { + startSkipSetupFindFpsDialog(); + return; + } + case FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_START: { final boolean isSuw = mViewModel.getRequest().isSuw(); if (!mViewModel.isWaitingActivityResult().compareAndSet(false, true)) { Log.w(TAG, "startNext, isSuw:" + isSuw + ", fail to set isWaiting flag"); } - final Intent intent = new Intent(this, isSuw - ? SetupFingerprintEnrollFindSensor.class - : FingerprintEnrollFindSensor.class); + Intent intent = new Intent(this, isSuw + ? SetupFingerprintEnrollEnrolling.class + : FingerprintEnrollEnrolling.class); intent.putExtras(mAutoCredentialViewModel.createCredentialIntentExtra()); intent.putExtras(mViewModel.getNextActivityBaseIntentExtras()); mNextActivityLauncher.launch(intent); @@ -253,6 +386,12 @@ public class FingerprintEnrollmentActivity extends FragmentActivity { mViewModel.checkFinishActivityDuringOnPause(isFinishing(), isChangingConfigurations()); } + @Override + protected void onApplyThemeResource(Resources.Theme theme, @StyleRes int resid, boolean first) { + theme.applyStyle(R.style.SetupWizardPartnerResource, true); + super.onApplyThemeResource(theme, resid, first); + } + @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { if (requestCode == LAUNCH_CONFIRM_LOCK_ACTIVITY) { diff --git a/src/com/android/settings/biometrics2/ui/view/GlifLayoutHelper.java b/src/com/android/settings/biometrics2/ui/view/GlifLayoutHelper.java new file mode 100644 index 00000000000..a1645d2f7ab --- /dev/null +++ b/src/com/android/settings/biometrics2/ui/view/GlifLayoutHelper.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2022 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.text.TextUtils; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; + +import com.google.android.setupdesign.GlifLayout; + +/** + * Utils class for GlifLayout + */ +public class GlifLayoutHelper { + + @NonNull private final Activity mActivity; + @NonNull private final GlifLayout mGlifLayout; + + public GlifLayoutHelper(@NonNull Activity activity, @NonNull GlifLayout glifLayout) { + mActivity = activity; + mGlifLayout = glifLayout; + } + + /** + * Sets header text to GlifLayout + */ + public void setHeaderText(@StringRes int textResId) { + TextView layoutTitle = mGlifLayout.getHeaderTextView(); + CharSequence previousTitle = layoutTitle.getText(); + CharSequence title = mActivity.getText(textResId); + if (previousTitle != title) { + if (!TextUtils.isEmpty(previousTitle)) { + layoutTitle.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); + } + mGlifLayout.setHeaderText(title); + mGlifLayout.getHeaderTextView().setContentDescription(title); + mActivity.setTitle(title); + } + mGlifLayout.getHeaderTextView().setContentDescription(title); + } + + /** + * Sets description text to GlifLayout + */ + public void setDescriptionText(CharSequence description) { + CharSequence previousDescription = mGlifLayout.getDescriptionText(); + // Prevent a11y for re-reading the same string + if (!TextUtils.equals(previousDescription, description)) { + mGlifLayout.setDescriptionText(description); + } + } +} diff --git a/src/com/android/settings/biometrics2/ui/view/SkipSetupFindFpsDialog.java b/src/com/android/settings/biometrics2/ui/view/SkipSetupFindFpsDialog.java new file mode 100644 index 00000000000..b011267c1b2 --- /dev/null +++ b/src/com/android/settings/biometrics2/ui/view/SkipSetupFindFpsDialog.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2022 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.content.DialogInterface.OnClickListener; + +import android.app.Dialog; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.lifecycle.ViewModelProvider; + +import com.android.settings.R; +import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; + +/** + * Skip dialog which shows when user clicks "Do it later" button in FingerprintFindSensor page. + */ +public class SkipSetupFindFpsDialog extends InstrumentedDialogFragment { + + private FingerprintEnrollFindSensorViewModel mViewModel; + private final OnClickListener mOnSkipClickListener = + (d, w) -> mViewModel.onSkipDialogButtonClick(); + + @Override + public int getMetricsCategory() { + return SettingsEnums.DIALOG_FINGERPRINT_SKIP_SETUP; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + return onCreateDialogBuilder().create(); + } + + /** + * Returns builder for this dialog + */ + @NonNull + private AlertDialog.Builder onCreateDialogBuilder() { + return new AlertDialog.Builder(getActivity(), R.style.Theme_AlertDialog) + .setTitle(R.string.setup_fingerprint_enroll_skip_title) + .setPositiveButton(R.string.skip_anyway_button_label, mOnSkipClickListener) + .setNegativeButton(R.string.go_back_button_label, null) + .setMessage(R.string.setup_fingerprint_enroll_skip_after_adding_lock_text); + } + + @Override + public void onAttach(Context context) { + mViewModel = new ViewModelProvider(getActivity()).get( + FingerprintEnrollFindSensorViewModel.class); + super.onAttach(context); + } +} diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModel.java index a4f94b5771f..aa20f23dab9 100644 --- a/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModel.java +++ b/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModel.java @@ -253,7 +253,11 @@ public class AutoCredentialViewModel extends AndroidViewModel { if (isUnspecifiedPassword()) { return CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK; } else if (mCredentialModel.isValidGkPwHandle()) { - generateChallenge(mCredentialModel.getGkPwHandle()); + final long gkPwHandle = mCredentialModel.getGkPwHandle(); + mCredentialModel.clearGkPwHandle(); + // GkPwHandle is got through caller activity, we shall not revoke it after + // generateChallenge(). Let caller activity to make decision. + generateChallenge(gkPwHandle, false /* revokeGkPwHandle */); mIsGeneratingChallengeDuringCheckingCredential = true; return CREDENTIAL_IS_GENERATING_CHALLENGE; } else { @@ -261,7 +265,7 @@ public class AutoCredentialViewModel extends AndroidViewModel { } } - private void generateChallenge(long gkPwHandle) { + private void generateChallenge(long gkPwHandle, boolean revokeGkPwHandle) { mChallengeGenerator.setCallback((sensorId, userId, challenge) -> { try { final byte[] newToken = requestGatekeeperHat(gkPwHandle, challenge, userId); @@ -274,11 +278,13 @@ public class AutoCredentialViewModel extends AndroidViewModel { return; } - mLockPatternUtils.removeGatekeeperPasswordHandle(gkPwHandle); - mCredentialModel.clearGkPwHandle(); + if (revokeGkPwHandle) { + mLockPatternUtils.removeGatekeeperPasswordHandle(gkPwHandle); + } if (DEBUG) { - Log.d(TAG, "generateChallenge " + mCredentialModel); + Log.d(TAG, "generateChallenge(), model:" + mCredentialModel + + ", revokeGkPwHandle:" + revokeGkPwHandle); } // Check credential again @@ -314,7 +320,9 @@ public class AutoCredentialViewModel extends AndroidViewModel { if (data != null) { final long gkPwHandle = result.getData().getLongExtra( EXTRA_KEY_GK_PW_HANDLE, INVALID_GK_PW_HANDLE); - generateChallenge(gkPwHandle); + // Revoke self requested GkPwHandle because it shall only used once inside this + // activity lifecycle. + generateChallenge(gkPwHandle, true /* revokeGkPwHandle */); return true; } } @@ -328,6 +336,14 @@ public class AutoCredentialViewModel extends AndroidViewModel { return mCredentialModel.getUserId(); } + /** + * Get userId for this credential + */ + @Nullable + public byte[] getToken() { + return mCredentialModel.getToken(); + } + @Nullable private byte[] requestGatekeeperHat(long gkPwHandle, long challenge, int userId) throws IllegalStateException { diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/DeviceFoldedViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/DeviceFoldedViewModel.java new file mode 100644 index 00000000000..6ea60b149a1 --- /dev/null +++ b/src/com/android/settings/biometrics2/ui/viewmodel/DeviceFoldedViewModel.java @@ -0,0 +1,66 @@ +/* + * 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.util.Log; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.android.systemui.unfold.compat.ScreenSizeFoldProvider; +import com.android.systemui.unfold.updates.FoldProvider; + +import java.util.concurrent.Executor; + +/** + * ViewModel explaining the fingerprint sensor location for fingerprint enrollment. + */ +public class DeviceFoldedViewModel extends ViewModel { + + private static final String TAG = "DeviceFoldedViewModel"; + + @NonNull private final MutableLiveData mLiveData = + new MutableLiveData<>(null); + + private final ScreenSizeFoldProvider mScreenSizeFoldProvider; + private final FoldProvider.FoldCallback mIsFoldedCallback = isFolded -> { + Log.d(TAG, "onFoldUpdated= " + isFolded); + mLiveData.postValue(isFolded); + }; + + public DeviceFoldedViewModel(@NonNull ScreenSizeFoldProvider screenSizeFoldProvider, + @NonNull Executor executor) { + super(); + mScreenSizeFoldProvider = screenSizeFoldProvider; + mScreenSizeFoldProvider.registerCallback(mIsFoldedCallback, executor); + } + + /** + * Returns FoldedLiveData + */ + public LiveData getLiveData() { + return mLiveData; + } + + @Override + protected void onCleared() { + mScreenSizeFoldProvider.unregisterCallback(mIsFoldedCallback); + super.onCleared(); + } +} diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/DeviceRotationViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/DeviceRotationViewModel.java new file mode 100644 index 00000000000..c9135fb6f0b --- /dev/null +++ b/src/com/android/settings/biometrics2/ui/viewmodel/DeviceRotationViewModel.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2022 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 static android.hardware.display.DisplayManager.DisplayListener; + +import android.app.Application; +import android.hardware.display.DisplayManager; +import android.util.Log; +import android.view.DisplayInfo; +import android.view.Surface; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +/** + * ViewModel explaining the fingerprint sensor location for fingerprint enrollment. + */ +public class DeviceRotationViewModel extends AndroidViewModel { + + private static final boolean DEBUG = false; + private static final String TAG = "RotationViewModel"; + + private final DisplayManager mDisplayManager; + @NonNull private final DisplayInfo mDisplayInfo = new DisplayInfo(); + private final DisplayListener mDisplayListener = new DisplayListener() { + @Override + public void onDisplayAdded(int displayId) { + } + + @Override + public void onDisplayRemoved(int displayId) { + } + + @Override + public void onDisplayChanged(int displayId) { + final int rotation = getRotation(); + if (DEBUG) { + Log.d(TAG, "onDisplayChanged(" + displayId + "), rotation:" + rotation); + } + mLiveData.postValue(rotation); + } + }; + + @NonNull private final MutableLiveData mLiveData = + new MutableLiveData<>(getRotation()); + + public DeviceRotationViewModel(@NonNull Application application) { + super(application); + mDisplayManager = application.getSystemService(DisplayManager.class); + mDisplayManager.registerDisplayListener(mDisplayListener, + application.getMainThreadHandler()); + } + + /** + * Returns current rotation + */ + @Surface.Rotation + private int getRotation() { + getApplication().getDisplay().getDisplayInfo(mDisplayInfo); + return mDisplayInfo.rotation; + } + + /** + * Returns RotationLiveData + */ + public LiveData getLiveData() { + return mLiveData; + } + + @Override + protected void onCleared() { + mDisplayManager.unregisterDisplayListener(mDisplayListener); + super.onCleared(); + } +} diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollFindSensorViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollFindSensorViewModel.java new file mode 100644 index 00000000000..a871fe120a8 --- /dev/null +++ b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollFindSensorViewModel.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2022 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.annotation.IntDef; +import android.app.Application; +import android.util.Log; +import android.view.accessibility.AccessibilityManager; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * ViewModel explaining the fingerprint sensor location for fingerprint enrollment. + */ +public class FingerprintEnrollFindSensorViewModel extends AndroidViewModel { + + private static final boolean DEBUG = false; + private static final String TAG = "FingerprintEnrollFindSensorViewModel"; + + /** + * User clicks 'Skip' button on this page in Settings + */ + public static final int FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_SKIP = 0; + + /** + * User clicks 'Skip' button on this page in SetupWizard flow + */ + public static final int FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_DIALOG = 1; + + /** + * User clicks 'Start' button on this page + */ + public static final int FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_START = 2; + + @IntDef(prefix = { "FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_" }, value = { + FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_SKIP, + FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_DIALOG, + FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_START + }) + @Retention(RetentionPolicy.SOURCE) + public @interface FingerprintEnrollFindSensorAction {} + + private final AccessibilityManager mAccessibilityManager; + + private boolean mIsSuw = false; + @NonNull private final MutableLiveData mActionLiveData = new MutableLiveData<>(); + + public FingerprintEnrollFindSensorViewModel(@NonNull Application application) { + super(application); + mAccessibilityManager = application.getSystemService(AccessibilityManager.class); + } + + /** + * Sets isSetupWizard or not + */ + public void setIsSuw(boolean isSuw) { + mIsSuw = isSuw; + } + + /** + * Returns action live data that user chooses + */ + public LiveData getActionLiveData() { + return mActionLiveData; + } + + /** + * Clear ActionLiveData to prevent get obsolete data + */ + public void clearActionLiveData() { + mActionLiveData.setValue(null); + } + + /** + * User clicks skip button on dialog + */ + public void onSkipDialogButtonClick() { + final int action = FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_SKIP; + if (DEBUG) { + Log.d(TAG, "onSkipDialogButtonClick, post " + action); + } + mActionLiveData.postValue(action); + } + + /** + * User clicks skip button + */ + public void onSkipButtonClick() { + final int action = mIsSuw + ? FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_DIALOG + : FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_SKIP; + if (DEBUG) { + Log.d(TAG, "onSkipButtonClick, post action " + action); + } + mActionLiveData.postValue(action); + } + + /** + * User clicks start button + */ + public void onStartButtonClick() { + final int action = FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_START; + if (DEBUG) { + Log.d(TAG, "onStartButtonClick, post action " + action); + } + mActionLiveData.postValue(action); + } + + /** + * Returns the info about accessibility is enabled or not + */ + public boolean isAccessibilityEnabled() { + return mAccessibilityManager.isEnabled(); + } +} diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModel.java index 72611a7c791..228ea19a8d4 100644 --- a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModel.java +++ b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModel.java @@ -24,7 +24,6 @@ import android.annotation.IntDef; import android.app.Application; import android.os.UserHandle; import android.util.Log; -import android.view.View; import androidx.annotation.NonNull; import androidx.lifecycle.AndroidViewModel; @@ -189,7 +188,7 @@ public class FingerprintEnrollIntroViewModel extends AndroidViewModel /** * User clicks next button */ - public void onNextButtonClick(View ignoredView) { + public void onNextButtonClick() { final Integer status = mEnrollableStatusLiveData.getValue(); switch (status != null ? status : ENROLLABLE_STATUS_DEFAULT) { case FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX: @@ -206,7 +205,7 @@ public class FingerprintEnrollIntroViewModel extends AndroidViewModel /** * User clicks skip/cancel button */ - public void onSkipOrCancelButtonClick(View ignoredView) { + public void onSkipOrCancelButtonClick() { mActionLiveData.postValue(FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL); } diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModel.java new file mode 100644 index 00000000000..cec94751f73 --- /dev/null +++ b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModel.java @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2022 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 static com.android.settings.biometrics2.ui.model.EnrollmentProgress.INITIAL_REMAINING; +import static com.android.settings.biometrics2.ui.model.EnrollmentProgress.INITIAL_STEPS; + +import android.app.Application; +import android.content.res.Resources; +import android.hardware.fingerprint.FingerprintManager.EnrollReason; +import android.hardware.fingerprint.FingerprintManager.EnrollmentCallback; +import android.os.CancellationSignal; +import android.os.SystemClock; +import android.os.UserHandle; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.android.settings.R; +import com.android.settings.biometrics.fingerprint.FingerprintUpdater; +import com.android.settings.biometrics.fingerprint.MessageDisplayController; +import com.android.settings.biometrics2.ui.model.EnrollmentProgress; + +/** + * Progress ViewModel handles the state around biometric enrollment. It manages the state of + * enrollment throughout the activity lifecycle so the app can continue after an event like + * rotation. + */ +public class FingerprintEnrollProgressViewModel extends AndroidViewModel { + + private static final boolean DEBUG = false; + private static final String TAG = "FingerprintEnrollProgressViewModel"; + + private final MutableLiveData mProgressLiveData = new MutableLiveData<>( + new EnrollmentProgress(INITIAL_STEPS, INITIAL_REMAINING)); + + private byte[] mToken = null; + private int mUserId = UserHandle.myUserId(); + + private final FingerprintUpdater mFingerprintUpdater; + private final MessageDisplayController mMessageDisplayController; + private EnrollmentHelper mEnrollmentHelper; + private final EnrollmentCallback mEnrollmentCallback = new EnrollmentCallback() { + + @Override + public void onEnrollmentProgress(int remaining) { + final int currentSteps = getSteps(); + final EnrollmentProgress progress = new EnrollmentProgress( + currentSteps == INITIAL_STEPS ? remaining : getSteps(), remaining); + if (DEBUG) { + Log.d(TAG, "onEnrollmentProgress(" + remaining + "), steps: " + currentSteps + + ", post progress as " + progress); + } + mProgressLiveData.postValue(progress); + // TODO set enrolling to false when remaining is 0 during implementing b/260957933 + } + + @Override + public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) { + // TODO add LiveData for help message during implementing b/260957933 + } + + @Override + public void onEnrollmentError(int errMsgId, CharSequence errString) { + // TODO add LiveData for error message during implementing b/260957933 + } + }; + + public FingerprintEnrollProgressViewModel(@NonNull Application application, + @NonNull FingerprintUpdater fingerprintUpdater) { + super(application); + mFingerprintUpdater = fingerprintUpdater; + final Resources res = application.getResources(); + mMessageDisplayController = + res.getBoolean(R.bool.enrollment_message_display_controller_flag) + ? new MessageDisplayController( + application.getMainThreadHandler(), + mEnrollmentCallback, + SystemClock.elapsedRealtimeClock(), + res.getInteger(R.integer.enrollment_help_minimum_time_display), + res.getInteger(R.integer.enrollment_progress_minimum_time_display), + res.getBoolean(R.bool.enrollment_progress_priority_over_help), + res.getBoolean(R.bool.enrollment_prioritize_acquire_messages), + res.getInteger(R.integer.enrollment_collect_time)) : null; + } + + public void setToken(byte[] token) { + mToken = token; + } + + public void setUserId(int userId) { + mUserId = userId; + } + + /** + * clear progress + */ + public void clearProgressLiveData() { + mProgressLiveData.setValue(new EnrollmentProgress(INITIAL_STEPS, INITIAL_REMAINING)); + } + + public LiveData getProgressLiveData() { + return mProgressLiveData; + } + + /** + * Starts enrollment and return latest isEnrolling() result + */ + public boolean startEnrollment(@EnrollReason int reason) { + if (mToken == null) { + Log.e(TAG, "Null hardware auth token for enroll"); + return false; + } + if (isEnrolling()) { + Log.w(TAG, "Enrolling has started, shall not start again"); + return true; + } + + mEnrollmentHelper = new EnrollmentHelper( + mMessageDisplayController != null + ? mMessageDisplayController + : mEnrollmentCallback); + mEnrollmentHelper.startEnrollment(mFingerprintUpdater, mToken, mUserId, reason); + return true; + } + + /** + * Cancels enrollment and return latest isEnrolling result + */ + public boolean cancelEnrollment() { + if (!isEnrolling() || mEnrollmentHelper == null) { + Log.e(TAG, "Fail to cancel enrollment, enrollmentController exist:" + + (mEnrollmentHelper != null)); + return false; + } + + mEnrollmentHelper.cancelEnrollment(); + mEnrollmentHelper = null; + return true; + } + + public boolean isEnrolling() { + return (mEnrollmentHelper != null); + } + + private int getSteps() { + return mProgressLiveData.getValue().getSteps(); + } + + /** + * This class is used to stop latest message from onEnrollmentError() after user cancelled + * enrollment. This class will not forward message anymore after mCancellationSignal is sent. + */ + private static class EnrollmentHelper extends EnrollmentCallback { + + @NonNull private final EnrollmentCallback mEnrollmentCallback; + @Nullable private CancellationSignal mCancellationSignal = new CancellationSignal(); + + EnrollmentHelper(@NonNull EnrollmentCallback enrollmentCallback) { + mEnrollmentCallback = enrollmentCallback; + } + + @Override + public void onEnrollmentError(int errMsgId, CharSequence errString) { + if (mCancellationSignal == null) { + return; + } + mEnrollmentCallback.onEnrollmentError(errMsgId, errString); + } + + @Override + public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) { + if (mCancellationSignal == null) { + return; + } + mEnrollmentCallback.onEnrollmentHelp(helpMsgId, helpString); + } + + @Override + public void onEnrollmentProgress(int remaining) { + if (mCancellationSignal == null) { + return; + } + mEnrollmentCallback.onEnrollmentProgress(remaining); + } + + /** + * Starts enrollment + */ + public boolean startEnrollment(@NonNull FingerprintUpdater fingerprintUpdater, + @NonNull byte[] token, int userId, @EnrollReason int reason) { + if (mCancellationSignal == null) { + // Not allow enrolling twice as same instance. Allocate a new instance for second + // enrollment. + return false; + } + fingerprintUpdater.enroll(token, mCancellationSignal, userId, this, reason); + return true; + } + + /** + * Cancels current enrollment + */ + public void cancelEnrollment() { + final CancellationSignal cancellationSignal = mCancellationSignal; + mCancellationSignal = null; + + if (cancellationSignal != null) { + cancellationSignal.cancel(); + } + } + } +} diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModel.java index 4b862731da5..30ed2032edd 100644 --- a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModel.java +++ b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModel.java @@ -124,8 +124,6 @@ public class FingerprintEnrollmentViewModel extends AndroidViewModel implements } } - - private boolean isKeyguardSecure() { return mKeyguardManager != null && mKeyguardManager.isKeyguardSecure(); } @@ -182,4 +180,18 @@ public class FingerprintEnrollmentViewModel extends AndroidViewModel implements public void onSaveInstanceState(@NonNull Bundle outState) { outState.putBoolean(SAVED_STATE_IS_WAITING_ACTIVITY_RESULT, mIsWaitingActivityResult.get()); } + + /** + * The first sensor type is UDFPS sensor or not + */ + public boolean canAssumeUdfps() { + return mFingerprintRepository.canAssumeUdfps(); + } + + /** + * The first sensor type is side fps sensor or not + */ + public boolean canAssumeSfps() { + return mFingerprintRepository.canAssumeSfps(); + } } diff --git a/tests/uitests/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivityTest.java b/tests/uitests/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivityTest.java index 24f4f89e5bc..b074090afae 100644 --- a/tests/uitests/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivityTest.java +++ b/tests/uitests/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivityTest.java @@ -58,26 +58,30 @@ public class FingerprintEnrollmentActivityTest { } @Test - public void lunchWithoutCredential() { + public void launchWithoutCredential() { launchFingerprintEnrollActivity(true); Assert.assertNotNull(mDevice.wait(Until.hasObject( By.text("Choose your backup screen lock method")), IDLE_TIMEOUT)); } @Test - public void lunchWithCredential() { + public void launchWithCredential() { LockScreenUtil.setLockscreen(LockScreenUtil.LockscreenType.PIN, "1234", true); launchFingerprintEnrollActivity(true); - Assert.assertNotNull(mDevice.wait(Until.hasObject( - By.text("More")), IDLE_TIMEOUT)); + for (long i = 0; i < IDLE_TIMEOUT; i += 100L) { + if (mDevice.wait(Until.hasObject(By.text("More")), 50L) != null) { + break; + } else if (mDevice.wait(Until.hasObject(By.text("I agree")), 50L) != null) { + break; + } + } - //click more btn twice and the introduction should stay in the last page - UiObject2 moreBtn = mDevice.findObject(By.text("More")); - moreBtn.click(); - Assert.assertNotNull(mDevice.wait(Until.hasObject( - By.text("More")), IDLE_TIMEOUT)); - moreBtn = mDevice.findObject(By.text("More")); - moreBtn.click(); + //click more btn at most twice and the introduction should stay in the last page + UiObject2 moreBtn; + for (int i = 0; i < 2 && (moreBtn = mDevice.findObject(By.text("More"))) != null; ++i) { + moreBtn.click(); + mDevice.wait(Until.hasObject(By.text("More")), IDLE_TIMEOUT); + } Assert.assertNotNull(mDevice.wait(Until.hasObject( By.text("I agree")), IDLE_TIMEOUT)); @@ -114,5 +118,4 @@ public class FingerprintEnrollmentActivityTest { intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); mContext.startActivity(intent); } - } diff --git a/tests/unit/src/com/android/settings/biometrics2/data/repository/FingerprintRepositoryTest.java b/tests/unit/src/com/android/settings/biometrics2/data/repository/FingerprintRepositoryTest.java index e5920f35b4a..6fb86903316 100644 --- a/tests/unit/src/com/android/settings/biometrics2/data/repository/FingerprintRepositoryTest.java +++ b/tests/unit/src/com/android/settings/biometrics2/data/repository/FingerprintRepositoryTest.java @@ -23,16 +23,17 @@ import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFP import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC; import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UNKNOWN; -import static com.android.settings.biometrics2.util.FingerprintManagerUtil.setupFingerprintEnrolledFingerprints; -import static com.android.settings.biometrics2.util.FingerprintManagerUtil.setupFingerprintFirstSensor; - import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.when; import android.content.Context; import android.content.res.Resources; +import android.hardware.biometrics.SensorProperties; +import android.hardware.fingerprint.Fingerprint; import android.hardware.fingerprint.FingerprintManager; +import android.hardware.fingerprint.FingerprintSensorProperties; +import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; @@ -48,6 +49,8 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import java.util.ArrayList; + @RunWith(AndroidJUnit4.class) public class FingerprintRepositoryTest { @@ -66,31 +69,49 @@ public class FingerprintRepositoryTest { } @Test - public void testCanAssumeSensorType() { + public void testCanAssumeSensorType_forUnknownSensor() { setupFingerprintFirstSensor(mFingerprintManager, TYPE_UNKNOWN, 1); assertThat(mFingerprintRepository.canAssumeUdfps()).isFalse(); + assertThat(mFingerprintRepository.canAssumeSfps()).isFalse(); + } + @Test + public void testCanAssumeSensorType_forRearSensor() { setupFingerprintFirstSensor(mFingerprintManager, TYPE_REAR, 1); assertThat(mFingerprintRepository.canAssumeUdfps()).isFalse(); + assertThat(mFingerprintRepository.canAssumeSfps()).isFalse(); + } + @Test + public void testCanAssumeSensorType_forUdfpsUltrasonicSensor() { setupFingerprintFirstSensor(mFingerprintManager, TYPE_UDFPS_ULTRASONIC, 1); assertThat(mFingerprintRepository.canAssumeUdfps()).isTrue(); + assertThat(mFingerprintRepository.canAssumeSfps()).isFalse(); + } + @Test + public void testCanAssumeSensorType_forUdfpsOpticalSensor() { setupFingerprintFirstSensor(mFingerprintManager, TYPE_UDFPS_OPTICAL, 1); assertThat(mFingerprintRepository.canAssumeUdfps()).isTrue(); + assertThat(mFingerprintRepository.canAssumeSfps()).isFalse(); + } + @Test + public void testCanAssumeSensorType_forPowerButtonSensor() { setupFingerprintFirstSensor(mFingerprintManager, TYPE_POWER_BUTTON, 1); assertThat(mFingerprintRepository.canAssumeUdfps()).isFalse(); + assertThat(mFingerprintRepository.canAssumeSfps()).isTrue(); + } + @Test + public void testCanAssumeSensorType_forHomeButtonSensor() { setupFingerprintFirstSensor(mFingerprintManager, TYPE_HOME_BUTTON, 1); assertThat(mFingerprintRepository.canAssumeUdfps()).isFalse(); + assertThat(mFingerprintRepository.canAssumeSfps()).isFalse(); } @Test public void testGetMaxFingerprints() { - setupFingerprintFirstSensor(mFingerprintManager, TYPE_UNKNOWN, 44); - assertThat(mFingerprintRepository.getMaxFingerprints()).isEqualTo(44); - setupFingerprintFirstSensor(mFingerprintManager, TYPE_UNKNOWN, 999); assertThat(mFingerprintRepository.getMaxFingerprints()).isEqualTo(999); } @@ -122,4 +143,31 @@ public class FingerprintRepositoryTest { "suw_max_fingerprints_enrollable"); when(mockedResources.getInteger(resId)).thenReturn(numOfFp); } + + public static void setupFingerprintFirstSensor( + @NonNull FingerprintManager mockedFingerprintManager, + @FingerprintSensorProperties.SensorType int sensorType, + int maxEnrollmentsPerUser) { + + final ArrayList props = new ArrayList<>(); + props.add(new FingerprintSensorPropertiesInternal( + 0 /* sensorId */, + SensorProperties.STRENGTH_STRONG, + maxEnrollmentsPerUser, + new ArrayList<>() /* componentInfo */, + sensorType, + true /* resetLockoutRequiresHardwareAuthToken */)); + when(mockedFingerprintManager.getSensorPropertiesInternal()).thenReturn(props); + } + + public static void setupFingerprintEnrolledFingerprints( + @NonNull FingerprintManager mockedFingerprintManager, + int userId, + int enrolledFingerprints) { + final ArrayList ret = new ArrayList<>(); + for (int i = 0; i < enrolledFingerprints; ++i) { + ret.add(new Fingerprint("name", 0, 0, 0L)); + } + when(mockedFingerprintManager.getEnrolledFingerprints(userId)).thenReturn(ret); + } } diff --git a/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModelTest.java b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModelTest.java index 4db005ec609..024607d7381 100644 --- a/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModelTest.java +++ b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModelTest.java @@ -45,6 +45,7 @@ import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_G import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.when; import android.annotation.NonNull; @@ -71,6 +72,8 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import java.util.concurrent.atomic.AtomicBoolean; + @RunWith(AndroidJUnit4.class) public class AutoCredentialViewModelTest { @@ -299,13 +302,18 @@ public class AutoCredentialViewModelTest { when(mLockPatternUtils.verifyGatekeeperPasswordHandle(gkPwHandle, newChallenge, userId)) .thenReturn(newGoodCredential(gkPwHandle, new byte[] { 1 })); + final AtomicBoolean hasCalledRemoveGkPwHandle = new AtomicBoolean(); + doAnswer(invocation -> { + hasCalledRemoveGkPwHandle.set(true); + return null; + }).when(mLockPatternUtils).removeGatekeeperPasswordHandle(gkPwHandle); + // Run credential check @CredentialAction final int action = mViewModel.checkCredential(); // Check viewModel behavior assertThat(action).isEqualTo(CREDENTIAL_IS_GENERATING_CHALLENGE); assertThat(mViewModel.getGenerateChallengeFailedLiveData().getValue()).isNull(); - assertThat(mChallengeGenerator.mCallbackRunCount).isEqualTo(1); // Check data inside CredentialModel final Bundle extras = mViewModel.createCredentialIntentExtra(); @@ -314,6 +322,8 @@ public class AutoCredentialViewModelTest { assertThat(extras.getByteArray(EXTRA_KEY_CHALLENGE_TOKEN)).isNotNull(); assertThat(extras.getLong(EXTRA_KEY_GK_PW_HANDLE)).isEqualTo(INVALID_GK_PW_HANDLE); assertThat(extras.getLong(EXTRA_KEY_CHALLENGE)).isNotEqualTo(INVALID_CHALLENGE); + assertThat(mChallengeGenerator.mCallbackRunCount).isEqualTo(1); + assertThat(hasCalledRemoveGkPwHandle.get()).isFalse(); // Check createGeneratingChallengeExtras() final Bundle generatingChallengeExtras = mViewModel.createGeneratingChallengeExtras(); @@ -511,6 +521,12 @@ public class AutoCredentialViewModelTest { when(mLockPatternUtils.verifyGatekeeperPasswordHandle(gkPwHandle, newChallenge, userId)) .thenReturn(newGoodCredential(gkPwHandle, new byte[] { 1 })); + final AtomicBoolean hasCalledRemoveGkPwHandle = new AtomicBoolean(); + doAnswer(invocation -> { + hasCalledRemoveGkPwHandle.set(true); + return null; + }).when(mLockPatternUtils).removeGatekeeperPasswordHandle(gkPwHandle); + // Run checkNewCredentialFromActivityResult() final Intent intent = new Intent().putExtra(EXTRA_KEY_GK_PW_HANDLE, gkPwHandle); final boolean ret = mViewModel.checkNewCredentialFromActivityResult(true, @@ -524,6 +540,7 @@ public class AutoCredentialViewModelTest { assertThat(extras.getByteArray(EXTRA_KEY_CHALLENGE_TOKEN)).isNotNull(); assertThat(extras.getLong(EXTRA_KEY_GK_PW_HANDLE)).isEqualTo(INVALID_GK_PW_HANDLE); assertThat(mChallengeGenerator.mCallbackRunCount).isEqualTo(1); + assertThat(hasCalledRemoveGkPwHandle.get()).isTrue(); } @Test @@ -541,6 +558,12 @@ public class AutoCredentialViewModelTest { when(mLockPatternUtils.verifyGatekeeperPasswordHandle(gkPwHandle, newChallenge, userId)) .thenReturn(newGoodCredential(gkPwHandle, new byte[] { 1 })); + final AtomicBoolean hasCalledRemoveGkPwHandle = new AtomicBoolean(); + doAnswer(invocation -> { + hasCalledRemoveGkPwHandle.set(true); + return null; + }).when(mLockPatternUtils).removeGatekeeperPasswordHandle(gkPwHandle); + // Run checkNewCredentialFromActivityResult() final Intent intent = new Intent().putExtra(EXTRA_KEY_GK_PW_HANDLE, gkPwHandle); final boolean ret = mViewModel.checkNewCredentialFromActivityResult(false, @@ -554,6 +577,7 @@ public class AutoCredentialViewModelTest { assertThat(extras.getByteArray(EXTRA_KEY_CHALLENGE_TOKEN)).isNotNull(); assertThat(extras.getLong(EXTRA_KEY_GK_PW_HANDLE)).isEqualTo(INVALID_GK_PW_HANDLE); assertThat(mChallengeGenerator.mCallbackRunCount).isEqualTo(1); + assertThat(hasCalledRemoveGkPwHandle.get()).isTrue(); } public static class TestChallengeGenerator implements ChallengeGenerator { diff --git a/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModelTest.java b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModelTest.java index a9536cffcbf..192a3050088 100644 --- a/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModelTest.java +++ b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModelTest.java @@ -20,6 +20,8 @@ import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_REAR import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_OPTICAL; import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC; +import static com.android.settings.biometrics2.data.repository.FingerprintRepositoryTest.setupFingerprintEnrolledFingerprints; +import static com.android.settings.biometrics2.data.repository.FingerprintRepositoryTest.setupFingerprintFirstSensor; import static com.android.settings.biometrics2.data.repository.FingerprintRepositoryTest.setupSuwMaxFingerprintsEnrollable; import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX; import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_OK; @@ -32,8 +34,6 @@ import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newIsS import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newIsSuwPortalRequest; import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newIsSuwRequest; import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newIsSuwSuggestedActionFlowRequest; -import static com.android.settings.biometrics2.util.FingerprintManagerUtil.setupFingerprintEnrolledFingerprints; -import static com.android.settings.biometrics2.util.FingerprintManagerUtil.setupFingerprintFirstSensor; import static com.google.common.truth.Truth.assertThat; @@ -134,26 +134,47 @@ public class FingerprintEnrollIntroViewModelTest { } @Test - public void testOnStartToUpdateEnrollableStatus_isNotSuw() { - testOnStartToUpdateEnrollableStatus(newAllFalseRequest(mApplication)); + public void testOnStartToUpdateEnrollableStatusOk_isNotSuw() { + testOnStartToUpdateEnrollableStatusOk(newAllFalseRequest(mApplication)); } @Test - public void testOnStartToUpdateEnrollableStatus_isSuwDeferred() { - testOnStartToUpdateEnrollableStatus(newIsSuwDeferredRequest(mApplication)); + public void testOnStartToUpdateEnrollableStatusReachMax_isNotSuw() { + testOnStartToUpdateEnrollableStatusReachMax(newAllFalseRequest(mApplication)); } @Test - public void testOnStartToUpdateEnrollableStatus_isSuwPortal() { - testOnStartToUpdateEnrollableStatus(newIsSuwPortalRequest(mApplication)); + public void testOnStartToUpdateEnrollableStatusOk_isSuwDeferred() { + testOnStartToUpdateEnrollableStatusOk(newIsSuwDeferredRequest(mApplication)); } @Test - public void testOnStartToUpdateEnrollableStatus_isSuwSuggestedActionFlow() { - testOnStartToUpdateEnrollableStatus(newIsSuwSuggestedActionFlowRequest(mApplication)); + public void testOnStartToUpdateEnrollableStatusReachMax_isSuwDeferred() { + testOnStartToUpdateEnrollableStatusReachMax(newIsSuwDeferredRequest(mApplication)); } - private void testOnStartToUpdateEnrollableStatus(@NonNull EnrollmentRequest request) { + @Test + public void testOnStartToUpdateEnrollableStatusOk_isSuwPortal() { + testOnStartToUpdateEnrollableStatusOk(newIsSuwPortalRequest(mApplication)); + } + + @Test + public void testOnStartToUpdateEnrollableStatusReachMax_isSuwPortal() { + testOnStartToUpdateEnrollableStatusReachMax(newIsSuwPortalRequest(mApplication)); + } + + @Test + public void testOnStartToUpdateEnrollableStatusOk_isSuwSuggestedActionFlow() { + testOnStartToUpdateEnrollableStatusOk(newIsSuwSuggestedActionFlowRequest(mApplication)); + } + + @Test + public void testOnStartToUpdateEnrollableStatusReachMax_isSuwSuggestedActionFlow() { + testOnStartToUpdateEnrollableStatusReachMax( + newIsSuwSuggestedActionFlowRequest(mApplication)); + } + + private void testOnStartToUpdateEnrollableStatusOk(@NonNull EnrollmentRequest request) { final int userId = 45; mViewModel.setUserId(userId); mViewModel.setEnrollmentRequest(request); @@ -163,19 +184,28 @@ public class FingerprintEnrollIntroViewModelTest { mViewModel.onStart(mLifecycleOwner); FingerprintEnrollIntroStatus status = mViewModel.getPageStatusLiveData().getValue(); assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_OK); + } + + private void testOnStartToUpdateEnrollableStatusReachMax(@NonNull EnrollmentRequest request) { + final int userId = 45; + mViewModel.setUserId(userId); + mViewModel.setEnrollmentRequest(request); setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, 5); setupFingerprintFirstSensor(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5); mViewModel.onStart(mLifecycleOwner); - status = mViewModel.getPageStatusLiveData().getValue(); + FingerprintEnrollIntroStatus status = mViewModel.getPageStatusLiveData().getValue(); assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX); } @Test - public void textCanAssumeUdfps() { + public void textCanAssumeUdfps_forUdfpsUltrasonicSensor() { setupFingerprintFirstSensor(mFingerprintManager, TYPE_UDFPS_ULTRASONIC, 1); assertThat(mViewModel.canAssumeUdfps()).isEqualTo(true); + } + @Test + public void textCanAssumeUdfps_forRearSensor() { setupFingerprintFirstSensor(mFingerprintManager, TYPE_REAR, 1); assertThat(mViewModel.canAssumeUdfps()).isEqualTo(false); } @@ -238,7 +268,7 @@ public class FingerprintEnrollIntroViewModelTest { assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_OK); // Perform click on `next` - mViewModel.onNextButtonClick(null); + mViewModel.onNextButtonClick(); assertThat(mViewModel.getActionLiveData().getValue()) .isEqualTo(FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL); @@ -258,7 +288,7 @@ public class FingerprintEnrollIntroViewModelTest { assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX); // Perform click on `next` - mViewModel.onNextButtonClick(null); + mViewModel.onNextButtonClick(); assertThat(mViewModel.getActionLiveData().getValue()) .isEqualTo(FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH); @@ -266,7 +296,7 @@ public class FingerprintEnrollIntroViewModelTest { @Test public void testOnSkipOrCancelButtonClick() { - mViewModel.onSkipOrCancelButtonClick(null); + mViewModel.onSkipOrCancelButtonClick(); assertThat(mViewModel.getActionLiveData().getValue()) .isEqualTo(FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL); diff --git a/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModelTest.java b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModelTest.java index b1d55aa332d..736e1ae5b39 100644 --- a/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModelTest.java +++ b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModelTest.java @@ -20,10 +20,10 @@ import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHE import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_SKIP; import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_TIMEOUT; import static com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollIntroduction.EXTRA_FINGERPRINT_ENROLLED_COUNT; +import static com.android.settings.biometrics2.data.repository.FingerprintRepositoryTest.setupFingerprintEnrolledFingerprints; import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollmentViewModel.SAVED_STATE_IS_WAITING_ACTIVITY_RESULT; import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newAllFalseRequest; import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newIsSuwRequest; -import static com.android.settings.biometrics2.util.FingerprintManagerUtil.setupFingerprintEnrolledFingerprints; import static com.google.common.truth.Truth.assertThat; diff --git a/tests/unit/src/com/android/settings/biometrics2/util/FingerprintManagerUtil.java b/tests/unit/src/com/android/settings/biometrics2/util/FingerprintManagerUtil.java deleted file mode 100644 index cb45fa4c6c2..00000000000 --- a/tests/unit/src/com/android/settings/biometrics2/util/FingerprintManagerUtil.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (C) 2022 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.util; - -import static org.mockito.Mockito.when; - -import android.hardware.biometrics.SensorProperties; -import android.hardware.fingerprint.Fingerprint; -import android.hardware.fingerprint.FingerprintManager; -import android.hardware.fingerprint.FingerprintSensorProperties; -import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; - -import androidx.annotation.NonNull; - -import java.util.ArrayList; - -public class FingerprintManagerUtil { - - public static void setupFingerprintFirstSensor( - @NonNull FingerprintManager mockedFingerprintManager, - @FingerprintSensorProperties.SensorType int sensorType, - int maxEnrollmentsPerUser) { - final ArrayList props = new ArrayList<>(); - props.add(new FingerprintSensorPropertiesInternal( - 0 /* sensorId */, - SensorProperties.STRENGTH_STRONG, - maxEnrollmentsPerUser, - new ArrayList<>() /* componentInfo */, - sensorType, - true /* resetLockoutRequiresHardwareAuthToken */)); - when(mockedFingerprintManager.getSensorPropertiesInternal()).thenReturn(props); - } - - public static void setupFingerprintEnrolledFingerprints( - @NonNull FingerprintManager mockedFingerprintManager, - int userId, - int enrolledFingerprints) { - final ArrayList ret = new ArrayList<>(); - for (int i = 0; i < enrolledFingerprints; ++i) { - ret.add(new Fingerprint("name", 0, 0, 0L)); - } - when(mockedFingerprintManager.getEnrolledFingerprints(userId)).thenReturn(ret); - } -}