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 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); + } +}