diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 5d713b02126..d58006497b8 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -2384,6 +2384,11 @@
+
+
+
\ No newline at end of file
diff --git a/src/com/android/settings/activityembedding/ActivityEmbeddingRulesController.java b/src/com/android/settings/activityembedding/ActivityEmbeddingRulesController.java
index ad0d4eab03f..0bd9996d4f2 100644
--- a/src/com/android/settings/activityembedding/ActivityEmbeddingRulesController.java
+++ b/src/com/android/settings/activityembedding/ActivityEmbeddingRulesController.java
@@ -39,6 +39,7 @@ import com.android.settings.SubSettings;
import com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling;
import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroduction;
import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroductionInternal;
+import com.android.settings.biometrics2.ui.view.FingerprintEnrollmentActivity;
import com.android.settings.core.FeatureFlags;
import com.android.settings.homepage.DeepLinkHomepageActivity;
import com.android.settings.homepage.DeepLinkHomepageActivityInternal;
@@ -225,6 +226,7 @@ public class ActivityEmbeddingRulesController {
.buildSearchIntent(mContext, SettingsEnums.SETTINGS_HOMEPAGE);
addActivityFilter(activityFilters, searchIntent);
}
+ addActivityFilter(activityFilters, FingerprintEnrollmentActivity.class);
addActivityFilter(activityFilters, FingerprintEnrollIntroduction.class);
addActivityFilter(activityFilters, FingerprintEnrollIntroductionInternal.class);
addActivityFilter(activityFilters, FingerprintEnrollEnrolling.class);
diff --git a/src/com/android/settings/biometrics/BiometricUtils.java b/src/com/android/settings/biometrics/BiometricUtils.java
index f395aca0419..08c8c4f1fe3 100644
--- a/src/com/android/settings/biometrics/BiometricUtils.java
+++ b/src/com/android/settings/biometrics/BiometricUtils.java
@@ -25,6 +25,7 @@ import android.content.IntentSender;
import android.hardware.biometrics.SensorProperties;
import android.hardware.face.FaceManager;
import android.hardware.face.FaceSensorPropertiesInternal;
+import android.os.Bundle;
import android.os.storage.StorageManager;
import android.util.Log;
import android.view.Surface;
@@ -145,6 +146,31 @@ public class BiometricUtils {
}
}
+ /**
+ * @param context caller's context
+ * @param isSuw if it is running in setup wizard flows
+ * @param suwExtras setup wizard extras for new intent
+ * @return Intent for starting ChooseLock*
+ */
+ public static Intent getChooseLockIntent(@NonNull Context context,
+ boolean isSuw, @NonNull Bundle suwExtras) {
+ if (isSuw) {
+ // Default to PIN lock in setup wizard
+ Intent intent = new Intent(context, SetupChooseLockGeneric.class);
+ if (StorageManager.isFileEncrypted()) {
+ intent.putExtra(
+ LockPatternUtils.PASSWORD_TYPE_KEY,
+ DevicePolicyManager.PASSWORD_QUALITY_NUMERIC);
+ intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment
+ .EXTRA_SHOW_OPTIONS_BUTTON, true);
+ }
+ intent.putExtras(suwExtras);
+ return intent;
+ } else {
+ return new Intent(context, ChooseLockGeneric.class);
+ }
+ }
+
/**
* @param context caller's context
* @param activityIntent The intent that started the caller's activity
diff --git a/src/com/android/settings/biometrics/fingerprint/SetupFingerprintEnrollIntroduction.java b/src/com/android/settings/biometrics/fingerprint/SetupFingerprintEnrollIntroduction.java
index b31396142e3..eb686879009 100644
--- a/src/com/android/settings/biometrics/fingerprint/SetupFingerprintEnrollIntroduction.java
+++ b/src/com/android/settings/biometrics/fingerprint/SetupFingerprintEnrollIntroduction.java
@@ -32,7 +32,7 @@ public class SetupFingerprintEnrollIntroduction extends FingerprintEnrollIntrodu
/**
* Returns the number of fingerprint enrolled.
*/
- private static final String EXTRA_FINGERPRINT_ENROLLED_COUNT = "fingerprint_enrolled_count";
+ public static final String EXTRA_FINGERPRINT_ENROLLED_COUNT = "fingerprint_enrolled_count";
private static final String KEY_LOCK_SCREEN_PRESENT = "wasLockScreenPresent";
diff --git a/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java b/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java
new file mode 100644
index 00000000000..f58175af520
--- /dev/null
+++ b/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java
@@ -0,0 +1,108 @@
+/*
+ * 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.data.repository;
+
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
+
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.content.res.Resources;
+import android.hardware.fingerprint.Fingerprint;
+import android.hardware.fingerprint.FingerprintManager;
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settings.R;
+import com.android.settings.biometrics.ParentalControlsUtils;
+import com.android.settingslib.RestrictedLockUtilsInternal;
+
+import java.util.List;
+
+/**
+ * This repository is used to call all APIs in {@link FingerprintManager}
+ */
+public class FingerprintRepository {
+
+ @NonNull private final FingerprintManager mFingerprintManager;
+
+ public FingerprintRepository(@NonNull FingerprintManager fingerprintManager) {
+ mFingerprintManager = fingerprintManager;
+ }
+
+ /**
+ * The first sensor type is UDFPS sensor or not
+ */
+ public boolean canAssumeUdfps() {
+ FingerprintSensorPropertiesInternal prop = getFirstFingerprintSensorPropertiesInternal();
+ return prop != null && prop.isAnyUdfpsType();
+ }
+
+ /**
+ * Get max possible number of fingerprints for a user
+ */
+ public int getMaxFingerprints() {
+ FingerprintSensorPropertiesInternal prop = getFirstFingerprintSensorPropertiesInternal();
+ return prop != null ? prop.maxEnrollmentsPerUser : 0;
+ }
+
+ /**
+ * Get number of fingerprints that this user enrolled.
+ */
+ public int getNumOfEnrolledFingerprintsSize(int userId) {
+ final List list = mFingerprintManager.getEnrolledFingerprints(userId);
+ return list != null ? list.size() : 0;
+ }
+
+ /**
+ * Get maximum possible fingerprints in setup wizard flow
+ */
+ public int getMaxFingerprintsInSuw(@NonNull Resources resources) {
+ return resources.getInteger(R.integer.suw_max_fingerprints_enrollable);
+ }
+
+ @Nullable
+ private FingerprintSensorPropertiesInternal getFirstFingerprintSensorPropertiesInternal() {
+ final List props =
+ mFingerprintManager.getSensorPropertiesInternal();
+ return props.size() > 0 ? props.get(0) : null;
+ }
+
+ /**
+ * Call FingerprintManager to generate challenge for first sensor
+ */
+ public void generateChallenge(int userId,
+ @NonNull FingerprintManager.GenerateChallengeCallback callback) {
+ mFingerprintManager.generateChallenge(userId, callback);
+ }
+
+ /**
+ * Get parental consent required or not during enrollment process
+ */
+ public boolean isParentalConsentRequired(@NonNull Context context) {
+ return ParentalControlsUtils.parentConsentRequired(context, TYPE_FINGERPRINT) != null;
+ }
+
+ /**
+ * Get fingerprint is disable by admin or not
+ */
+ public boolean isDisabledByAdmin(@NonNull Context context, int userId) {
+ return RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled(
+ context, DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT, userId) != null;
+ }
+}
diff --git a/src/com/android/settings/biometrics2/factory/BiometricsFragmentFactory.java b/src/com/android/settings/biometrics2/factory/BiometricsFragmentFactory.java
new file mode 100644
index 00000000000..9a0cab21d5a
--- /dev/null
+++ b/src/com/android/settings/biometrics2/factory/BiometricsFragmentFactory.java
@@ -0,0 +1,57 @@
+/*
+ * 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.factory;
+
+import android.app.Application;
+import android.app.admin.DevicePolicyManager;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentFactory;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.android.settings.biometrics2.ui.view.FingerprintEnrollIntroFragment;
+
+/**
+ * Fragment factory for biometrics
+ */
+public class BiometricsFragmentFactory extends FragmentFactory {
+
+ private final Application mApplication;
+ private final ViewModelProvider mViewModelProvider;
+
+ public BiometricsFragmentFactory(Application application,
+ ViewModelProvider viewModelProvider) {
+ mApplication = application;
+ mViewModelProvider = viewModelProvider;
+ }
+
+ @NonNull
+ @Override
+ public Fragment instantiate(@NonNull ClassLoader classLoader, @NonNull String className) {
+ final Class extends Fragment> clazz = loadFragmentClass(classLoader, className);
+ if (FingerprintEnrollIntroFragment.class.equals(clazz)) {
+ final DevicePolicyManager devicePolicyManager =
+ mApplication.getSystemService(DevicePolicyManager.class);
+ if (devicePolicyManager != null) {
+ return new FingerprintEnrollIntroFragment(mViewModelProvider,
+ devicePolicyManager.getResources());
+ }
+ }
+ return super.instantiate(classLoader, className);
+ }
+}
diff --git a/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProvider.java b/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProvider.java
new file mode 100644
index 00000000000..fdc5745e926
--- /dev/null
+++ b/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProvider.java
@@ -0,0 +1,36 @@
+/*
+ * 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.factory;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+
+/**
+ * Interface for BiometricsRepositoryProvider
+ */
+public interface BiometricsRepositoryProvider {
+
+ /**
+ * Get FingerprintRepository
+ */
+ @Nullable
+ FingerprintRepository getFingerprintRepository(@NonNull Application application);
+}
diff --git a/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProviderImpl.java b/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProviderImpl.java
new file mode 100644
index 00000000000..87b41e93080
--- /dev/null
+++ b/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProviderImpl.java
@@ -0,0 +1,46 @@
+/*
+ * 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.factory;
+
+import android.app.Application;
+import android.hardware.fingerprint.FingerprintManager;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settings.Utils;
+import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+
+/**
+ * Implementation for BiometricsRepositoryProvider
+ */
+public class BiometricsRepositoryProviderImpl implements BiometricsRepositoryProvider {
+
+ /**
+ * Get FingerprintRepository
+ */
+ @Nullable
+ @Override
+ public FingerprintRepository getFingerprintRepository(@NonNull Application application) {
+ final FingerprintManager fingerprintManager =
+ Utils.getFingerprintManagerOrNull(application);
+ if (fingerprintManager == null) {
+ return null;
+ }
+ return new FingerprintRepository(fingerprintManager);
+ }
+}
diff --git a/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java b/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java
new file mode 100644
index 00000000000..477fdb6971c
--- /dev/null
+++ b/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java
@@ -0,0 +1,84 @@
+/*
+ * 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.factory;
+
+import android.app.Application;
+import android.app.KeyguardManager;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory;
+import androidx.lifecycle.viewmodel.CreationExtras;
+
+import com.android.internal.widget.LockPatternUtils;
+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.FingerprintEnrollIntroViewModel;
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollmentViewModel;
+import com.android.settings.overlay.FeatureFactory;
+
+/**
+ * View model factory for biometric enrollment fragment
+ */
+public class BiometricsViewModelFactory implements ViewModelProvider.Factory {
+
+ private static final String TAG = "BiometricsViewModelFact";
+
+ public static final CreationExtras.Key CHALLENGE_GENERATOR =
+ new CreationExtras.Key<>() {};
+
+ @NonNull
+ @Override
+ @SuppressWarnings("unchecked")
+ public T create(@NonNull Class modelClass,
+ @NonNull CreationExtras extras) {
+ final Application application = extras.get(AndroidViewModelFactory.APPLICATION_KEY);
+
+ if (application == null) {
+ Log.w(TAG, "create, null application");
+ return create(modelClass);
+ }
+ final FeatureFactory featureFactory = FeatureFactory.getFactory(application);
+ final BiometricsRepositoryProvider provider = FeatureFactory.getFactory(application)
+ .getBiometricsRepositoryProvider();
+
+ if (modelClass.isAssignableFrom(FingerprintEnrollIntroViewModel.class)) {
+ final FingerprintRepository repository = provider.getFingerprintRepository(application);
+ if (repository != null) {
+ return (T) new FingerprintEnrollIntroViewModel(application, repository);
+ }
+ } else if (modelClass.isAssignableFrom(FingerprintEnrollmentViewModel.class)) {
+ final FingerprintRepository repository = provider.getFingerprintRepository(application);
+ if (repository != null) {
+ 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);
+ }
+ }
+ return create(modelClass);
+ }
+}
diff --git a/src/com/android/settings/biometrics2/ui/model/CredentialModel.java b/src/com/android/settings/biometrics2/ui/model/CredentialModel.java
new file mode 100644
index 00000000000..06caf5e6945
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/model/CredentialModel.java
@@ -0,0 +1,202 @@
+/*
+ * 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;
+
+import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_CHALLENGE;
+import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_SENSOR_ID;
+import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN;
+import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE;
+
+import android.content.Intent;
+import android.os.UserHandle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import java.time.Clock;
+
+/**
+ * Secret credential data including
+ * 1. userId
+ * 2. sensorId
+ * 3. challenge
+ * 4. token
+ * 5. gkPwHandle
+ */
+public final class CredentialModel {
+
+ /**
+ * Default value for an invalid challenge
+ */
+ @VisibleForTesting
+ public static final long INVALID_CHALLENGE = -1L;
+
+ /**
+ * Default value if GkPwHandle is invalid.
+ */
+ public static final long INVALID_GK_PW_HANDLE = 0L;
+
+ /**
+ * Default value for a invalid sensor id
+ */
+ @VisibleForTesting
+ public static final int INVALID_SENSOR_ID = -1;
+
+ private final Clock mClock;
+
+ private final long mInitMillis;
+
+ private final int mUserId;
+
+ private int mSensorId;
+ @Nullable
+ private Long mUpdateSensorIdMillis = null;
+
+ private long mChallenge;
+ @Nullable
+ private Long mUpdateChallengeMillis = null;
+
+ @Nullable
+ private byte[] mToken;
+ @Nullable
+ private Long mUpdateTokenMillis = null;
+
+ private long mGkPwHandle;
+ @Nullable
+ private Long mClearGkPwHandleMillis = null;
+
+ public CredentialModel(@NonNull Intent intent, @NonNull Clock clock) {
+ mUserId = intent.getIntExtra(Intent.EXTRA_USER_ID, UserHandle.myUserId());
+ mSensorId = intent.getIntExtra(EXTRA_KEY_SENSOR_ID, INVALID_SENSOR_ID);
+ mChallenge = intent.getLongExtra(EXTRA_KEY_CHALLENGE, INVALID_CHALLENGE);
+ mToken = intent.getByteArrayExtra(EXTRA_KEY_CHALLENGE_TOKEN);
+ mGkPwHandle = intent.getLongExtra(EXTRA_KEY_GK_PW_HANDLE,
+ INVALID_GK_PW_HANDLE);
+ mClock = clock;
+ mInitMillis = mClock.millis();
+ }
+
+ /**
+ * Get userId for this credential
+ */
+ public int getUserId() {
+ return mUserId;
+ }
+
+ /**
+ * Check user id is valid or not
+ */
+ public static boolean isValidUserId(int userId) {
+ return userId != UserHandle.USER_NULL;
+ }
+
+ /**
+ * Get challenge
+ */
+ public long getChallenge() {
+ return mChallenge;
+ }
+
+ /**
+ * Set challenge
+ */
+ public void setChallenge(long value) {
+ mUpdateChallengeMillis = mClock.millis();
+ mChallenge = value;
+ }
+
+ /**
+ * Get challenge token
+ */
+ @Nullable
+ public byte[] getToken() {
+ return mToken;
+ }
+
+ /**
+ * Set challenge token
+ */
+ public void setToken(@Nullable byte[] value) {
+ mUpdateTokenMillis = mClock.millis();
+ mToken = value;
+ }
+
+ /**
+ * Check challengeToken is valid or not
+ */
+ public static boolean isValidToken(@Nullable byte[] token) {
+ return token != null;
+ }
+
+ /**
+ * Get gatekeeper password handle
+ */
+ public long getGkPwHandle() {
+ return mGkPwHandle;
+ }
+
+ /**
+ * Clear gatekeeper password handle data
+ */
+ public void clearGkPwHandle() {
+ mClearGkPwHandleMillis = mClock.millis();
+ mGkPwHandle = INVALID_GK_PW_HANDLE;
+ }
+
+ /**
+ * Check gkPwHandle is valid or not
+ */
+ public static boolean isValidGkPwHandle(long gkPwHandle) {
+ return gkPwHandle != INVALID_GK_PW_HANDLE;
+ }
+
+ /**
+ * Get sensor id
+ */
+ public int getSensorId() {
+ return mSensorId;
+ }
+
+ /**
+ * Set sensor id
+ */
+ public void setSensorId(int value) {
+ mUpdateSensorIdMillis = mClock.millis();
+ mSensorId = value;
+ }
+
+ /**
+ * Returns a string representation of the object
+ */
+ @Override
+ public String toString() {
+ final int gkPwHandleLen = ("" + mGkPwHandle).length();
+ final int tokenLen = mToken == null ? 0 : mToken.length;
+ final int challengeLen = ("" + mChallenge).length();
+ return getClass().getSimpleName() + ":{initMillis:" + mInitMillis
+ + ", userId:" + mUserId
+ + ", challenge:{len:" + challengeLen
+ + ", updateMillis:" + mUpdateChallengeMillis + "}"
+ + ", token:{len:" + tokenLen + ", isValid:" + isValidToken(mToken)
+ + ", updateMillis:" + mUpdateTokenMillis + "}"
+ + ", gkPwHandle:{len:" + gkPwHandleLen + ", isValid:"
+ + isValidGkPwHandle(mGkPwHandle) + ", clearMillis:" + mClearGkPwHandleMillis + "}"
+ + ", mSensorId:{id:" + mSensorId + ", updateMillis:" + mUpdateSensorIdMillis + "}"
+ + " }";
+ }
+}
diff --git a/src/com/android/settings/biometrics2/ui/model/EnrollmentRequest.java b/src/com/android/settings/biometrics2/ui/model/EnrollmentRequest.java
new file mode 100644
index 00000000000..de8526a1657
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/model/EnrollmentRequest.java
@@ -0,0 +1,99 @@
+/*
+ * 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;
+
+import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_FROM_SETTINGS_SUMMARY;
+
+import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_SUW_SUGGESTED_ACTION_FLOW;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+
+import com.android.settings.SetupWizardUtils;
+
+import com.google.android.setupcompat.util.WizardManagerHelper;
+
+/**
+ * Biometric enrollment generic intent data, which includes
+ * 1. isSuw
+ * 2. isAfterSuwOrSuwSuggestedAction
+ * 3. theme
+ * 4. isFromSettingsSummery
+ * 5. a helper method, getSetupWizardExtras
+ */
+public final class EnrollmentRequest {
+
+ private final boolean mIsSuw;
+ private final boolean mIsAfterSuwOrSuwSuggestedAction;
+ private final boolean mIsFromSettingsSummery;
+ private final int mTheme;
+ private final Bundle mSuwExtras;
+
+ public EnrollmentRequest(@NonNull Intent intent, @NonNull Context context) {
+ mIsSuw = WizardManagerHelper.isAnySetupWizard(intent);
+ mIsAfterSuwOrSuwSuggestedAction = WizardManagerHelper.isDeferredSetupWizard(intent)
+ || WizardManagerHelper.isPortalSetupWizard(intent)
+ || intent.getBooleanExtra(EXTRA_IS_SUW_SUGGESTED_ACTION_FLOW, false);
+ mSuwExtras = getSuwExtras(mIsSuw, intent);
+ mIsFromSettingsSummery = intent.getBooleanExtra(EXTRA_FROM_SETTINGS_SUMMARY, false);
+ mTheme = SetupWizardUtils.getTheme(context, intent);
+ }
+
+ public boolean isSuw() {
+ return mIsSuw;
+ }
+
+ public boolean isAfterSuwOrSuwSuggestedAction() {
+ return mIsAfterSuwOrSuwSuggestedAction;
+ }
+
+ public boolean isFromSettingsSummery() {
+ return mIsFromSettingsSummery;
+ }
+
+ public int getTheme() {
+ return mTheme;
+ }
+
+ @NonNull
+ public Bundle getSuwExtras() {
+ return new Bundle(mSuwExtras);
+ }
+
+ /**
+ * Returns a string representation of the object
+ */
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + ":{isSuw:" + mIsSuw
+ + ", isAfterSuwOrSuwSuggestedAction:" + mIsAfterSuwOrSuwSuggestedAction
+ + ", isFromSettingsSummery:" + mIsFromSettingsSummery
+ + "}";
+ }
+
+ @NonNull
+ private static Bundle getSuwExtras(boolean isSuw, @NonNull Intent intent) {
+ final Intent toIntent = new Intent();
+ if (isSuw) {
+ SetupWizardUtils.copySetupExtras(intent, toIntent);
+ }
+ return toIntent.getExtras() != null ? toIntent.getExtras() : new Bundle();
+ }
+}
diff --git a/src/com/android/settings/biometrics2/ui/model/FingerprintEnrollIntroStatus.java b/src/com/android/settings/biometrics2/ui/model/FingerprintEnrollIntroStatus.java
new file mode 100644
index 00000000000..b5e462e5048
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/model/FingerprintEnrollIntroStatus.java
@@ -0,0 +1,81 @@
+/*
+ * 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;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Fingerprint onboarding introduction page data, it contains following information which needs
+ * to be passed from view model to view.
+ * 1. mEnrollableStatus: User is allowed to enroll a new fingerprint or not.
+ * 2. mHasScrollToBottom: User has scrolled to the bottom of this page or not.
+ */
+public final class FingerprintEnrollIntroStatus {
+
+ /**
+ * Unconfirmed case, it means that this value is invalid, and view shall bypass this value.
+ */
+ public static final int FINGERPRINT_ENROLLABLE_UNKNOWN = -1;
+
+ /**
+ * User is allowed to enrolled a new fingerprint.
+ */
+ public static final int FINGERPRINT_ENROLLABLE_OK = 0;
+
+ /**
+ * User is not allowed to enrolled a new fingerprint because the number of enrolled fingerprint
+ * has reached maximum.
+ */
+ public static final int FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX = 1;
+
+ @IntDef(prefix = {"FINGERPRINT_ENROLLABLE_"}, value = {
+ FINGERPRINT_ENROLLABLE_UNKNOWN,
+ FINGERPRINT_ENROLLABLE_OK,
+ FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface FingerprintEnrollableStatus {
+ }
+
+ private final boolean mHasScrollToBottom;
+
+ @FingerprintEnrollableStatus
+ private final int mEnrollableStatus;
+
+ public FingerprintEnrollIntroStatus(boolean hasScrollToBottom, int enrollableStatus) {
+ mEnrollableStatus = enrollableStatus;
+ mHasScrollToBottom = hasScrollToBottom;
+ }
+
+ /**
+ * Get enrollable status. It means that user is allowed to enroll a new fingerprint or not.
+ */
+ @FingerprintEnrollableStatus
+ public int getEnrollableStatus() {
+ return mEnrollableStatus;
+ }
+
+ /**
+ * Get info for this onboarding introduction page has scrolled to bottom or not
+ */
+ public boolean hasScrollToBottom() {
+ return mHasScrollToBottom;
+ }
+}
diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollIntroFragment.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollIntroFragment.java
new file mode 100644
index 00000000000..e788da5341a
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollIntroFragment.java
@@ -0,0 +1,288 @@
+/*
+ * 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.app.admin.DevicePolicyResources.Strings.Settings.FINGERPRINT_UNLOCK_DISABLED;
+
+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;
+import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_UNKNOWN;
+
+import static com.google.android.setupdesign.util.DynamicColorPalette.ColorType.ACCENT;
+
+import android.app.Activity;
+import android.app.admin.DevicePolicyResourcesManager;
+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.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.android.settings.R;
+import com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus;
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel;
+
+import com.google.android.setupcompat.template.FooterBarMixin;
+import com.google.android.setupcompat.template.FooterButton;
+import com.google.android.setupdesign.GlifLayout;
+import com.google.android.setupdesign.template.RequireScrollMixin;
+import com.google.android.setupdesign.util.DynamicColorPalette;
+
+/**
+ * Fingerprint intro onboarding page fragment implementation
+ */
+public class FingerprintEnrollIntroFragment extends Fragment {
+
+ private static final String TAG = "FingerprintEnrollIntroFragment";
+
+ @NonNull private final ViewModelProvider mViewModelProvider;
+ @Nullable private final DevicePolicyResourcesManager mDevicePolicyMgrRes;
+
+ private FingerprintEnrollIntroViewModel mViewModel = null;
+
+ private View mView = null;
+ private FooterButton mPrimaryFooterButton = null;
+ private FooterButton mSecondaryFooterButton = null;
+ private ImageView mIconShield = null;
+ private TextView mFooterMessage6 = null;
+ @Nullable private PorterDuffColorFilter mIconColorFilter;
+
+ public FingerprintEnrollIntroFragment(
+ @NonNull ViewModelProvider viewModelProvider,
+ @Nullable DevicePolicyResourcesManager devicePolicyMgrRes) {
+ super();
+ mViewModelProvider = viewModelProvider;
+ mDevicePolicyMgrRes = devicePolicyMgrRes;
+ }
+
+ @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_introduction, container);
+
+ final ImageView iconFingerprint = mView.findViewById(R.id.icon_fingerprint);
+ final ImageView iconDeviceLocked = mView.findViewById(R.id.icon_device_locked);
+ final ImageView iconTrashCan = mView.findViewById(R.id.icon_trash_can);
+ final ImageView iconInfo = mView.findViewById(R.id.icon_info);
+ mIconShield = mView.findViewById(R.id.icon_shield);
+ final ImageView iconLink = mView.findViewById(R.id.icon_link);
+ iconFingerprint.getDrawable().setColorFilter(getIconColorFilter(context));
+ iconDeviceLocked.getDrawable().setColorFilter(getIconColorFilter(context));
+ iconTrashCan.getDrawable().setColorFilter(getIconColorFilter(context));
+ iconInfo.getDrawable().setColorFilter(getIconColorFilter(context));
+ mIconShield.getDrawable().setColorFilter(getIconColorFilter(context));
+ iconLink.getDrawable().setColorFilter(getIconColorFilter(context));
+
+ final TextView footerMessage2 = mView.findViewById(R.id.footer_message_2);
+ final TextView footerMessage3 = mView.findViewById(R.id.footer_message_3);
+ final TextView footerMessage4 = mView.findViewById(R.id.footer_message_4);
+ final TextView footerMessage5 = mView.findViewById(R.id.footer_message_5);
+ mFooterMessage6 = mView.findViewById(R.id.footer_message_6);
+ footerMessage2.setText(
+ R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_2);
+ footerMessage3.setText(
+ R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_3);
+ footerMessage4.setText(
+ R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_4);
+ footerMessage5.setText(
+ R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_5);
+ mFooterMessage6.setText(
+ R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_6);
+
+ final TextView footerTitle1 = mView.findViewById(R.id.footer_title_1);
+ final TextView footerTitle2 = mView.findViewById(R.id.footer_title_2);
+ footerTitle1.setText(
+ R.string.security_settings_fingerprint_enroll_introduction_footer_title_1);
+ footerTitle2.setText(
+ R.string.security_settings_fingerprint_enroll_introduction_footer_title_2);
+
+ return mView;
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ final Context context = view.getContext();
+
+ final TextView footerLink = mView.findViewById(R.id.footer_learn_more);
+ footerLink.setMovementMethod(LinkMovementMethod.getInstance());
+ final String footerLinkStr = getContext().getString(
+ R.string.security_settings_fingerprint_v2_enroll_introduction_message_learn_more,
+ Html.FROM_HTML_MODE_LEGACY);
+ footerLink.setText(Html.fromHtml(footerLinkStr));
+
+ // footer buttons
+ mPrimaryFooterButton = new FooterButton.Builder(context)
+ .setText(R.string.security_settings_fingerprint_enroll_introduction_agree)
+ .setListener(mViewModel::onNextButtonClick)
+ .setButtonType(FooterButton.ButtonType.OPT_IN)
+ .setTheme(R.style.SudGlifButton_Primary)
+ .build();
+ mSecondaryFooterButton = new FooterButton.Builder(context)
+ .setListener(mViewModel::onSkipOrCancelButtonClick)
+ .setButtonType(FooterButton.ButtonType.NEXT)
+ .setTheme(R.style.SudGlifButton_Primary)
+ .build();
+ getFooterBarMixin().setPrimaryButton(mPrimaryFooterButton);
+ getFooterBarMixin().setSecondaryButton(mSecondaryFooterButton, true /* usePrimaryStyle */);
+
+ if (mViewModel.canAssumeUdfps()) {
+ mFooterMessage6.setVisibility(View.VISIBLE);
+ mIconShield.setVisibility(View.VISIBLE);
+ } else {
+ mFooterMessage6.setVisibility(View.GONE);
+ mIconShield.setVisibility(View.GONE);
+ }
+ mSecondaryFooterButton.setText(getContext(),
+ mViewModel.getEnrollmentRequest().isAfterSuwOrSuwSuggestedAction()
+ ? R.string.security_settings_fingerprint_enroll_introduction_cancel
+ : R.string.security_settings_fingerprint_enroll_introduction_no_thanks);
+
+ if (mViewModel.isBiometricUnlockDisabledByAdmin()
+ && !mViewModel.isParentalConsentRequired()) {
+ setHeaderText(
+ getActivity(),
+ R.string.security_settings_fingerprint_enroll_introduction_title_unlock_disabled
+ );
+ getLayout().setDescriptionText(getDescriptionDisabledByAdmin(context));
+ } else {
+ setHeaderText(getActivity(),
+ R.string.security_settings_fingerprint_enroll_introduction_title);
+ }
+
+ mViewModel.getPageStatusLiveData().observe(this, this::updateFooterButtons);
+
+ final RequireScrollMixin requireScrollMixin = getLayout()
+ .getMixin(RequireScrollMixin.class);
+ requireScrollMixin.requireScrollWithButton(getActivity(), mPrimaryFooterButton,
+ getMoreButtonTextRes(), mViewModel::onNextButtonClick);
+ requireScrollMixin.setOnRequireScrollStateChangedListener(scrollNeeded -> {
+ if (!scrollNeeded) {
+ mViewModel.setHasScrolledToBottom();
+ }
+ });
+ }
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ mViewModel = mViewModelProvider.get(FingerprintEnrollIntroViewModel.class);
+ getLifecycle().addObserver(mViewModel);
+ super.onAttach(context);
+ }
+
+ @Override
+ public void onDetach() {
+ getLifecycle().removeObserver(mViewModel);
+ super.onDetach();
+ }
+
+ @NonNull
+ private PorterDuffColorFilter getIconColorFilter(@NonNull Context context) {
+ if (mIconColorFilter == null) {
+ mIconColorFilter = new PorterDuffColorFilter(
+ DynamicColorPalette.getColor(context, ACCENT),
+ PorterDuff.Mode.SRC_IN);
+ }
+ return mIconColorFilter;
+ }
+
+ private GlifLayout getLayout() {
+ return mView.findViewById(R.id.setup_wizard_layout);
+ }
+
+ @NonNull
+ private FooterBarMixin getFooterBarMixin() {
+ final GlifLayout layout = getLayout();
+ return layout.getMixin(FooterBarMixin.class);
+ }
+
+ @NonNull
+ private String getDescriptionDisabledByAdmin(@NonNull Context context) {
+ final int defaultStrId =
+ R.string.security_settings_fingerprint_enroll_introduction_message_unlock_disabled;
+ if (mDevicePolicyMgrRes == null) {
+ Log.w(TAG, "getDescriptionDisabledByAdmin, null device policy manager res");
+ return "";
+ }
+ return mDevicePolicyMgrRes.getString(FINGERPRINT_UNLOCK_DISABLED,
+ () -> context.getString(defaultStrId));
+ }
+
+ 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
+ ? R.string.security_settings_fingerprint_enroll_introduction_agree
+ : R.string.done;
+
+ mPrimaryFooterButton.setText(getContext(),
+ status.hasScrollToBottom() ? scrollToBottomPrimaryResId : getMoreButtonTextRes());
+ mSecondaryFooterButton.setVisibility(
+ status.hasScrollToBottom() ? View.VISIBLE : View.INVISIBLE);
+
+ final TextView errorTextView = mView.findViewById(R.id.error_text);
+ switch (status.getEnrollableStatus()) {
+ case FINGERPRINT_ENROLLABLE_OK:
+ errorTextView.setText(null);
+ errorTextView.setVisibility(View.GONE);
+ break;
+ case FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX:
+ errorTextView.setText(R.string.fingerprint_intro_error_max);
+ errorTextView.setVisibility(View.VISIBLE);
+ break;
+ case FINGERPRINT_ENROLLABLE_UNKNOWN:
+ // default case, do nothing.
+ }
+ }
+
+ @StringRes
+ private int getMoreButtonTextRes() {
+ return R.string.security_settings_face_enroll_introduction_more;
+ }
+}
diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.java
new file mode 100644
index 00000000000..e9cf6fd49b1
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.java
@@ -0,0 +1,276 @@
+/*
+ * 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 androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY;
+
+import static com.android.settings.biometrics2.factory.BiometricsViewModelFactory.CHALLENGE_GENERATOR;
+import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_FAIL_DURING_GENERATE_CHALLENGE;
+import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK;
+import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK;
+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 android.app.Activity;
+import android.app.Application;
+import android.content.Intent;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.util.Log;
+
+import androidx.activity.result.ActivityResult;
+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.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.lifecycle.viewmodel.CreationExtras;
+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.SetupFingerprintEnrollEnrolling;
+import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+import com.android.settings.biometrics2.factory.BiometricsFragmentFactory;
+import com.android.settings.biometrics2.factory.BiometricsViewModelFactory;
+import com.android.settings.biometrics2.ui.model.CredentialModel;
+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.FingerprintEnrollIntroViewModel;
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollmentViewModel;
+import com.android.settings.overlay.FeatureFactory;
+
+import com.google.android.setupdesign.util.ThemeHelper;
+
+/**
+ * Fingerprint enrollment activity implementation
+ */
+public class FingerprintEnrollmentActivity extends FragmentActivity {
+
+ private static final String TAG = "FingerprintEnrollmentActivity";
+
+ protected static final int LAUNCH_CONFIRM_LOCK_ACTIVITY = 1;
+
+ private FingerprintEnrollmentViewModel mViewModel;
+ private AutoCredentialViewModel mAutoCredentialViewModel;
+ private ActivityResultLauncher mNextActivityLauncher;
+ private ActivityResultLauncher mChooseLockLauncher;
+
+ @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)
+ );
+
+ ViewModelProvider viewModelProvider = new ViewModelProvider(this);
+
+ mViewModel = viewModelProvider.get(FingerprintEnrollmentViewModel.class);
+ mViewModel.setRequest(new EnrollmentRequest(getIntent(), getApplicationContext()));
+ mViewModel.setSavedInstanceState(savedInstanceState);
+ getLifecycle().addObserver(mViewModel);
+
+ mAutoCredentialViewModel = viewModelProvider.get(AutoCredentialViewModel.class);
+ mAutoCredentialViewModel.setCredentialModel(new CredentialModel(getIntent(),
+ SystemClock.elapsedRealtimeClock()));
+ getLifecycle().addObserver(mAutoCredentialViewModel);
+
+ mViewModel.getSetResultLiveData().observe(this, this::onSetActivityResult);
+ mAutoCredentialViewModel.getActionLiveData().observe(this, this::onCredentialAction);
+
+ // Theme
+ setTheme(mViewModel.getRequest().getTheme());
+ ThemeHelper.trySetDynamicColor(this);
+ getWindow().setStatusBarColor(android.graphics.Color.TRANSPARENT);
+
+ // fragment
+ setContentView(R.layout.biometric_enrollment_container);
+ final FragmentManager fragmentManager = getSupportFragmentManager();
+ fragmentManager.setFragmentFactory(
+ new BiometricsFragmentFactory(getApplication(), viewModelProvider));
+
+ final FingerprintEnrollIntroViewModel fingerprintEnrollIntroViewModel =
+ viewModelProvider.get(FingerprintEnrollIntroViewModel.class);
+ fingerprintEnrollIntroViewModel.setEnrollmentRequest(mViewModel.getRequest());
+ fingerprintEnrollIntroViewModel.setUserId(mAutoCredentialViewModel.getUserId());
+ fingerprintEnrollIntroViewModel.getActionLiveData().observe(
+ this, this::observeIntroAction);
+ final String tag = "FingerprintEnrollIntroFragment";
+ fragmentManager.beginTransaction()
+ .setReorderingAllowed(true)
+ .add(R.id.fragment_container_view, FingerprintEnrollIntroFragment.class, null, tag)
+ .commit();
+ }
+
+ private void onSetActivityResult(@NonNull ActivityResult result) {
+ setResult(mViewModel.getRequest().isAfterSuwOrSuwSuggestedAction()
+ ? RESULT_CANCELED
+ : result.getResultCode(),
+ result.getData());
+ finish();
+ }
+
+ private void onCredentialAction(@NonNull Integer action) {
+ switch (action) {
+ case CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK: {
+ final Intent intent = mAutoCredentialViewModel.getChooseLockIntent(this,
+ mViewModel.getRequest().isSuw(), mViewModel.getRequest().getSuwExtras());
+ if (!mViewModel.isWaitingActivityResult().compareAndSet(false, true)) {
+ Log.w(TAG, "chooseLock, fail to set isWaiting flag to true");
+ }
+ mChooseLockLauncher.launch(intent);
+ return;
+ }
+ case CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK: {
+ final boolean launched = mAutoCredentialViewModel.getConfirmLockLauncher(
+ this,
+ LAUNCH_CONFIRM_LOCK_ACTIVITY,
+ getString(R.string.security_settings_fingerprint_preference_title)
+ ).launch();
+ if (!launched) {
+ // This shouldn't happen, as we should only end up at this step if a lock thingy
+ // is already set.
+ Log.e(TAG, "confirmLock, launched is true");
+ finish();
+ } else if (!mViewModel.isWaitingActivityResult().compareAndSet(false, true)) {
+ Log.w(TAG, "confirmLock, fail to set isWaiting flag to true");
+ }
+ return;
+ }
+ case CREDENTIAL_FAIL_DURING_GENERATE_CHALLENGE: {
+ Log.w(TAG, "observeCredentialLiveData, finish with action:" + action);
+ if (mViewModel.getRequest().isAfterSuwOrSuwSuggestedAction()) {
+ setResult(Activity.RESULT_CANCELED);
+ }
+ finish();
+ }
+ }
+ }
+
+ private void onChooseOrConfirmLockResult(boolean isChooseLock,
+ @NonNull ActivityResult activityResult) {
+ if (!mViewModel.isWaitingActivityResult().compareAndSet(true, false)) {
+ Log.w(TAG, "isChooseLock:" + isChooseLock + ", fail to unset waiting flag");
+ }
+ if (mAutoCredentialViewModel.checkNewCredentialFromActivityResult(
+ isChooseLock, activityResult)) {
+ overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out);
+ }
+ }
+
+ private void observeIntroAction(@NonNull Integer action) {
+ switch (action) {
+ case FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH: {
+ onSetActivityResult(
+ new ActivityResult(BiometricEnrollBase.RESULT_FINISHED, null));
+ return;
+ }
+ case FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL: {
+ onSetActivityResult(
+ new ActivityResult(BiometricEnrollBase.RESULT_SKIP, null));
+ return;
+ }
+ case FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL: {
+ 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
+ ? SetupFingerprintEnrollEnrolling.class
+ : FingerprintEnrollFindSensor.class);
+ intent.putExtras(mAutoCredentialViewModel.getCredentialBundle());
+ intent.putExtras(mViewModel.getNextActivityBaseIntentExtras());
+ mNextActivityLauncher.launch(intent);
+ }
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mViewModel.checkFinishActivityDuringOnPause(isFinishing(), isChangingConfigurations());
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ if (requestCode == LAUNCH_CONFIRM_LOCK_ACTIVITY) {
+ onChooseOrConfirmLockResult(false, new ActivityResult(resultCode, data));
+ return;
+ }
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ @NonNull
+ @Override
+ public CreationExtras getDefaultViewModelCreationExtras() {
+ final Application application =
+ super.getDefaultViewModelCreationExtras().get(APPLICATION_KEY);
+ final MutableCreationExtras ret = new MutableCreationExtras();
+ ret.set(APPLICATION_KEY, application);
+ final FingerprintRepository repository = FeatureFactory.getFactory(application)
+ .getBiometricsRepositoryProvider().getFingerprintRepository(application);
+ ret.set(CHALLENGE_GENERATOR, new FingerprintChallengeGenerator(repository));
+ return ret;
+ }
+
+ @NonNull
+ @Override
+ public ViewModelProvider.Factory getDefaultViewModelProviderFactory() {
+ return new BiometricsViewModelFactory();
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ getWindow().setStatusBarColor(getBackgroundColor());
+ }
+
+ @ColorInt
+ private int getBackgroundColor() {
+ final ColorStateList stateList = Utils.getColorAttr(this, android.R.attr.windowBackground);
+ return stateList != null ? stateList.getDefaultColor() : Color.TRANSPARENT;
+ }
+
+ @Override
+ protected void onDestroy() {
+ getLifecycle().removeObserver(mViewModel);
+ super.onDestroy();
+ }
+
+ @Override
+ protected void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mViewModel.onSaveInstanceState(outState);
+ }
+}
diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModel.java
new file mode 100644
index 00000000000..b1a7f90e33f
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModel.java
@@ -0,0 +1,354 @@
+/*
+ * 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.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
+
+import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_CHALLENGE;
+import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_SENSOR_ID;
+import static com.android.settings.biometrics2.ui.model.CredentialModel.INVALID_GK_PW_HANDLE;
+import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN;
+import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE;
+
+import android.annotation.IntDef;
+import android.app.Activity;
+import android.app.Application;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.activity.result.ActivityResult;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import com.android.internal.widget.LockPatternUtils;
+import com.android.internal.widget.VerifyCredentialResponse;
+import com.android.settings.biometrics.BiometricUtils;
+import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+import com.android.settings.biometrics2.ui.model.CredentialModel;
+import com.android.settings.password.ChooseLockGeneric;
+import com.android.settings.password.ChooseLockPattern;
+import com.android.settings.password.ChooseLockSettingsHelper;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * AutoCredentialViewModel which uses CredentialModel to determine next actions for activity, like
+ * start ChooseLockActivity, start ConfirmLockActivity, GenerateCredential, or do nothing.
+ */
+public class AutoCredentialViewModel extends AndroidViewModel implements DefaultLifecycleObserver {
+
+ private static final String TAG = "AutoCredentialViewModel";
+ private static final boolean DEBUG = true;
+
+ /**
+ * Need activity to run choose lock
+ */
+ public static final int CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK = 1;
+
+ /**
+ * Need activity to run confirm lock
+ */
+ public static final int CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK = 2;
+
+ /**
+ * Fail to use challenge from hardware generateChallenge(), shall finish activity with proper
+ * error code
+ */
+ public static final int CREDENTIAL_FAIL_DURING_GENERATE_CHALLENGE = 3;
+
+ @IntDef(prefix = { "CREDENTIAL_" }, value = {
+ CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK,
+ CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK,
+ CREDENTIAL_FAIL_DURING_GENERATE_CHALLENGE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface CredentialAction {}
+
+ /**
+ * Generic callback for FingerprintManager#generateChallenge or FaceManager#generateChallenge
+ */
+ public interface GenerateChallengeCallback {
+ /**
+ * Generic generateChallenge method for FingerprintManager or FaceManager
+ */
+ void onChallengeGenerated(int sensorId, int userId, long challenge);
+ }
+
+ /**
+ * A generic interface class for calling different generateChallenge from FingerprintManager or
+ * FaceManager
+ */
+ public interface ChallengeGenerator {
+ /**
+ * Get callback that will be called later after challenge generated
+ */
+ @Nullable
+ GenerateChallengeCallback getCallback();
+
+ /**
+ * Set callback that will be called later after challenge generated
+ */
+ void setCallback(@Nullable GenerateChallengeCallback callback);
+
+ /**
+ * Method for generating challenge from FingerprintManager or FaceManager
+ */
+ void generateChallenge(int userId);
+ }
+
+ /**
+ * Used to generate challenge through FingerprintRepository
+ */
+ public static class FingerprintChallengeGenerator implements ChallengeGenerator {
+
+ private static final String TAG = "FingerprintChallengeGenerator";
+
+ @NonNull
+ private final FingerprintRepository mFingerprintRepository;
+
+ @Nullable
+ private GenerateChallengeCallback mCallback = null;
+
+ public FingerprintChallengeGenerator(@NonNull FingerprintRepository fingerprintRepository) {
+ mFingerprintRepository = fingerprintRepository;
+ }
+
+ @Nullable
+ @Override
+ public GenerateChallengeCallback getCallback() {
+ return mCallback;
+ }
+
+ @Override
+ public void setCallback(@Nullable GenerateChallengeCallback callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void generateChallenge(int userId) {
+ final GenerateChallengeCallback callback = mCallback;
+ if (callback == null) {
+ Log.e(TAG, "generateChallenge, null callback");
+ return;
+ }
+ mFingerprintRepository.generateChallenge(userId, callback::onChallengeGenerated);
+ }
+ }
+
+
+ @NonNull private final LockPatternUtils mLockPatternUtils;
+ @NonNull private final ChallengeGenerator mChallengeGenerator;
+ private CredentialModel mCredentialModel = null;
+ @NonNull private final MutableLiveData mActionLiveData =
+ new MutableLiveData<>();
+
+ public AutoCredentialViewModel(
+ @NonNull Application application,
+ @NonNull LockPatternUtils lockPatternUtils,
+ @NonNull ChallengeGenerator challengeGenerator) {
+ super(application);
+ mLockPatternUtils = lockPatternUtils;
+ mChallengeGenerator = challengeGenerator;
+ }
+
+ public void setCredentialModel(@NonNull CredentialModel credentialModel) {
+ mCredentialModel = credentialModel;
+ }
+
+ /**
+ * Observe ActionLiveData for actions about choosing lock, confirming lock, or finishing
+ * activity
+ */
+ @NonNull
+ public LiveData getActionLiveData() {
+ return mActionLiveData;
+ }
+
+ @Override
+ public void onCreate(@NonNull LifecycleOwner owner) {
+ checkCredential();
+ }
+
+ /**
+ * Check credential status for biometric enrollment.
+ */
+ private void checkCredential() {
+ if (isValidCredential()) {
+ return;
+ }
+ final long gkPwHandle = mCredentialModel.getGkPwHandle();
+ if (isUnspecifiedPassword()) {
+ mActionLiveData.postValue(CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK);
+ } else if (CredentialModel.isValidGkPwHandle(gkPwHandle)) {
+ generateChallenge(gkPwHandle);
+ } else {
+ mActionLiveData.postValue(CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK);
+ }
+ }
+
+ private void generateChallenge(long gkPwHandle) {
+ mChallengeGenerator.setCallback((sensorId, userId, challenge) -> {
+ mCredentialModel.setSensorId(sensorId);
+ mCredentialModel.setChallenge(challenge);
+ try {
+ final byte[] newToken = requestGatekeeperHat(gkPwHandle, challenge, userId);
+ mCredentialModel.setToken(newToken);
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "generateChallenge, IllegalStateException", e);
+ mActionLiveData.postValue(CREDENTIAL_FAIL_DURING_GENERATE_CHALLENGE);
+ return;
+ }
+
+ mLockPatternUtils.removeGatekeeperPasswordHandle(gkPwHandle);
+ mCredentialModel.clearGkPwHandle();
+
+ if (DEBUG) {
+ Log.d(TAG, "generateChallenge " + mCredentialModel);
+ }
+
+ // Check credential again
+ if (!isValidCredential()) {
+ Log.w(TAG, "generateChallenge, invalid Credential");
+ mActionLiveData.postValue(CREDENTIAL_FAIL_DURING_GENERATE_CHALLENGE);
+ }
+ });
+ mChallengeGenerator.generateChallenge(getUserId());
+ }
+
+ private boolean isValidCredential() {
+ return !isUnspecifiedPassword()
+ && CredentialModel.isValidToken(mCredentialModel.getToken());
+ }
+
+ private boolean isUnspecifiedPassword() {
+ return mLockPatternUtils.getActivePasswordQuality(getUserId())
+ == PASSWORD_QUALITY_UNSPECIFIED;
+ }
+
+ /**
+ * Handle activity result from ChooseLockGeneric, ConfirmLockPassword, or ConfirmLockPattern
+ * @param isChooseLock true if result is coming from ChooseLockGeneric. False if result is
+ * coming from ConfirmLockPassword or ConfirmLockPattern
+ * @param result activity result
+ * @return if it is a valid result
+ */
+ public boolean checkNewCredentialFromActivityResult(boolean isChooseLock,
+ @NonNull ActivityResult result) {
+ if ((isChooseLock && result.getResultCode() == ChooseLockPattern.RESULT_FINISHED)
+ || (!isChooseLock && result.getResultCode() == Activity.RESULT_OK)) {
+ final Intent data = result.getData();
+ if (data != null) {
+ final long gkPwHandle = result.getData().getLongExtra(
+ EXTRA_KEY_GK_PW_HANDLE, INVALID_GK_PW_HANDLE);
+ generateChallenge(gkPwHandle);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Get userId for this credential
+ */
+ public int getUserId() {
+ return mCredentialModel.getUserId();
+ }
+
+ @Nullable
+ private byte[] requestGatekeeperHat(long gkPwHandle, long challenge, int userId)
+ throws IllegalStateException {
+ final VerifyCredentialResponse response = mLockPatternUtils
+ .verifyGatekeeperPasswordHandle(gkPwHandle, challenge, userId);
+ if (!response.isMatched()) {
+ throw new IllegalStateException("Unable to request Gatekeeper HAT");
+ }
+ return response.getGatekeeperHAT();
+ }
+
+ /**
+ * Get Credential bundle which will be used to launch next activity.
+ */
+ @NonNull
+ public Bundle getCredentialBundle() {
+ final Bundle retBundle = new Bundle();
+ final long gkPwHandle = mCredentialModel.getGkPwHandle();
+ if (CredentialModel.isValidGkPwHandle(gkPwHandle)) {
+ retBundle.putLong(EXTRA_KEY_GK_PW_HANDLE, gkPwHandle);
+ }
+ final byte[] token = mCredentialModel.getToken();
+ if (CredentialModel.isValidToken(token)) {
+ retBundle.putByteArray(EXTRA_KEY_CHALLENGE_TOKEN, token);
+ }
+ final int userId = getUserId();
+ if (CredentialModel.isValidUserId(userId)) {
+ retBundle.putInt(Intent.EXTRA_USER_ID, userId);
+ }
+ retBundle.putLong(EXTRA_KEY_CHALLENGE, mCredentialModel.getChallenge());
+ retBundle.putInt(EXTRA_KEY_SENSOR_ID, mCredentialModel.getSensorId());
+ return retBundle;
+ }
+
+ /**
+ * Get Intent for choosing lock
+ */
+ @NonNull
+ public Intent getChooseLockIntent(@NonNull Context context, boolean isSuw,
+ @NonNull Bundle suwExtras) {
+ final Intent intent = BiometricUtils.getChooseLockIntent(context, isSuw,
+ suwExtras);
+ intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.HIDE_INSECURE_OPTIONS,
+ true);
+ intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, true);
+ intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT, true);
+
+ final int userId = getUserId();
+ if (CredentialModel.isValidUserId(userId)) {
+ intent.putExtra(Intent.EXTRA_USER_ID, userId);
+ }
+ return intent;
+ }
+
+ /**
+ * Get ConfirmLockLauncher
+ */
+ @NonNull
+ public ChooseLockSettingsHelper getConfirmLockLauncher(@NonNull Activity activity,
+ int requestCode, @NonNull String title) {
+ final ChooseLockSettingsHelper.Builder builder =
+ new ChooseLockSettingsHelper.Builder(activity);
+ builder.setRequestCode(requestCode)
+ .setTitle(title)
+ .setRequestGatekeeperPasswordHandle(true)
+ .setForegroundOnly(true)
+ .setReturnCredentials(true);
+
+ final int userId = mCredentialModel.getUserId();
+ if (CredentialModel.isValidUserId(userId)) {
+ builder.setUserId(userId);
+ }
+ return builder.build();
+ }
+
+}
diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModel.java
new file mode 100644
index 00000000000..252a508e630
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModel.java
@@ -0,0 +1,211 @@
+/*
+ * 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.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX;
+import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_OK;
+import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_UNKNOWN;
+
+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;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MediatorLiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
+import com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Fingerprint intro onboarding page view model implementation
+ */
+public class FingerprintEnrollIntroViewModel extends AndroidViewModel
+ implements DefaultLifecycleObserver {
+
+ private static final String TAG = "FingerprintEnrollIntroViewModel";
+ private static final boolean HAS_SCROLLED_TO_BOTTOM_DEFAULT = false;
+ private static final int ENROLLABLE_STATUS_DEFAULT = FINGERPRINT_ENROLLABLE_UNKNOWN;
+
+ /**
+ * User clicks 'Done' button on this page
+ */
+ public static final int FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH = 0;
+
+ /**
+ * User clicks 'Agree' button on this page
+ */
+ public static final int FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL = 1;
+
+ /**
+ * User clicks 'Skip' button on this page
+ */
+ public static final int FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL = 2;
+
+ @IntDef(prefix = { "FINGERPRINT_ENROLL_INTRO_ACTION_" }, value = {
+ FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH,
+ FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL,
+ FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface FingerprintEnrollIntroAction {}
+
+ @NonNull private final FingerprintRepository mFingerprintRepository;
+
+ private final MutableLiveData mHasScrolledToBottomLiveData =
+ new MutableLiveData<>(HAS_SCROLLED_TO_BOTTOM_DEFAULT);
+ private final MutableLiveData mEnrollableStatusLiveData =
+ new MutableLiveData<>(ENROLLABLE_STATUS_DEFAULT);
+ private final MediatorLiveData mPageStatusLiveData =
+ new MediatorLiveData<>();
+ private final MutableLiveData mActionLiveData = new MutableLiveData<>();
+ private int mUserId = UserHandle.myUserId();
+ private EnrollmentRequest mEnrollmentRequest = null;
+
+ public FingerprintEnrollIntroViewModel(@NonNull Application application,
+ @NonNull FingerprintRepository fingerprintRepository) {
+ super(application);
+ mFingerprintRepository = fingerprintRepository;
+
+ mPageStatusLiveData.addSource(
+ mEnrollableStatusLiveData,
+ enrollable -> {
+ final Boolean toBottomValue = mHasScrolledToBottomLiveData.getValue();
+ final FingerprintEnrollIntroStatus status = new FingerprintEnrollIntroStatus(
+ toBottomValue != null ? toBottomValue : HAS_SCROLLED_TO_BOTTOM_DEFAULT,
+ enrollable);
+ mPageStatusLiveData.setValue(status);
+ });
+ mPageStatusLiveData.addSource(
+ mHasScrolledToBottomLiveData,
+ hasScrolledToBottom -> {
+ final Integer enrollableValue = mEnrollableStatusLiveData.getValue();
+ final FingerprintEnrollIntroStatus status = new FingerprintEnrollIntroStatus(
+ hasScrolledToBottom,
+ enrollableValue != null ? enrollableValue : ENROLLABLE_STATUS_DEFAULT);
+ mPageStatusLiveData.setValue(status);
+ });
+ }
+
+ public void setUserId(int userId) {
+ mUserId = userId;
+ }
+
+ public void setEnrollmentRequest(@NonNull EnrollmentRequest enrollmentRequest) {
+ mEnrollmentRequest = enrollmentRequest;
+ }
+
+ /**
+ * Get enrollment request
+ */
+ public EnrollmentRequest getEnrollmentRequest() {
+ return mEnrollmentRequest;
+ }
+
+ private void updateEnrollableStatus() {
+ final int num = mFingerprintRepository.getNumOfEnrolledFingerprintsSize(mUserId);
+ final int max =
+ mEnrollmentRequest.isSuw() && !mEnrollmentRequest.isAfterSuwOrSuwSuggestedAction()
+ ? mFingerprintRepository.getMaxFingerprintsInSuw(getApplication().getResources())
+ : mFingerprintRepository.getMaxFingerprints();
+ mEnrollableStatusLiveData.postValue(num >= max
+ ? FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX
+ : FINGERPRINT_ENROLLABLE_OK);
+ }
+
+ /**
+ * Get enrollable status and hasScrollToBottom live data
+ */
+ public LiveData getPageStatusLiveData() {
+ return mPageStatusLiveData;
+ }
+
+ /**
+ * Get user's action live data (like clicking Agree, Skip, or Done)
+ */
+ public LiveData getActionLiveData() {
+ return mActionLiveData;
+ }
+
+ /**
+ * The first sensor type is UDFPS sensor or not
+ */
+ public boolean canAssumeUdfps() {
+ return mFingerprintRepository.canAssumeUdfps();
+ }
+
+ /**
+ * Update onboarding intro page has scrolled to bottom
+ */
+ public void setHasScrolledToBottom() {
+ mHasScrolledToBottomLiveData.postValue(true);
+ }
+
+ /**
+ * Get parental consent required or not during enrollment process
+ */
+ public boolean isParentalConsentRequired() {
+ return mFingerprintRepository.isParentalConsentRequired(getApplication());
+ }
+
+ /**
+ * Get fingerprint is disable by admin or not
+ */
+ public boolean isBiometricUnlockDisabledByAdmin() {
+ return mFingerprintRepository.isDisabledByAdmin(getApplication(), mUserId);
+ }
+
+ /**
+ * User clicks next button
+ */
+ public void onNextButtonClick(View ignoredView) {
+ final Integer status = mEnrollableStatusLiveData.getValue();
+ switch (status != null ? status : ENROLLABLE_STATUS_DEFAULT) {
+ case FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX:
+ mActionLiveData.postValue(FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH);
+ break;
+ case FINGERPRINT_ENROLLABLE_OK:
+ mActionLiveData.postValue(FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL);
+ break;
+ default:
+ Log.w(TAG, "fail to click next, enrolled:" + status);
+ }
+ }
+
+ /**
+ * User clicks skip/cancel button
+ */
+ public void onSkipOrCancelButtonClick(View ignoredView) {
+ mActionLiveData.postValue(FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL);
+ }
+
+ @Override
+ public void onStart(@NonNull LifecycleOwner owner) {
+ updateEnrollableStatus();
+ }
+
+}
diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModel.java
new file mode 100644
index 00000000000..468e132555d
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModel.java
@@ -0,0 +1,185 @@
+/*
+ * 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.biometrics.BiometricEnrollBase.EXTRA_FROM_SETTINGS_SUMMARY;
+import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHED;
+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 android.app.Application;
+import android.app.KeyguardManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.activity.result.ActivityResult;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import com.android.settings.biometrics.BiometricEnrollBase;
+import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
+import com.android.settings.password.SetupSkipDialog;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Fingerprint enrollment view model implementation
+ */
+public class FingerprintEnrollmentViewModel extends AndroidViewModel implements
+ DefaultLifecycleObserver {
+
+ private static final String TAG = "FingerprintEnrollmentViewModel";
+
+ @VisibleForTesting
+ static final String SAVED_STATE_IS_WAITING_ACTIVITY_RESULT = "is_waiting_activity_result";
+
+ @NonNull private final FingerprintRepository mFingerprintRepository;
+ @Nullable private final KeyguardManager mKeyguardManager;
+
+ private final AtomicBoolean mIsWaitingActivityResult = new AtomicBoolean(false);
+ private final MutableLiveData mSetResultLiveData = new MutableLiveData<>();
+
+ /**
+ * Even this variable may be nullable, but activity will call setIntent() immediately during
+ * its onCreate(), we do not assign @Nullable for this variable here.
+ */
+ private EnrollmentRequest mRequest = null;
+
+ public FingerprintEnrollmentViewModel(
+ @NonNull Application application,
+ @NonNull FingerprintRepository fingerprintRepository,
+ @Nullable KeyguardManager keyguardManager) {
+ super(application);
+ mFingerprintRepository = fingerprintRepository;
+ mKeyguardManager = keyguardManager;
+ }
+
+ /**
+ * Set EnrollmentRequest
+ */
+ public void setRequest(@NonNull EnrollmentRequest request) {
+ mRequest = request;
+ }
+
+ /**
+ * Get EnrollmentRequest
+ */
+ public EnrollmentRequest getRequest() {
+ return mRequest;
+ }
+
+ /**
+ * Copy necessary extra data from activity intent
+ */
+ @NonNull
+ public Bundle getNextActivityBaseIntentExtras() {
+ final Bundle bundle = mRequest.getSuwExtras();
+ bundle.putBoolean(EXTRA_FROM_SETTINGS_SUMMARY, mRequest.isFromSettingsSummery());
+ return bundle;
+ }
+
+ /**
+ * Handle activity result from FingerprintFindSensor
+ */
+ public void onContinueEnrollActivityResult(@NonNull ActivityResult result, int userId) {
+ if (mIsWaitingActivityResult.compareAndSet(true, false)) {
+ Log.w(TAG, "fail to reset isWaiting flag for enrollment");
+ }
+ if (result.getResultCode() == RESULT_FINISHED
+ || result.getResultCode() == RESULT_TIMEOUT) {
+ Intent data = result.getData();
+ if (mRequest.isSuw() && isKeyguardSecure()
+ && result.getResultCode() == RESULT_FINISHED) {
+ if (data == null) {
+ data = new Intent();
+ }
+ data.putExtras(getSuwFingerprintCountExtra(userId));
+ }
+ mSetResultLiveData.postValue(new ActivityResult(result.getResultCode(), data));
+ } else if (result.getResultCode() == RESULT_SKIP
+ || result.getResultCode() == SetupSkipDialog.RESULT_SKIP) {
+ mSetResultLiveData.postValue(result);
+ }
+ }
+
+
+
+ private boolean isKeyguardSecure() {
+ return mKeyguardManager != null && mKeyguardManager.isKeyguardSecure();
+ }
+
+ /**
+ * Activity calls this method during onPause() to finish itself when back to background.
+ *
+ * @param isActivityFinishing Activity has called finish() or not
+ * @param isChangingConfigurations Activity is finished because of configuration changed or not.
+ */
+ public void checkFinishActivityDuringOnPause(boolean isActivityFinishing,
+ boolean isChangingConfigurations) {
+ if (isChangingConfigurations || isActivityFinishing || mRequest.isSuw()
+ || isWaitingActivityResult().get()) {
+ return;
+ }
+
+ mSetResultLiveData.postValue(new ActivityResult(BiometricEnrollBase.RESULT_TIMEOUT, null));
+ }
+
+ @NonNull
+ private Bundle getSuwFingerprintCountExtra(int userId) {
+ final Bundle bundle = new Bundle();
+ bundle.putInt(EXTRA_FINGERPRINT_ENROLLED_COUNT,
+ mFingerprintRepository.getNumOfEnrolledFingerprintsSize(userId));
+ return bundle;
+ }
+
+ @NonNull
+ public LiveData getSetResultLiveData() {
+ return mSetResultLiveData;
+ }
+
+ @NonNull
+ public AtomicBoolean isWaitingActivityResult() {
+ return mIsWaitingActivityResult;
+ }
+
+ /**
+ * Handle savedInstanceState from activity onCreated()
+ */
+ public void setSavedInstanceState(@Nullable Bundle savedInstanceState) {
+ if (savedInstanceState == null) {
+ return;
+ }
+ mIsWaitingActivityResult.set(
+ savedInstanceState.getBoolean(SAVED_STATE_IS_WAITING_ACTIVITY_RESULT, false)
+ );
+ }
+
+ /**
+ * Handle onSaveInstanceState from activity
+ */
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ outState.putBoolean(SAVED_STATE_IS_WAITING_ACTIVITY_RESULT, mIsWaitingActivityResult.get());
+ }
+}
diff --git a/src/com/android/settings/overlay/FeatureFactory.java b/src/com/android/settings/overlay/FeatureFactory.java
index cf8698c9a3f..5da93107006 100644
--- a/src/com/android/settings/overlay/FeatureFactory.java
+++ b/src/com/android/settings/overlay/FeatureFactory.java
@@ -29,6 +29,7 @@ import com.android.settings.accounts.AccountFeatureProvider;
import com.android.settings.applications.ApplicationFeatureProvider;
import com.android.settings.aware.AwareFeatureProvider;
import com.android.settings.biometrics.face.FaceFeatureProvider;
+import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider;
import com.android.settings.bluetooth.BluetoothFeatureProvider;
import com.android.settings.dashboard.DashboardFeatureProvider;
import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider;
@@ -161,6 +162,11 @@ public abstract class FeatureFactory {
public abstract FaceFeatureProvider getFaceFeatureProvider();
+ /**
+ * Gets implementation for Biometrics repository provider.
+ */
+ public abstract BiometricsRepositoryProvider getBiometricsRepositoryProvider();
+
/**
* Gets implementation for the WifiTrackerLib.
*/
diff --git a/src/com/android/settings/overlay/FeatureFactoryImpl.java b/src/com/android/settings/overlay/FeatureFactoryImpl.java
index b7797165899..bc78f2eff01 100644
--- a/src/com/android/settings/overlay/FeatureFactoryImpl.java
+++ b/src/com/android/settings/overlay/FeatureFactoryImpl.java
@@ -37,6 +37,8 @@ import com.android.settings.aware.AwareFeatureProvider;
import com.android.settings.aware.AwareFeatureProviderImpl;
import com.android.settings.biometrics.face.FaceFeatureProvider;
import com.android.settings.biometrics.face.FaceFeatureProviderImpl;
+import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider;
+import com.android.settings.biometrics2.factory.BiometricsRepositoryProviderImpl;
import com.android.settings.bluetooth.BluetoothFeatureProvider;
import com.android.settings.bluetooth.BluetoothFeatureProviderImpl;
import com.android.settings.connecteddevice.dock.DockUpdaterFeatureProviderImpl;
@@ -104,6 +106,7 @@ public class FeatureFactoryImpl extends FeatureFactory {
private BluetoothFeatureProvider mBluetoothFeatureProvider;
private AwareFeatureProvider mAwareFeatureProvider;
private FaceFeatureProvider mFaceFeatureProvider;
+ private BiometricsRepositoryProvider mBiometricsRepositoryProvider;
private WifiTrackerLibProvider mWifiTrackerLibProvider;
private SecuritySettingsFeatureProvider mSecuritySettingsFeatureProvider;
private AccessibilitySearchFeatureProvider mAccessibilitySearchFeatureProvider;
@@ -305,6 +308,14 @@ public class FeatureFactoryImpl extends FeatureFactory {
return mFaceFeatureProvider;
}
+ @Override
+ public BiometricsRepositoryProvider getBiometricsRepositoryProvider() {
+ if (mBiometricsRepositoryProvider == null) {
+ mBiometricsRepositoryProvider = new BiometricsRepositoryProviderImpl();
+ }
+ return mBiometricsRepositoryProvider;
+ }
+
@Override
public WifiTrackerLibProvider getWifiTrackerLibProvider() {
if (mWifiTrackerLibProvider == null) {
diff --git a/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java b/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java
index b87d9833b4c..c1e7bfbbe9f 100644
--- a/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java
+++ b/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java
@@ -27,6 +27,7 @@ import com.android.settings.accounts.AccountFeatureProvider;
import com.android.settings.applications.ApplicationFeatureProvider;
import com.android.settings.aware.AwareFeatureProvider;
import com.android.settings.biometrics.face.FaceFeatureProvider;
+import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider;
import com.android.settings.bluetooth.BluetoothFeatureProvider;
import com.android.settings.dashboard.DashboardFeatureProvider;
import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider;
@@ -78,6 +79,7 @@ public class FakeFeatureFactory extends FeatureFactory {
public final BluetoothFeatureProvider mBluetoothFeatureProvider;
public final AwareFeatureProvider mAwareFeatureProvider;
public final FaceFeatureProvider mFaceFeatureProvider;
+ public final BiometricsRepositoryProvider mBiometricsRepositoryProvider;
public PanelFeatureProvider panelFeatureProvider;
public SlicesFeatureProvider slicesFeatureProvider;
@@ -134,6 +136,7 @@ public class FakeFeatureFactory extends FeatureFactory {
mBluetoothFeatureProvider = mock(BluetoothFeatureProvider.class);
mAwareFeatureProvider = mock(AwareFeatureProvider.class);
mFaceFeatureProvider = mock(FaceFeatureProvider.class);
+ mBiometricsRepositoryProvider = mock(BiometricsRepositoryProvider.class);
wifiTrackerLibProvider = mock(WifiTrackerLibProvider.class);
securitySettingsFeatureProvider = mock(SecuritySettingsFeatureProvider.class);
mAccessibilitySearchFeatureProvider = mock(AccessibilitySearchFeatureProvider.class);
@@ -256,6 +259,11 @@ public class FakeFeatureFactory extends FeatureFactory {
return mFaceFeatureProvider;
}
+ @Override
+ public BiometricsRepositoryProvider getBiometricsRepositoryProvider() {
+ return mBiometricsRepositoryProvider;
+ }
+
@Override
public WifiTrackerLibProvider getWifiTrackerLibProvider() {
return wifiTrackerLibProvider;
diff --git a/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt b/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt
index 7a93f111493..054b4150e8e 100644
--- a/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt
+++ b/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt
@@ -23,6 +23,7 @@ import com.android.settings.accounts.AccountFeatureProvider
import com.android.settings.applications.ApplicationFeatureProvider
import com.android.settings.aware.AwareFeatureProvider
import com.android.settings.biometrics.face.FaceFeatureProvider
+import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider
import com.android.settings.bluetooth.BluetoothFeatureProvider
import com.android.settings.dashboard.DashboardFeatureProvider
import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider
@@ -153,6 +154,10 @@ class FakeFeatureFactory : FeatureFactory() {
TODO("Not yet implemented")
}
+ override fun getBiometricsRepositoryProvider(): BiometricsRepositoryProvider {
+ TODO("Not yet implemented")
+ }
+
override fun getWifiTrackerLibProvider(): WifiTrackerLibProvider {
TODO("Not yet implemented")
}
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
new file mode 100644
index 00000000000..e5920f35b4a
--- /dev/null
+++ b/tests/unit/src/com/android/settings/biometrics2/data/repository/FingerprintRepositoryTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.data.repository;
+
+import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_HOME_BUTTON;
+import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_POWER_BUTTON;
+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 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.fingerprint.FingerprintManager;
+
+import androidx.annotation.NonNull;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.settings.testutils.ResourcesUtils;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@RunWith(AndroidJUnit4.class)
+public class FingerprintRepositoryTest {
+
+ @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+
+ @Mock private Resources mResources;
+ @Mock private FingerprintManager mFingerprintManager;
+
+ private Context mContext;
+ private FingerprintRepository mFingerprintRepository;
+
+ @Before
+ public void setUp() {
+ mContext = ApplicationProvider.getApplicationContext();
+ mFingerprintRepository = new FingerprintRepository(mFingerprintManager);
+ }
+
+ @Test
+ public void testCanAssumeSensorType() {
+ setupFingerprintFirstSensor(mFingerprintManager, TYPE_UNKNOWN, 1);
+ assertThat(mFingerprintRepository.canAssumeUdfps()).isFalse();
+
+ setupFingerprintFirstSensor(mFingerprintManager, TYPE_REAR, 1);
+ assertThat(mFingerprintRepository.canAssumeUdfps()).isFalse();
+
+ setupFingerprintFirstSensor(mFingerprintManager, TYPE_UDFPS_ULTRASONIC, 1);
+ assertThat(mFingerprintRepository.canAssumeUdfps()).isTrue();
+
+ setupFingerprintFirstSensor(mFingerprintManager, TYPE_UDFPS_OPTICAL, 1);
+ assertThat(mFingerprintRepository.canAssumeUdfps()).isTrue();
+
+ setupFingerprintFirstSensor(mFingerprintManager, TYPE_POWER_BUTTON, 1);
+ assertThat(mFingerprintRepository.canAssumeUdfps()).isFalse();
+
+ setupFingerprintFirstSensor(mFingerprintManager, TYPE_HOME_BUTTON, 1);
+ assertThat(mFingerprintRepository.canAssumeUdfps()).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);
+ }
+
+ @Test
+ public void testGetNumOfEnrolledFingerprintsSize() {
+ setupFingerprintEnrolledFingerprints(mFingerprintManager, 10, 3);
+ setupFingerprintEnrolledFingerprints(mFingerprintManager, 22, 99);
+
+ assertThat(mFingerprintRepository.getNumOfEnrolledFingerprintsSize(10)).isEqualTo(3);
+ assertThat(mFingerprintRepository.getNumOfEnrolledFingerprintsSize(22)).isEqualTo(99);
+ }
+
+ @Test
+ public void testGetMaxFingerprintsInSuw() {
+ setupSuwMaxFingerprintsEnrollable(mContext, mResources, 333);
+ assertThat(mFingerprintRepository.getMaxFingerprintsInSuw(mResources))
+ .isEqualTo(333);
+
+ setupSuwMaxFingerprintsEnrollable(mContext, mResources, 20);
+ assertThat(mFingerprintRepository.getMaxFingerprintsInSuw(mResources)).isEqualTo(20);
+ }
+
+ public static void setupSuwMaxFingerprintsEnrollable(
+ @NonNull Context context,
+ @NonNull Resources mockedResources,
+ int numOfFp) {
+ final int resId = ResourcesUtils.getResourcesId(context, "integer",
+ "suw_max_fingerprints_enrollable");
+ when(mockedResources.getInteger(resId)).thenReturn(numOfFp);
+ }
+}
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
new file mode 100644
index 00000000000..7a13875a8fa
--- /dev/null
+++ b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModelTest.java
@@ -0,0 +1,380 @@
+/*
+ * 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.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
+
+import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_CHALLENGE;
+import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_SENSOR_ID;
+import static com.android.settings.biometrics2.ui.model.CredentialModel.INVALID_CHALLENGE;
+import static com.android.settings.biometrics2.ui.model.CredentialModel.INVALID_SENSOR_ID;
+import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK;
+import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK;
+import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.ChallengeGenerator;
+import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CredentialAction;
+import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.GenerateChallengeCallback;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.app.Activity;
+import android.content.Intent;
+import android.os.SystemClock;
+import android.os.UserHandle;
+
+import androidx.activity.result.ActivityResult;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.internal.widget.LockPatternUtils;
+import com.android.internal.widget.VerifyCredentialResponse;
+import com.android.settings.biometrics2.ui.model.CredentialModel;
+import com.android.settings.password.ChooseLockPattern;
+import com.android.settings.password.ChooseLockSettingsHelper;
+import com.android.settings.testutils.InstantTaskExecutorRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@RunWith(AndroidJUnit4.class)
+public class AutoCredentialViewModelTest {
+
+ @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+ @Rule public final InstantTaskExecutorRule mTaskExecutorRule = new InstantTaskExecutorRule();
+
+ @Mock private LifecycleOwner mLifecycleOwner;
+ @Mock private LockPatternUtils mLockPatternUtils;
+ private TestChallengeGenerator mChallengeGenerator = null;
+ private AutoCredentialViewModel mAutoCredentialViewModel;
+
+ @Before
+ public void setUp() {
+ mChallengeGenerator = new TestChallengeGenerator();
+ mAutoCredentialViewModel = new AutoCredentialViewModel(
+ ApplicationProvider.getApplicationContext(),
+ mLockPatternUtils,
+ mChallengeGenerator);
+ }
+
+ private CredentialModel newCredentialModel(int userId, long challenge,
+ @Nullable byte[] token, long gkPwHandle) {
+ final Intent intent = new Intent();
+ intent.putExtra(Intent.EXTRA_USER_ID, userId);
+ intent.putExtra(EXTRA_KEY_SENSOR_ID, 1);
+ intent.putExtra(EXTRA_KEY_CHALLENGE, challenge);
+ intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token);
+ intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gkPwHandle);
+ return new CredentialModel(intent, SystemClock.elapsedRealtimeClock());
+ }
+
+ private CredentialModel newValidTokenCredentialModel(int userId) {
+ return newCredentialModel(userId, 1L, new byte[] { 0 }, 0L);
+ }
+
+ private CredentialModel newInvalidChallengeCredentialModel(int userId) {
+ return newCredentialModel(userId, INVALID_CHALLENGE, null, 0L);
+ }
+
+ private CredentialModel newGkPwHandleCredentialModel(int userId, long gkPwHandle) {
+ return newCredentialModel(userId, INVALID_CHALLENGE, null, gkPwHandle);
+ }
+
+ private void verifyNothingHappen() {
+ assertThat(mAutoCredentialViewModel.getActionLiveData().getValue()).isNull();
+ }
+
+ private void verifyOnlyActionLiveData(@CredentialAction int action) {
+ final Integer value = mAutoCredentialViewModel.getActionLiveData().getValue();
+ assertThat(value).isEqualTo(action);
+ }
+
+ private void setupGenerateTokenFlow(long gkPwHandle, int userId, int newSensorId,
+ long newChallenge) {
+ when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
+ PASSWORD_QUALITY_SOMETHING);
+ mChallengeGenerator.mUserId = userId;
+ mChallengeGenerator.mSensorId = newSensorId;
+ mChallengeGenerator.mChallenge = newChallenge;
+ when(mLockPatternUtils.verifyGatekeeperPasswordHandle(gkPwHandle, newChallenge, userId))
+ .thenReturn(newGoodCredential(gkPwHandle, new byte[] { 1 }));
+ }
+
+ @Test
+ public void checkCredential_validCredentialCase() {
+ final int userId = 99;
+ mAutoCredentialViewModel.setCredentialModel(newValidTokenCredentialModel(userId));
+ when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
+ PASSWORD_QUALITY_SOMETHING);
+
+ // Run credential check
+ mAutoCredentialViewModel.onCreate(mLifecycleOwner);
+
+ verifyNothingHappen();
+ }
+
+ @Test
+ public void checkCredential_needToChooseLock() {
+ final int userId = 100;
+ mAutoCredentialViewModel.setCredentialModel(newInvalidChallengeCredentialModel(userId));
+ when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
+ PASSWORD_QUALITY_UNSPECIFIED);
+
+ // Run credential check
+ mAutoCredentialViewModel.onCreate(mLifecycleOwner);
+
+ verifyOnlyActionLiveData(CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK);
+ }
+
+ @Test
+ public void checkCredential_needToConfirmLockFoSomething() {
+ final int userId = 101;
+ mAutoCredentialViewModel.setCredentialModel(newInvalidChallengeCredentialModel(userId));
+ when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
+ PASSWORD_QUALITY_SOMETHING);
+
+ // Run credential check
+ mAutoCredentialViewModel.onCreate(mLifecycleOwner);
+
+ verifyOnlyActionLiveData(CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK);
+ }
+
+ @Test
+ public void checkCredential_needToConfirmLockForNumeric() {
+ final int userId = 102;
+ mAutoCredentialViewModel.setCredentialModel(newInvalidChallengeCredentialModel(userId));
+ when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
+ PASSWORD_QUALITY_NUMERIC);
+
+ // Run credential check
+ mAutoCredentialViewModel.onCreate(mLifecycleOwner);
+
+ verifyOnlyActionLiveData(CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK);
+ }
+
+ @Test
+ public void checkCredential_needToConfirmLockForAlphabetic() {
+ final int userId = 103;
+ mAutoCredentialViewModel.setCredentialModel(newInvalidChallengeCredentialModel(userId));
+ when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
+ PASSWORD_QUALITY_ALPHABETIC);
+
+ // Run credential check
+ mAutoCredentialViewModel.onCreate(mLifecycleOwner);
+
+ verifyOnlyActionLiveData(CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK);
+ }
+
+ @Test
+ public void checkCredential_generateChallenge() {
+ final int userId = 104;
+ final long gkPwHandle = 1111L;
+ final CredentialModel credentialModel = newGkPwHandleCredentialModel(userId, gkPwHandle);
+ mAutoCredentialViewModel.setCredentialModel(credentialModel);
+ when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
+ PASSWORD_QUALITY_SOMETHING);
+
+ final int newSensorId = 10;
+ final long newChallenge = 20L;
+ setupGenerateTokenFlow(gkPwHandle, userId, newSensorId, newChallenge);
+
+ // Run credential check
+ mAutoCredentialViewModel.onCreate(mLifecycleOwner);
+
+ assertThat(mAutoCredentialViewModel.getActionLiveData().getValue()).isNull();
+ assertThat(credentialModel.getSensorId()).isEqualTo(newSensorId);
+ assertThat(credentialModel.getChallenge()).isEqualTo(newChallenge);
+ assertThat(CredentialModel.isValidToken(credentialModel.getToken())).isTrue();
+ assertThat(CredentialModel.isValidGkPwHandle(credentialModel.getGkPwHandle())).isFalse();
+ assertThat(mChallengeGenerator.mCallbackRunCount).isEqualTo(1);
+ }
+
+ @Test
+ public void testGetUserId() {
+ final int userId = 106;
+ mAutoCredentialViewModel.setCredentialModel(newInvalidChallengeCredentialModel(userId));
+
+ // Get userId
+ assertThat(mAutoCredentialViewModel.getUserId()).isEqualTo(userId);
+ }
+
+ @Test
+ public void testCheckNewCredentialFromActivityResult_invalidChooseLock() {
+ final int userId = 107;
+ final long gkPwHandle = 3333L;
+ mAutoCredentialViewModel.setCredentialModel(
+ newGkPwHandleCredentialModel(userId, gkPwHandle));
+ final Intent intent = new Intent();
+ intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gkPwHandle);
+
+ // run checkNewCredentialFromActivityResult()
+ final boolean ret = mAutoCredentialViewModel.checkNewCredentialFromActivityResult(true,
+ new ActivityResult(ChooseLockPattern.RESULT_FINISHED + 1, intent));
+
+ assertThat(ret).isFalse();
+ verifyNothingHappen();
+ }
+
+ @Test
+ public void testCheckNewCredentialFromActivityResult_invalidConfirmLock() {
+ final int userId = 107;
+ final long gkPwHandle = 3333L;
+ mAutoCredentialViewModel.setCredentialModel(
+ newGkPwHandleCredentialModel(userId, gkPwHandle));
+ final Intent intent = new Intent();
+ intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gkPwHandle);
+
+ // run checkNewCredentialFromActivityResult()
+ final boolean ret = mAutoCredentialViewModel.checkNewCredentialFromActivityResult(false,
+ new ActivityResult(Activity.RESULT_OK + 1, intent));
+
+ assertThat(ret).isFalse();
+ verifyNothingHappen();
+ }
+
+ @Test
+ public void testCheckNewCredentialFromActivityResult_nullDataChooseLock() {
+ final int userId = 108;
+ final long gkPwHandle = 4444L;
+ mAutoCredentialViewModel.setCredentialModel(
+ newGkPwHandleCredentialModel(userId, gkPwHandle));
+
+ // run checkNewCredentialFromActivityResult()
+ final boolean ret = mAutoCredentialViewModel.checkNewCredentialFromActivityResult(true,
+ new ActivityResult(ChooseLockPattern.RESULT_FINISHED, null));
+
+ assertThat(ret).isFalse();
+ verifyNothingHappen();
+ }
+
+ @Test
+ public void testCheckNewCredentialFromActivityResult_nullDataConfirmLock() {
+ final int userId = 109;
+ mAutoCredentialViewModel.setCredentialModel(newInvalidChallengeCredentialModel(userId));
+
+ // run checkNewCredentialFromActivityResult()
+ final boolean ret = mAutoCredentialViewModel.checkNewCredentialFromActivityResult(false,
+ new ActivityResult(Activity.RESULT_OK, null));
+
+ assertThat(ret).isFalse();
+ verifyNothingHappen();
+ }
+
+ @Test
+ public void testCheckNewCredentialFromActivityResult_validChooseLock() {
+ final int userId = 108;
+ final CredentialModel credentialModel = newInvalidChallengeCredentialModel(userId);
+ mAutoCredentialViewModel.setCredentialModel(credentialModel);
+ when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
+ PASSWORD_QUALITY_SOMETHING);
+
+ final long gkPwHandle = 6666L;
+ final int newSensorId = 50;
+ final long newChallenge = 60L;
+ setupGenerateTokenFlow(gkPwHandle, userId, newSensorId, newChallenge);
+ final Intent intent = new Intent();
+ intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gkPwHandle);
+
+ // Run checkNewCredentialFromActivityResult()
+ final boolean ret = mAutoCredentialViewModel.checkNewCredentialFromActivityResult(true,
+ new ActivityResult(ChooseLockPattern.RESULT_FINISHED, intent));
+
+ assertThat(ret).isTrue();
+ assertThat(mAutoCredentialViewModel.getActionLiveData().getValue()).isNull();
+ assertThat(credentialModel.getSensorId()).isEqualTo(newSensorId);
+ assertThat(credentialModel.getChallenge()).isEqualTo(newChallenge);
+ assertThat(CredentialModel.isValidToken(credentialModel.getToken())).isTrue();
+ assertThat(CredentialModel.isValidGkPwHandle(credentialModel.getGkPwHandle())).isFalse();
+ assertThat(mChallengeGenerator.mCallbackRunCount).isEqualTo(1);
+ }
+
+
+ @Test
+ public void testCheckNewCredentialFromActivityResult_validConfirmLock() {
+ final int userId = 109;
+ final CredentialModel credentialModel = newInvalidChallengeCredentialModel(userId);
+ mAutoCredentialViewModel.setCredentialModel(credentialModel);
+ when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
+ PASSWORD_QUALITY_SOMETHING);
+
+ final long gkPwHandle = 5555L;
+ final int newSensorId = 80;
+ final long newChallenge = 90L;
+ setupGenerateTokenFlow(gkPwHandle, userId, newSensorId, newChallenge);
+ final Intent intent = new Intent();
+ intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gkPwHandle);
+
+ // Run checkNewCredentialFromActivityResult()
+ final boolean ret = mAutoCredentialViewModel.checkNewCredentialFromActivityResult(false,
+ new ActivityResult(Activity.RESULT_OK, intent));
+
+ assertThat(ret).isTrue();
+ assertThat(mAutoCredentialViewModel.getActionLiveData().getValue()).isNull();
+ assertThat(credentialModel.getSensorId()).isEqualTo(newSensorId);
+ assertThat(credentialModel.getChallenge()).isEqualTo(newChallenge);
+ assertThat(CredentialModel.isValidToken(credentialModel.getToken())).isTrue();
+ assertThat(CredentialModel.isValidGkPwHandle(credentialModel.getGkPwHandle())).isFalse();
+ assertThat(mChallengeGenerator.mCallbackRunCount).isEqualTo(1);
+ }
+
+ public static class TestChallengeGenerator implements ChallengeGenerator {
+ public int mSensorId = INVALID_SENSOR_ID;
+ public int mUserId = UserHandle.myUserId();
+ public long mChallenge = INVALID_CHALLENGE;
+ public int mCallbackRunCount = 0;
+ private GenerateChallengeCallback mCallback;
+
+ @Nullable
+ @Override
+ public GenerateChallengeCallback getCallback() {
+ return mCallback;
+ }
+
+ @Override
+ public void setCallback(@Nullable GenerateChallengeCallback callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void generateChallenge(int userId) {
+ final GenerateChallengeCallback callback = mCallback;
+ if (callback == null) {
+ return;
+ }
+ callback.onChallengeGenerated(mSensorId, mUserId, mChallenge);
+ ++mCallbackRunCount;
+ }
+ }
+
+ private VerifyCredentialResponse newGoodCredential(long gkPwHandle, @NonNull byte[] hat) {
+ return new VerifyCredentialResponse.Builder()
+ .setGatekeeperPasswordHandle(gkPwHandle)
+ .setGatekeeperHAT(hat)
+ .build();
+ }
+}
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
new file mode 100644
index 00000000000..5069ea1962a
--- /dev/null
+++ b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModelTest.java
@@ -0,0 +1,259 @@
+/*
+ * 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.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.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;
+import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_UNKNOWN;
+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.util.EnrollmentRequestUtil.newAllFalseRequest;
+import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newIsSuwDeferredRequest;
+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;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.app.Application;
+import android.content.res.Resources;
+import android.hardware.fingerprint.FingerprintManager;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
+import com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus;
+import com.android.settings.testutils.InstantTaskExecutorRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@RunWith(AndroidJUnit4.class)
+public class FingerprintEnrollIntroViewModelTest {
+
+ @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+ @Rule public final InstantTaskExecutorRule mTaskExecutorRule = new InstantTaskExecutorRule();
+
+ @Mock private Resources mResources;
+ @Mock private LifecycleOwner mLifecycleOwner;
+ @Mock private FingerprintManager mFingerprintManager;
+
+ private Application mApplication;
+ private FingerprintRepository mFingerprintRepository;
+ private FingerprintEnrollIntroViewModel mViewModel;
+
+ @Before
+ public void setUp() {
+ mApplication = ApplicationProvider.getApplicationContext();
+ mFingerprintRepository = new FingerprintRepository(mFingerprintManager);
+ mViewModel = new FingerprintEnrollIntroViewModel(mApplication, mFingerprintRepository);
+ // MediatorLiveData won't update itself unless observed
+ mViewModel.getPageStatusLiveData().observeForever(event -> {});
+ }
+
+ @Test
+ public void testPageStatusLiveDataDefaultValue() {
+ final FingerprintEnrollIntroStatus status = mViewModel.getPageStatusLiveData().getValue();
+ assertThat(status.hasScrollToBottom()).isFalse();
+ assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_UNKNOWN);
+ }
+
+ @Test
+ public void testGetEnrollmentRequest() {
+ final EnrollmentRequest request = newAllFalseRequest(mApplication);
+
+ mViewModel.setEnrollmentRequest(request);
+
+ assertThat(mViewModel.getEnrollmentRequest()).isEqualTo(request);
+ }
+
+ @Test
+ public void testOnStartToUpdateEnrollableStatus_isSuw() {
+ final int userId = 44;
+ mViewModel.setUserId(userId);
+ mViewModel.setEnrollmentRequest(newIsSuwRequest(mApplication));
+
+ setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, 0);
+ setupSuwMaxFingerprintsEnrollable(mApplication, mResources, 1);
+ mViewModel.onStart(mLifecycleOwner);
+ FingerprintEnrollIntroStatus status = mViewModel.getPageStatusLiveData().getValue();
+ assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_OK);
+
+ setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, 1);
+ setupSuwMaxFingerprintsEnrollable(mApplication, mResources, 1);
+ mViewModel.onStart(mLifecycleOwner);
+ status = mViewModel.getPageStatusLiveData().getValue();
+ assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX);
+ }
+
+ @Test
+ public void testOnStartToUpdateEnrollableStatus_isNotSuw() {
+ testOnStartToUpdateEnrollableStatus(newAllFalseRequest(mApplication));
+ }
+
+ @Test
+ public void testOnStartToUpdateEnrollableStatus_isSuwDeferred() {
+ testOnStartToUpdateEnrollableStatus(newIsSuwDeferredRequest(mApplication));
+ }
+
+ @Test
+ public void testOnStartToUpdateEnrollableStatus_isSuwPortal() {
+ testOnStartToUpdateEnrollableStatus(newIsSuwPortalRequest(mApplication));
+ }
+
+ @Test
+ public void testOnStartToUpdateEnrollableStatus_isSuwSuggestedActionFlow() {
+ testOnStartToUpdateEnrollableStatus(newIsSuwSuggestedActionFlowRequest(mApplication));
+ }
+
+ private void testOnStartToUpdateEnrollableStatus(@NonNull EnrollmentRequest request) {
+ final int userId = 45;
+ mViewModel.setUserId(userId);
+ mViewModel.setEnrollmentRequest(request);
+
+ setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, 0);
+ setupFingerprintFirstSensor(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5);
+ mViewModel.onStart(mLifecycleOwner);
+ FingerprintEnrollIntroStatus status = mViewModel.getPageStatusLiveData().getValue();
+ assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_OK);
+
+ setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, 5);
+ setupFingerprintFirstSensor(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5);
+ mViewModel.onStart(mLifecycleOwner);
+ status = mViewModel.getPageStatusLiveData().getValue();
+ assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX);
+ }
+
+ @Test
+ public void textCanAssumeUdfps() {
+ setupFingerprintFirstSensor(mFingerprintManager, TYPE_UDFPS_ULTRASONIC, 1);
+ assertThat(mViewModel.canAssumeUdfps()).isEqualTo(true);
+
+ setupFingerprintFirstSensor(mFingerprintManager, TYPE_REAR, 1);
+ assertThat(mViewModel.canAssumeUdfps()).isEqualTo(false);
+ }
+
+ @Test
+ public void testIsParentalConsentRequired() {
+ // We shall not mock FingerprintRepository, but
+ // FingerprintRepository.isParentalConsentRequired() calls static method inside, we can't
+ // mock static method
+ final FingerprintRepository fingerprintRepository = mock(FingerprintRepository.class);
+ mViewModel = new FingerprintEnrollIntroViewModel(mApplication, fingerprintRepository);
+
+ when(fingerprintRepository.isParentalConsentRequired(mApplication)).thenReturn(true);
+ assertThat(mViewModel.isParentalConsentRequired()).isEqualTo(true);
+
+ when(fingerprintRepository.isParentalConsentRequired(mApplication)).thenReturn(false);
+ assertThat(mViewModel.isParentalConsentRequired()).isEqualTo(false);
+ }
+
+ @Test
+ public void testIsBiometricUnlockDisabledByAdmin() {
+ // We shall not mock FingerprintRepository, but
+ // FingerprintRepository.isDisabledByAdmin() calls static method inside, we can't mock
+ // static method
+ final FingerprintRepository fingerprintRepository = mock(FingerprintRepository.class);
+ mViewModel = new FingerprintEnrollIntroViewModel(mApplication, fingerprintRepository);
+
+ final int userId = 33;
+ mViewModel.setUserId(userId);
+
+ when(fingerprintRepository.isDisabledByAdmin(mApplication, userId)).thenReturn(true);
+ assertThat(mViewModel.isBiometricUnlockDisabledByAdmin()).isEqualTo(true);
+
+ when(fingerprintRepository.isDisabledByAdmin(mApplication, userId)).thenReturn(false);
+ assertThat(mViewModel.isBiometricUnlockDisabledByAdmin()).isEqualTo(false);
+ }
+
+ @Test
+ public void testSetHasScrolledToBottom() {
+ mViewModel.setHasScrolledToBottom();
+
+ FingerprintEnrollIntroStatus status = mViewModel.getPageStatusLiveData().getValue();
+
+ assertThat(status.hasScrollToBottom()).isEqualTo(true);
+ }
+
+ @Test
+ public void testOnNextButtonClick_enrollNext() {
+ final int userId = 46;
+ mViewModel.setUserId(userId);
+ mViewModel.setEnrollmentRequest(newIsSuwRequest(mApplication));
+
+ // Set latest status to FINGERPRINT_ENROLLABLE_OK
+ setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, 0);
+ setupSuwMaxFingerprintsEnrollable(mApplication, mResources, 1);
+ mViewModel.onStart(mLifecycleOwner);
+ FingerprintEnrollIntroStatus status = mViewModel.getPageStatusLiveData().getValue();
+ assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_OK);
+
+ // Perform click on `next`
+ mViewModel.onNextButtonClick(null);
+
+ assertThat(mViewModel.getActionLiveData().getValue())
+ .isEqualTo(FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL);
+ }
+
+ @Test
+ public void testOnNextButtonClick_doneAndFinish() {
+ final int userId = 46;
+ mViewModel.setUserId(userId);
+ mViewModel.setEnrollmentRequest(newIsSuwRequest(mApplication));
+
+ // Set latest status to FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX
+ setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, 1);
+ setupSuwMaxFingerprintsEnrollable(mApplication, mResources, 1);
+ mViewModel.onStart(mLifecycleOwner);
+ FingerprintEnrollIntroStatus status = mViewModel.getPageStatusLiveData().getValue();
+ assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX);
+
+ // Perform click on `next`
+ mViewModel.onNextButtonClick(null);
+
+ assertThat(mViewModel.getActionLiveData().getValue())
+ .isEqualTo(FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH);
+ }
+
+ @Test
+ public void testOnSkipOrCancelButtonClick() {
+ mViewModel.onSkipOrCancelButtonClick(null);
+
+ 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
new file mode 100644
index 00000000000..b1d55aa332d
--- /dev/null
+++ b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModelTest.java
@@ -0,0 +1,252 @@
+/*
+ * 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.biometrics.BiometricEnrollBase.RESULT_FINISHED;
+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.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;
+
+import static org.mockito.Mockito.when;
+
+import android.app.Application;
+import android.app.KeyguardManager;
+import android.content.Intent;
+import android.hardware.fingerprint.FingerprintManager;
+import android.os.Bundle;
+
+import androidx.activity.result.ActivityResult;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
+import com.android.settings.password.SetupSkipDialog;
+import com.android.settings.testutils.InstantTaskExecutorRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@RunWith(AndroidJUnit4.class)
+public class FingerprintEnrollmentViewModelTest {
+
+ @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+ @Rule public final InstantTaskExecutorRule mTaskExecutorRule = new InstantTaskExecutorRule();
+
+ @Mock private FingerprintManager mFingerprintManager;
+ @Mock private KeyguardManager mKeyguardManager;
+
+ private Application mApplication;
+ private FingerprintRepository mFingerprintRepository;
+ private FingerprintEnrollmentViewModel mViewModel;
+
+ @Before
+ public void setUp() {
+ mApplication = ApplicationProvider.getApplicationContext();
+ mFingerprintRepository = new FingerprintRepository(mFingerprintManager);
+ mViewModel = new FingerprintEnrollmentViewModel(mApplication, mFingerprintRepository,
+ mKeyguardManager);
+ }
+
+ @Test
+ public void testGetRequest() {
+ when(mKeyguardManager.isKeyguardSecure()).thenReturn(true);
+ assertThat(mViewModel.getRequest()).isNull();
+
+ final EnrollmentRequest request = newAllFalseRequest(mApplication);
+ mViewModel.setRequest(request);
+ assertThat(mViewModel.getRequest()).isEqualTo(request);
+ }
+
+ @Test
+ public void testGetNextActivityBaseIntentExtras() {
+ mViewModel.setRequest(newAllFalseRequest(mApplication));
+ assertThat(mViewModel.getNextActivityBaseIntentExtras()).isNotNull();
+ }
+
+ @Test
+ public void testOnContinueEnrollActivityResult_shouldRelaySkip1Result() {
+ mViewModel.setRequest(newAllFalseRequest(mApplication));
+ final ActivityResult result = new ActivityResult(RESULT_SKIP, null);
+
+ // Run onContinueEnrollActivityResult
+ mViewModel.onContinueEnrollActivityResult(result, 100);
+
+ assertThat(mViewModel.getSetResultLiveData().getValue()).isEqualTo(result);
+ }
+
+ @Test
+ public void testOnContinueEnrollActivityResult_shouldRelaySkip2Result() {
+ mViewModel.setRequest(newAllFalseRequest(mApplication));
+ final ActivityResult result = new ActivityResult(SetupSkipDialog.RESULT_SKIP, null);
+
+ // Run onContinueEnrollActivityResult
+ mViewModel.onContinueEnrollActivityResult(result, 100);
+
+ assertThat(mViewModel.getSetResultLiveData().getValue()).isEqualTo(result);
+ }
+
+ @Test
+ public void testOnContinueEnrollActivityResult_shouldRelayNullDataTimeoutResult() {
+ mViewModel.setRequest(newAllFalseRequest(mApplication));
+ final ActivityResult result = new ActivityResult(RESULT_TIMEOUT, null);
+
+ // Run onContinueEnrollActivityResult
+ mViewModel.onContinueEnrollActivityResult(result, 100);
+ final ActivityResult setResult = mViewModel.getSetResultLiveData().getValue();
+
+ assertThat(setResult).isNotNull();
+ assertThat(setResult.getResultCode()).isEqualTo(result.getResultCode());
+ assertThat(setResult.getData()).isEqualTo(result.getData());
+ }
+
+ @Test
+ public void testOnContinueEnrollActivityResult_shouldRelayWithDataTimeoutResult() {
+ mViewModel.setRequest(newAllFalseRequest(mApplication));
+ final Intent intent = new Intent("testAction");
+ intent.putExtra("testKey", "testValue");
+ final ActivityResult result = new ActivityResult(RESULT_TIMEOUT, intent);
+
+ // Run onContinueEnrollActivityResult
+ mViewModel.onContinueEnrollActivityResult(result, 100);
+ final ActivityResult setResult = mViewModel.getSetResultLiveData().getValue();
+
+ assertThat(setResult).isNotNull();
+ assertThat(setResult.getResultCode()).isEqualTo(result.getResultCode());
+ assertThat(setResult.getData()).isEqualTo(intent);
+ }
+
+ @Test
+ public void testOnContinueEnrollActivityResult_shouldRelayNullDataFinishResult() {
+ mViewModel.setRequest(newAllFalseRequest(mApplication));
+ final ActivityResult result = new ActivityResult(RESULT_FINISHED, null);
+
+ // Run onContinueEnrollActivityResult
+ mViewModel.onContinueEnrollActivityResult(result, 100);
+ final ActivityResult setResult = mViewModel.getSetResultLiveData().getValue();
+
+ assertThat(setResult).isNotNull();
+ assertThat(setResult.getResultCode()).isEqualTo(result.getResultCode());
+ assertThat(setResult.getData()).isEqualTo(result.getData());
+ }
+
+ @Test
+ public void testOnContinueEnrollActivityResult_shouldRelayWithDataFinishResult() {
+ mViewModel.setRequest(newAllFalseRequest(mApplication));
+ final Intent intent = new Intent("testAction");
+ intent.putExtra("testKey", "testValue");
+ final ActivityResult result = new ActivityResult(RESULT_FINISHED, intent);
+
+ // Run onContinueEnrollActivityResult
+ mViewModel.onContinueEnrollActivityResult(result, 100);
+ final ActivityResult setResult = mViewModel.getSetResultLiveData().getValue();
+
+ assertThat(setResult).isNotNull();
+ assertThat(setResult.getResultCode()).isEqualTo(result.getResultCode());
+ assertThat(setResult.getData()).isEqualTo(intent);
+ }
+
+ @Test
+ public void testOnContinueEnrollActivityResult_shouldRelayNullDataFinishResultAsNewData() {
+ when(mKeyguardManager.isKeyguardSecure()).thenReturn(true);
+ final int userId = 111;
+ final int numOfFp = 4;
+ setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, numOfFp);
+ mViewModel.setRequest(newIsSuwRequest(mApplication));
+ final ActivityResult result = new ActivityResult(RESULT_FINISHED, null);
+
+ // Run onContinueEnrollActivityResult
+ mViewModel.onContinueEnrollActivityResult(result, userId);
+ final ActivityResult setResult = mViewModel.getSetResultLiveData().getValue();
+
+ assertThat(setResult).isNotNull();
+ assertThat(setResult.getResultCode()).isEqualTo(result.getResultCode());
+ assertThat(setResult.getData()).isNotNull();
+ assertThat(setResult.getData().getExtras()).isNotNull();
+ assertThat(setResult.getData().getExtras().getInt(EXTRA_FINGERPRINT_ENROLLED_COUNT, -1))
+ .isEqualTo(numOfFp);
+ }
+
+ @Test
+ public void testOnContinueEnrollActivityResult_shouldRelayWithDataFinishResultAsNewData() {
+ when(mKeyguardManager.isKeyguardSecure()).thenReturn(true);
+ final int userId = 20;
+ final int numOfFp = 9;
+ setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, numOfFp);
+ mViewModel.setRequest(newIsSuwRequest(mApplication));
+ final String action = "testAction";
+ final String key = "testKey";
+ final String value = "testValue";
+ final Intent intent = new Intent(action);
+ intent.putExtra(key, value);
+ final ActivityResult result = new ActivityResult(RESULT_FINISHED, intent);
+
+ // Run onContinueEnrollActivityResult
+ mViewModel.onContinueEnrollActivityResult(result, userId);
+ final ActivityResult setResult = mViewModel.getSetResultLiveData().getValue();
+
+ assertThat(setResult).isNotNull();
+ assertThat(setResult.getResultCode()).isEqualTo(result.getResultCode());
+ assertThat(setResult.getData()).isNotNull();
+ assertThat(setResult.getData().getExtras()).isNotNull();
+ assertThat(setResult.getData().getExtras().getInt(EXTRA_FINGERPRINT_ENROLLED_COUNT, -1))
+ .isEqualTo(numOfFp);
+ assertThat(setResult.getData().getExtras().getString(key)).isEqualTo(value);
+ }
+
+ @Test
+ public void testSetSavedInstanceState() {
+ final Bundle bundle = new Bundle();
+ mViewModel.isWaitingActivityResult().set(true);
+
+ // setSavedInstanceState() as false
+ bundle.putBoolean(SAVED_STATE_IS_WAITING_ACTIVITY_RESULT, false);
+ mViewModel.setSavedInstanceState(bundle);
+ assertThat(mViewModel.isWaitingActivityResult().get()).isFalse();
+
+ // setSavedInstanceState() as false
+ bundle.putBoolean(SAVED_STATE_IS_WAITING_ACTIVITY_RESULT, true);
+ mViewModel.setSavedInstanceState(bundle);
+ assertThat(mViewModel.isWaitingActivityResult().get()).isTrue();
+ }
+
+ @Test
+ public void testOnSaveInstanceState() {
+ final Bundle bundle = new Bundle();
+
+ // setSavedInstanceState() as false
+ mViewModel.isWaitingActivityResult().set(false);
+ mViewModel.onSaveInstanceState(bundle);
+ assertThat(bundle.getBoolean(SAVED_STATE_IS_WAITING_ACTIVITY_RESULT)).isFalse();
+
+ // setSavedInstanceState() as false
+ mViewModel.isWaitingActivityResult().set(true);
+ mViewModel.onSaveInstanceState(bundle);
+ assertThat(bundle.getBoolean(SAVED_STATE_IS_WAITING_ACTIVITY_RESULT)).isTrue();
+ }
+}
diff --git a/tests/unit/src/com/android/settings/biometrics2/util/EnrollmentRequestUtil.java b/tests/unit/src/com/android/settings/biometrics2/util/EnrollmentRequestUtil.java
new file mode 100644
index 00000000000..5977c57af2c
--- /dev/null
+++ b/tests/unit/src/com/android/settings/biometrics2/util/EnrollmentRequestUtil.java
@@ -0,0 +1,81 @@
+/*
+ * 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 com.android.settings.biometrics.BiometricEnrollBase.EXTRA_FROM_SETTINGS_SUMMARY;
+
+import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_DEFERRED_SETUP;
+import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_FIRST_RUN;
+import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_PORTAL_SETUP;
+import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_SETUP_FLOW;
+import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_SUW_SUGGESTED_ACTION_FLOW;
+import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_THEME;
+
+import android.content.Context;
+import android.content.Intent;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+
+import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
+
+public class EnrollmentRequestUtil {
+
+ @NonNull
+ public static EnrollmentRequest newAllFalseRequest(@NonNull Context context) {
+ return newRequest(context, false, false, false, false, false, false, null);
+ }
+
+ @NonNull
+ public static EnrollmentRequest newIsSuwRequest(@NonNull Context context) {
+ return newRequest(context, true, false, false, false, false, false, null);
+ }
+
+ @NonNull
+ public static EnrollmentRequest newIsSuwDeferredRequest(@NonNull Context context) {
+ return newRequest(context, true, true, false, false, false, false, null);
+ }
+
+ @NonNull
+ public static EnrollmentRequest newIsSuwPortalRequest(@NonNull Context context) {
+ return newRequest(context, true, false, true, false, false, false, null);
+ }
+
+ @NonNull
+ public static EnrollmentRequest newIsSuwSuggestedActionFlowRequest(
+ @NonNull Context context) {
+ return newRequest(context, true, false, false, true, false, false, null);
+ }
+
+ @NonNull
+ public static EnrollmentRequest newRequest(@NonNull Context context, boolean isSuw,
+ boolean isSuwDeferred, boolean isSuwPortal, boolean isSuwSuggestedActionFlow,
+ boolean isSuwFirstRun, boolean isFromSettingsSummery, String theme) {
+ Intent i = new Intent();
+ i.putExtra(EXTRA_IS_SETUP_FLOW, isSuw);
+ i.putExtra(EXTRA_IS_DEFERRED_SETUP, isSuwDeferred);
+ i.putExtra(EXTRA_IS_PORTAL_SETUP, isSuwPortal);
+ i.putExtra(EXTRA_IS_SUW_SUGGESTED_ACTION_FLOW, isSuwSuggestedActionFlow);
+ i.putExtra(EXTRA_IS_FIRST_RUN, isSuwFirstRun);
+ i.putExtra(EXTRA_FROM_SETTINGS_SUMMARY, isFromSettingsSummery);
+ if (!TextUtils.isEmpty(theme)) {
+ i.putExtra(EXTRA_THEME, theme);
+ }
+ return new EnrollmentRequest(i, context);
+ }
+
+}
diff --git a/tests/unit/src/com/android/settings/biometrics2/util/FingerprintManagerUtil.java b/tests/unit/src/com/android/settings/biometrics2/util/FingerprintManagerUtil.java
new file mode 100644
index 00000000000..cb45fa4c6c2
--- /dev/null
+++ b/tests/unit/src/com/android/settings/biometrics2/util/FingerprintManagerUtil.java
@@ -0,0 +1,58 @@
+/*
+ * 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);
+ }
+}
diff --git a/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java b/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java
index 8f57f4ea235..d4127d72f9d 100644
--- a/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java
+++ b/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java
@@ -25,6 +25,7 @@ import com.android.settings.accounts.AccountFeatureProvider;
import com.android.settings.applications.ApplicationFeatureProvider;
import com.android.settings.aware.AwareFeatureProvider;
import com.android.settings.biometrics.face.FaceFeatureProvider;
+import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider;
import com.android.settings.bluetooth.BluetoothFeatureProvider;
import com.android.settings.dashboard.DashboardFeatureProvider;
import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider;
@@ -73,6 +74,7 @@ public class FakeFeatureFactory extends FeatureFactory {
public final BluetoothFeatureProvider mBluetoothFeatureProvider;
public final AwareFeatureProvider mAwareFeatureProvider;
public final FaceFeatureProvider mFaceFeatureProvider;
+ public final BiometricsRepositoryProvider mBiometricsRepositoryProvider;
public PanelFeatureProvider panelFeatureProvider;
public SlicesFeatureProvider slicesFeatureProvider;
@@ -120,6 +122,7 @@ public class FakeFeatureFactory extends FeatureFactory {
mBluetoothFeatureProvider = mock(BluetoothFeatureProvider.class);
mAwareFeatureProvider = mock(AwareFeatureProvider.class);
mFaceFeatureProvider = mock(FaceFeatureProvider.class);
+ mBiometricsRepositoryProvider = mock(BiometricsRepositoryProvider.class);
wifiTrackerLibProvider = mock(WifiTrackerLibProvider.class);
securitySettingsFeatureProvider = mock(SecuritySettingsFeatureProvider.class);
mAccessibilitySearchFeatureProvider = mock(AccessibilitySearchFeatureProvider.class);
@@ -242,6 +245,11 @@ public class FakeFeatureFactory extends FeatureFactory {
return mFaceFeatureProvider;
}
+ @Override
+ public BiometricsRepositoryProvider getBiometricsRepositoryProvider() {
+ return mBiometricsRepositoryProvider;
+ }
+
@Override
public WifiTrackerLibProvider getWifiTrackerLibProvider() {
return wifiTrackerLibProvider;
diff --git a/tests/unit/src/com/android/settings/testutils/InstantTaskExecutorRule.java b/tests/unit/src/com/android/settings/testutils/InstantTaskExecutorRule.java
new file mode 100644
index 00000000000..0a455718df5
--- /dev/null
+++ b/tests/unit/src/com/android/settings/testutils/InstantTaskExecutorRule.java
@@ -0,0 +1,59 @@
+/*
+ * 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.testutils;
+
+import androidx.arch.core.executor.ArchTaskExecutor;
+import androidx.arch.core.executor.TaskExecutor;
+
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+
+/**
+ * A JUnit Test Rule that swaps the background executor used by the Architecture Components with a
+ * different one which executes each task synchronously.
+ *
+ * We can't refer it in prebuilt androidX library.
+ * Copied it from androidx/arch/core/executor/testing/InstantTaskExecutorRule.java
+ */
+public class InstantTaskExecutorRule extends TestWatcher {
+ @Override
+ protected void starting(Description description) {
+ super.starting(description);
+ ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() {
+ @Override
+ public void executeOnDiskIO(Runnable runnable) {
+ runnable.run();
+ }
+
+ @Override
+ public void postToMainThread(Runnable runnable) {
+ runnable.run();
+ }
+
+ @Override
+ public boolean isMainThread() {
+ return true;
+ }
+ });
+ }
+
+ @Override
+ protected void finished(Description description) {
+ super.finished(description);
+ ArchTaskExecutor.getInstance().setDelegate(null);
+ }
+}