From acb8be5d257a5c581f77011503e56736fc5a9b78 Mon Sep 17 00:00:00 2001 From: Milton Wu Date: Mon, 24 Jul 2023 16:29:20 +0800 Subject: [PATCH] [BiometricsV2] Refactor AutoCredentialViewModel Refactor AutoCredentialViewModelTest and FingerprintEnrollmentViewModel to kotlin and change LiveData to Flow Bug: 286197659 Test: atest -m CredentialModelTest Test: atest -m AutoCredentialViewModelTest Test: atest -m FingerprintEnrollmentViewModelTest Test: atest -m FingerprintEnrollmentActivityTest Test: atest -m biometrics-enrollment-test Change-Id: I84bab0b46e023303c0046a6ae6886ab1cf9458b8 --- .../factory/BiometricsViewModelFactory.java | 38 +- .../biometrics2/ui/model/CredentialModel.kt | 14 - .../ui/view/FingerprintEnrollmentActivity.kt | 90 ++- .../ui/viewmodel/AutoCredentialViewModel.java | 393 ------------ .../ui/viewmodel/AutoCredentialViewModel.kt | 300 +++++++++ .../FingerprintEnrollmentViewModel.kt | 34 +- .../ui/model/CredentialModelTest.kt | 47 -- .../AutoCredentialViewModelTest.java | 596 ------------------ .../viewmodel/AutoCredentialViewModelTest.kt | 541 ++++++++++++++++ .../FingerprintEnrollmentViewModelTest.kt | 178 +++++- 10 files changed, 1067 insertions(+), 1164 deletions(-) delete mode 100644 src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModel.java create mode 100644 src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModel.kt delete mode 100644 tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModelTest.java create mode 100644 tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModelTest.kt diff --git a/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java b/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java index b83614c59f8..2cf607a7311 100644 --- a/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java +++ b/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java @@ -28,6 +28,7 @@ import androidx.lifecycle.viewmodel.CreationExtras; import com.android.internal.widget.LockPatternUtils; import com.android.settings.biometrics.fingerprint.FingerprintUpdater; import com.android.settings.biometrics2.data.repository.FingerprintRepository; +import com.android.settings.biometrics2.ui.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.ChallengeGenerator; @@ -54,8 +55,8 @@ public class BiometricsViewModelFactory implements ViewModelProvider.Factory { new CreationExtras.Key() {}; public static final CreationExtras.Key ENROLLMENT_REQUEST_KEY = new CreationExtras.Key() {}; - public static final CreationExtras.Key USER_ID_KEY = - new CreationExtras.Key() {}; + public static final CreationExtras.Key CREDENTIAL_MODEL_KEY = + new CreationExtras.Key() {}; @NonNull @Override @@ -76,9 +77,10 @@ public class BiometricsViewModelFactory implements ViewModelProvider.Factory { final LockPatternUtils lockPatternUtils = featureFactory.getSecurityFeatureProvider().getLockPatternUtils(application); final ChallengeGenerator challengeGenerator = extras.get(CHALLENGE_GENERATOR_KEY); - if (challengeGenerator != null) { + final CredentialModel credentialModel = extras.get(CREDENTIAL_MODEL_KEY); + if (challengeGenerator != null && credentialModel != null) { return (T) new AutoCredentialViewModel(application, lockPatternUtils, - challengeGenerator); + challengeGenerator, credentialModel); } } else if (modelClass.isAssignableFrom(DeviceFoldedViewModel.class)) { return (T) new DeviceFoldedViewModel(new ScreenSizeFoldProvider(application), @@ -93,10 +95,10 @@ public class BiometricsViewModelFactory implements ViewModelProvider.Factory { } else if (modelClass.isAssignableFrom(FingerprintEnrollIntroViewModel.class)) { final FingerprintRepository repository = provider.getFingerprintRepository(application); final EnrollmentRequest request = extras.get(ENROLLMENT_REQUEST_KEY); - final Integer userId = extras.get(USER_ID_KEY); - if (repository != null && request != null && userId != null) { + final CredentialModel credentialModel = extras.get(CREDENTIAL_MODEL_KEY); + if (repository != null && request != null && credentialModel != null) { return (T) new FingerprintEnrollIntroViewModel(application, repository, request, - userId); + credentialModel.getUserId()); } } else if (modelClass.isAssignableFrom(FingerprintEnrollmentViewModel.class)) { final FingerprintRepository repository = provider.getFingerprintRepository(application); @@ -105,27 +107,27 @@ public class BiometricsViewModelFactory implements ViewModelProvider.Factory { return (T) new FingerprintEnrollmentViewModel(application, repository, request); } } else if (modelClass.isAssignableFrom(FingerprintEnrollProgressViewModel.class)) { - final Integer userId = extras.get(USER_ID_KEY); - if (userId != null) { + final CredentialModel credentialModel = extras.get(CREDENTIAL_MODEL_KEY); + if (credentialModel != null) { return (T) new FingerprintEnrollProgressViewModel(application, - new FingerprintUpdater(application), userId); + new FingerprintUpdater(application), credentialModel.getUserId()); } } else if (modelClass.isAssignableFrom(FingerprintEnrollEnrollingViewModel.class)) { - final Integer userId = extras.get(USER_ID_KEY); + final CredentialModel credentialModel = extras.get(CREDENTIAL_MODEL_KEY); final FingerprintRepository fingerprint = provider.getFingerprintRepository( application); - if (fingerprint != null && userId != null) { - return (T) new FingerprintEnrollEnrollingViewModel(application, userId, - fingerprint); + if (fingerprint != null && credentialModel != null) { + return (T) new FingerprintEnrollEnrollingViewModel(application, + credentialModel.getUserId(), fingerprint); } } else if (modelClass.isAssignableFrom(FingerprintEnrollFinishViewModel.class)) { - final Integer userId = extras.get(USER_ID_KEY); + final CredentialModel credentialModel = extras.get(CREDENTIAL_MODEL_KEY); final EnrollmentRequest request = extras.get(ENROLLMENT_REQUEST_KEY); final FingerprintRepository fingerprint = provider.getFingerprintRepository( application); - if (fingerprint != null && userId != null && request != null) { - return (T) new FingerprintEnrollFinishViewModel(application, userId, request, - fingerprint); + if (fingerprint != null && credentialModel != null && request != null) { + return (T) new FingerprintEnrollFinishViewModel(application, + credentialModel.getUserId(), request, fingerprint); } } else if (modelClass.isAssignableFrom(FingerprintEnrollErrorDialogViewModel.class)) { final EnrollmentRequest request = extras.get(ENROLLMENT_REQUEST_KEY); diff --git a/src/com/android/settings/biometrics2/ui/model/CredentialModel.kt b/src/com/android/settings/biometrics2/ui/model/CredentialModel.kt index 7999ab8e1bb..53507330b45 100644 --- a/src/com/android/settings/biometrics2/ui/model/CredentialModel.kt +++ b/src/com/android/settings/biometrics2/ui/model/CredentialModel.kt @@ -80,20 +80,6 @@ class CredentialModel(bundle: Bundle?, private val clock: Clock) { val isValidToken: Boolean get() = token != null - val bundle: Bundle - /** - * Get a bundle which can be used to recreate CredentialModel - */ - get() { - val bundle = Bundle() - bundle.putInt(EXTRA_USER_ID, userId) - bundle.putLong(EXTRA_KEY_CHALLENGE, challenge) - bundle.putByteArray(EXTRA_KEY_CHALLENGE_TOKEN, token) - bundle.putLong(EXTRA_KEY_GK_PW_HANDLE, gkPwHandle) - return bundle - } - - /** Returns a string representation of the object */ override fun toString(): String { val gkPwHandleLen = "$gkPwHandle".length diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.kt b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.kt index fa8a3fabaf8..562b7dd317b 100644 --- a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.kt +++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.kt @@ -44,16 +44,13 @@ import com.android.settings.Utils import com.android.settings.biometrics.BiometricEnrollBase import com.android.settings.biometrics2.factory.BiometricsViewModelFactory import com.android.settings.biometrics2.factory.BiometricsViewModelFactory.CHALLENGE_GENERATOR_KEY +import com.android.settings.biometrics2.factory.BiometricsViewModelFactory.CREDENTIAL_MODEL_KEY import com.android.settings.biometrics2.factory.BiometricsViewModelFactory.ENROLLMENT_REQUEST_KEY -import com.android.settings.biometrics2.factory.BiometricsViewModelFactory.USER_ID_KEY 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.CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK -import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK -import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_IS_GENERATING_CHALLENGE -import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_VALID import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.FingerprintChallengeGenerator +import com.android.settings.biometrics2.ui.viewmodel.CredentialAction import com.android.settings.biometrics2.ui.viewmodel.DeviceFoldedViewModel import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel.FINGERPRINT_ENROLL_ENROLLING_ACTION_DONE @@ -170,7 +167,6 @@ open class FingerprintEnrollmentActivity : FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - autoCredentialViewModel.setCredentialModel(savedInstanceState, intent) // Theme setTheme(viewModel.request.theme) @@ -219,14 +215,23 @@ open class FingerprintEnrollmentActivity : FragmentActivity() { } } - // observe LiveData - viewModel.setResultLiveData.observe(this) { - result: ActivityResult -> onSetActivityResult(result) - } - autoCredentialViewModel.generateChallengeFailedLiveData.observe(this) { - _: Boolean -> onGenerateChallengeFailed() - } + collectFlows() + } + + private fun collectFlows() { lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.setResultFlow.collect { + Log.d(TAG, "setResultLiveData($it)") + onSetActivityResult(it) + } + } + repeatOnLifecycle(Lifecycle.State.STARTED) { + autoCredentialViewModel.generateChallengeFailedFlow.collect { + Log.d(TAG, "generateChallengeFailedFlow($it)") + onSetActivityResult(ActivityResult(RESULT_CANCELED, null)) + } + } repeatOnLifecycle(Lifecycle.State.STARTED) { errorDialogViewModel.newDialogFlow.collect { Log.d(TAG, "newErrorDialogFlow($it)") @@ -236,8 +241,6 @@ open class FingerprintEnrollmentActivity : FragmentActivity() { ) } } - } - lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { errorDialogViewModel.setResultFlow.collect { Log.d(TAG, "errorDialogSetResultFlow($it)") @@ -408,10 +411,6 @@ open class FingerprintEnrollmentActivity : FragmentActivity() { } } - private fun onGenerateChallengeFailed() { - onSetActivityResult(ActivityResult(RESULT_CANCELED, null)) - } - private fun onSetActivityResult(result: ActivityResult) { val challengeExtras: Bundle? = autoCredentialViewModel.createGeneratingChallengeExtras() val overrideResult: ActivityResult = viewModel.getOverrideActivityResult( @@ -428,8 +427,8 @@ open class FingerprintEnrollmentActivity : FragmentActivity() { } private fun checkCredential() { - when (autoCredentialViewModel.checkCredential()) { - CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK -> { + when (autoCredentialViewModel.checkCredential(lifecycleScope)) { + CredentialAction.FAIL_NEED_TO_CHOOSE_LOCK -> { val intent: Intent = autoCredentialViewModel.createChooseLockIntent( this, viewModel.request.isSuw, @@ -442,7 +441,7 @@ open class FingerprintEnrollmentActivity : FragmentActivity() { return } - CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK -> { + CredentialAction.FAIL_NEED_TO_CONFIRM_LOCK -> { val launched: Boolean = autoCredentialViewModel.createConfirmLockLauncher( this, LAUNCH_CONFIRM_LOCK_ACTIVITY, @@ -459,21 +458,24 @@ open class FingerprintEnrollmentActivity : FragmentActivity() { return } - CREDENTIAL_VALID, - CREDENTIAL_IS_GENERATING_CHALLENGE -> {} + CredentialAction.CREDENTIAL_VALID, + CredentialAction.IS_GENERATING_CHALLENGE -> {} } } - private fun onChooseOrConfirmLockResult(isChooseLock: Boolean, activityResult: ActivityResult) { + private fun onChooseOrConfirmLockResult( + isChooseLock: Boolean, + activityResult: ActivityResult + ) { if (!viewModel.isWaitingActivityResult.compareAndSet(true, false)) { Log.w(TAG, "isChooseLock:$isChooseLock, fail to unset waiting flag") } - if (autoCredentialViewModel.checkNewCredentialFromActivityResult( - isChooseLock, activityResult + if (!autoCredentialViewModel.generateChallengeAsCredentialActivityResult( + isChooseLock, + activityResult, + lifecycleScope ) ) { - overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out) - } else { onSetActivityResult(activityResult) } } @@ -573,7 +575,11 @@ open class FingerprintEnrollmentActivity : FragmentActivity() { override fun onPause() { super.onPause() - viewModel.checkFinishActivityDuringOnPause(isFinishing, isChangingConfigurations) + viewModel.checkFinishActivityDuringOnPause( + isFinishing, + isChangingConfigurations, + lifecycleScope + ) } override fun onDestroy() { @@ -596,17 +602,14 @@ open class FingerprintEnrollmentActivity : FragmentActivity() { } override val defaultViewModelCreationExtras: CreationExtras - get() { - val fingerprintRepository = featureFactory.biometricsRepositoryProvider - .getFingerprintRepository(application)!! - val credentialModel = CredentialModel(intent.extras, SystemClock.elapsedRealtimeClock()) - - return MutableCreationExtras(super.defaultViewModelCreationExtras).also { - it[CHALLENGE_GENERATOR_KEY] = FingerprintChallengeGenerator(fingerprintRepository) - it[ENROLLMENT_REQUEST_KEY] = - EnrollmentRequest(intent, applicationContext, this is SetupActivity) - it[USER_ID_KEY] = credentialModel.userId - } + get() = MutableCreationExtras(super.defaultViewModelCreationExtras).also { + it[CHALLENGE_GENERATOR_KEY] = FingerprintChallengeGenerator( + featureFactory.biometricsRepositoryProvider.getFingerprintRepository(application)!! + ) + it[ENROLLMENT_REQUEST_KEY] = + EnrollmentRequest(intent, applicationContext, this is SetupActivity) + it[CREDENTIAL_MODEL_KEY] = + CredentialModel(intent.extras, SystemClock.elapsedRealtimeClock()) } override val defaultViewModelProviderFactory: ViewModelProvider.Factory @@ -630,11 +633,6 @@ open class FingerprintEnrollmentActivity : FragmentActivity() { super.onConfigurationChanged(newConfig) } - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - autoCredentialViewModel.onSaveInstanceState(outState) - } - companion object { private const val DEBUG = false private const val TAG = "FingerprintEnrollmentActivity" diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModel.java deleted file mode 100644 index 7e48f82136d..00000000000 --- a/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModel.java +++ /dev/null @@ -1,393 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings.biometrics2.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.biometrics2.ui.model.CredentialModel.INVALID_GK_PW_HANDLE; -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.os.SystemClock; -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.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.biometrics.BiometricUtils.GatekeeperCredentialNotMatchException; -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 { - - private static final String TAG = "AutoCredentialViewModel"; - - @VisibleForTesting - static final String KEY_CREDENTIAL_MODEL = "credential_model"; - - @VisibleForTesting - static final String KEY_IS_GENERATING_CHALLENGE_DURING_CHECKING_CREDENTIAL = - "is_generating_challenge_during_checking_credential"; - - private static final boolean DEBUG = false; - - /** - * Valid credential, activity does nothing. - */ - public static final int CREDENTIAL_VALID = 0; - - /** - * This credential looks good, but still need to run generateChallenge(). - */ - public static final int CREDENTIAL_IS_GENERATING_CHALLENGE = 1; - - /** - * Need activity to run choose lock - */ - public static final int CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK = 2; - - /** - * Need activity to run confirm lock - */ - public static final int CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK = 3; - - @IntDef(prefix = { "CREDENTIAL_" }, value = { - CREDENTIAL_VALID, - CREDENTIAL_IS_GENERATING_CHALLENGE, - CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK, - CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK - }) - @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 mGenerateChallengeFailedLiveData = - new MutableLiveData<>(); - - // flag if token is generating through checkCredential()'s generateChallenge() - private boolean mIsGeneratingChallengeDuringCheckingCredential; - - public AutoCredentialViewModel( - @NonNull Application application, - @NonNull LockPatternUtils lockPatternUtils, - @NonNull ChallengeGenerator challengeGenerator) { - super(application); - mLockPatternUtils = lockPatternUtils; - mChallengeGenerator = challengeGenerator; - } - - /** - * Set CredentialModel, the source is coming from savedInstanceState or activity intent - */ - public void setCredentialModel(@Nullable Bundle savedInstanceState, @NonNull Intent intent) { - final Bundle bundle; - if (savedInstanceState != null) { - bundle = savedInstanceState.getBundle(KEY_CREDENTIAL_MODEL); - mIsGeneratingChallengeDuringCheckingCredential = savedInstanceState.getBoolean( - KEY_IS_GENERATING_CHALLENGE_DURING_CHECKING_CREDENTIAL); - } else { - bundle = intent.getExtras(); - } - mCredentialModel = new CredentialModel(bundle, SystemClock.elapsedRealtimeClock()); - - if (DEBUG) { - Log.d(TAG, "setCredentialModel " + mCredentialModel + ", savedInstanceState exist:" - + (savedInstanceState != null)); - } - } - - /** - * Handle onSaveInstanceState from activity - */ - public void onSaveInstanceState(@NonNull Bundle outState) { - outState.putBoolean(KEY_IS_GENERATING_CHALLENGE_DURING_CHECKING_CREDENTIAL, - mIsGeneratingChallengeDuringCheckingCredential); - outState.putBundle(KEY_CREDENTIAL_MODEL, mCredentialModel.getBundle()); - } - - @NonNull - public LiveData getGenerateChallengeFailedLiveData() { - return mGenerateChallengeFailedLiveData; - } - - /** - * Get bundle which passing back to FingerprintSettings for late generateChallenge() - */ - @Nullable - public Bundle createGeneratingChallengeExtras() { - if (!mIsGeneratingChallengeDuringCheckingCredential - || !mCredentialModel.isValidToken() - || !mCredentialModel.isValidChallenge()) { - return null; - } - - Bundle bundle = new Bundle(); - bundle.putByteArray(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, - mCredentialModel.getToken()); - bundle.putLong(EXTRA_KEY_CHALLENGE, mCredentialModel.getChallenge()); - return bundle; - } - - /** - * Check credential status for biometric enrollment. - */ - @CredentialAction - public int checkCredential() { - if (isValidCredential()) { - return CREDENTIAL_VALID; - } - if (isUnspecifiedPassword()) { - return CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK; - } else if (mCredentialModel.isValidGkPwHandle()) { - final long gkPwHandle = mCredentialModel.getGkPwHandle(); - mCredentialModel.clearGkPwHandle(); - // GkPwHandle is got through caller activity, we shall not revoke it after - // generateChallenge(). Let caller activity to make decision. - generateChallenge(gkPwHandle, false /* revokeGkPwHandle */); - mIsGeneratingChallengeDuringCheckingCredential = true; - return CREDENTIAL_IS_GENERATING_CHALLENGE; - } else { - return CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK; - } - } - - private void generateChallenge(long gkPwHandle, boolean revokeGkPwHandle) { - mChallengeGenerator.setCallback((sensorId, userId, challenge) -> { - try { - final byte[] newToken = requestGatekeeperHat(gkPwHandle, challenge, userId); - mCredentialModel.setChallenge(challenge); - mCredentialModel.setToken(newToken); - } catch (IllegalStateException e) { - Log.e(TAG, "generateChallenge, IllegalStateException", e); - mGenerateChallengeFailedLiveData.postValue(true); - return; - } - - if (revokeGkPwHandle) { - mLockPatternUtils.removeGatekeeperPasswordHandle(gkPwHandle); - } - - if (DEBUG) { - Log.d(TAG, "generateChallenge(), model:" + mCredentialModel - + ", revokeGkPwHandle:" + revokeGkPwHandle); - } - - // Check credential again - if (!isValidCredential()) { - Log.w(TAG, "generateChallenge, invalid Credential"); - mGenerateChallengeFailedLiveData.postValue(true); - } - }); - mChallengeGenerator.generateChallenge(getUserId()); - } - - private boolean isValidCredential() { - return !isUnspecifiedPassword() && mCredentialModel.isValidToken(); - } - - 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); - // Revoke self requested GkPwHandle because it shall only used once inside this - // activity lifecycle. - generateChallenge(gkPwHandle, true /* revokeGkPwHandle */); - return true; - } - } - return false; - } - - /** - * Get userId for this credential - */ - public int getUserId() { - return mCredentialModel.getUserId(); - } - - /** - * Get userId for this credential - */ - @Nullable - public byte[] getToken() { - return mCredentialModel.getToken(); - } - - @Nullable - private byte[] requestGatekeeperHat(long gkPwHandle, long challenge, int userId) - throws IllegalStateException { - final VerifyCredentialResponse response = mLockPatternUtils - .verifyGatekeeperPasswordHandle(gkPwHandle, challenge, userId); - if (!response.isMatched()) { - throw new GatekeeperCredentialNotMatchException("Unable to request Gatekeeper HAT"); - } - return response.getGatekeeperHAT(); - } - - /** - * Create Intent for choosing lock - */ - @NonNull - public Intent createChooseLockIntent(@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); - - if (mCredentialModel.isValidUserId()) { - intent.putExtra(Intent.EXTRA_USER_ID, mCredentialModel.getUserId()); - } - return intent; - } - - /** - * Create ConfirmLockLauncher - */ - @NonNull - public ChooseLockSettingsHelper createConfirmLockLauncher(@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); - - if (mCredentialModel.isValidUserId()) { - builder.setUserId(mCredentialModel.getUserId()); - } - return builder.build(); - } - -} diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModel.kt b/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModel.kt new file mode 100644 index 00000000000..3fd4d51e472 --- /dev/null +++ b/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModel.kt @@ -0,0 +1,300 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.biometrics2.ui.viewmodel + +import android.app.Activity +import android.app.Application +import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.util.Log +import androidx.activity.result.ActivityResult +import androidx.lifecycle.AndroidViewModel +import com.android.internal.widget.LockPatternUtils +import com.android.settings.biometrics.BiometricEnrollBase +import com.android.settings.biometrics.BiometricUtils +import com.android.settings.biometrics.BiometricUtils.GatekeeperCredentialNotMatchException +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 kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch + +/** + * AutoCredentialViewModel which uses CredentialModel to determine next actions for activity, like + * start ChooseLockActivity, start ConfirmLockActivity, GenerateCredential, or do nothing. + */ +class AutoCredentialViewModel( + application: Application, + private val lockPatternUtils: LockPatternUtils, + private val challengeGenerator: ChallengeGenerator, + private val credentialModel: CredentialModel +) : AndroidViewModel(application) { + + /** + * Generic callback for FingerprintManager#generateChallenge or FaceManager#generateChallenge + */ + interface GenerateChallengeCallback { + /** Generic generateChallenge method for FingerprintManager or FaceManager */ + fun onChallengeGenerated(sensorId: Int, userId: Int, challenge: Long) + } + + /** + * A generic interface class for calling different generateChallenge from FingerprintManager or + * FaceManager + */ + interface ChallengeGenerator { + + /** Get callback that will be called later after challenge generated */ + fun getCallback(): GenerateChallengeCallback? + + /** Set callback that will be called later after challenge generated */ + fun setCallback(callback: GenerateChallengeCallback?) + + /** Method for generating challenge from FingerprintManager or FaceManager */ + fun generateChallenge(userId: Int) + } + + /** Used to generate challenge through FingerprintRepository */ + class FingerprintChallengeGenerator( + private val fingerprintRepository: FingerprintRepository + ) : ChallengeGenerator { + + private var mCallback: GenerateChallengeCallback? = null + + override fun getCallback(): GenerateChallengeCallback? { + return mCallback + } + + override fun setCallback(callback: GenerateChallengeCallback?) { + mCallback = callback + } + + override fun generateChallenge(userId: Int) { + val callback = mCallback + if (callback == null) { + Log.e(TAG, "generateChallenge, null callback") + return + } + + fingerprintRepository.generateChallenge(userId) { + sensorId: Int, uid: Int, challenge: Long -> + callback.onChallengeGenerated( + sensorId, + uid, + challenge + ) + } + } + + companion object { + private const val TAG = "FingerprintChallengeGenerator" + } + } + + private val _generateChallengeFailedFlow = MutableSharedFlow() + val generateChallengeFailedFlow: SharedFlow + get() = _generateChallengeFailedFlow.asSharedFlow() + + + // flag if token is generating through checkCredential()'s generateChallenge() + private var isGeneratingChallengeDuringCheckingCredential = false + + /** Get bundle which passing back to FingerprintSettings for late generateChallenge() */ + fun createGeneratingChallengeExtras(): Bundle? { + if (!isGeneratingChallengeDuringCheckingCredential + || !credentialModel.isValidToken + || !credentialModel.isValidChallenge + ) { + return null + } + val bundle = Bundle() + bundle.putByteArray( + ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, + credentialModel.token + ) + bundle.putLong(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, credentialModel.challenge) + return bundle + } + + /** Check credential status for biometric enrollment. */ + fun checkCredential(scope: CoroutineScope): CredentialAction { + return if (isValidCredential) { + CredentialAction.CREDENTIAL_VALID + } else if (isUnspecifiedPassword) { + CredentialAction.FAIL_NEED_TO_CHOOSE_LOCK + } else if (credentialModel.isValidGkPwHandle) { + val gkPwHandle = credentialModel.gkPwHandle + credentialModel.clearGkPwHandle() + // GkPwHandle is got through caller activity, we shall not revoke it after + // generateChallenge(). Let caller activity to make decision. + generateChallenge(gkPwHandle, false, scope) + isGeneratingChallengeDuringCheckingCredential = true + CredentialAction.IS_GENERATING_CHALLENGE + } else { + CredentialAction.FAIL_NEED_TO_CONFIRM_LOCK + } + } + + private fun generateChallenge( + gkPwHandle: Long, + revokeGkPwHandle: Boolean, + scope: CoroutineScope + ) { + challengeGenerator.setCallback(object : GenerateChallengeCallback { + override fun onChallengeGenerated(sensorId: Int, userId: Int, challenge: Long) { + var illegalStateExceptionCaught = false + try { + val newToken = requestGatekeeperHat(gkPwHandle, challenge, userId) + credentialModel.challenge = challenge + credentialModel.token = newToken + } catch (e: IllegalStateException) { + Log.e(TAG, "generateChallenge, IllegalStateException", e) + illegalStateExceptionCaught = true + } finally { + if (revokeGkPwHandle) { + lockPatternUtils.removeGatekeeperPasswordHandle(gkPwHandle) + } + Log.d( + TAG, + "generateChallenge(), model:$credentialModel" + + ", revokeGkPwHandle:$revokeGkPwHandle" + ) + // Check credential again + if (!isValidCredential || illegalStateExceptionCaught) { + Log.w(TAG, "generateChallenge, invalid Credential or IllegalStateException") + scope.launch { + _generateChallengeFailedFlow.emit(true) + } + } + } + } + }) + challengeGenerator.generateChallenge(userId) + } + + private val isValidCredential: Boolean + get() = !isUnspecifiedPassword && credentialModel.isValidToken + + private val isUnspecifiedPassword: Boolean + get() = lockPatternUtils.getActivePasswordQuality(userId) == 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 and viewModel is generating challenge + */ + fun generateChallengeAsCredentialActivityResult( + isChooseLock: Boolean, + result: ActivityResult, + scope: CoroutineScope + ): Boolean { + if ((isChooseLock && result.resultCode == ChooseLockPattern.RESULT_FINISHED) || + (!isChooseLock && result.resultCode == Activity.RESULT_OK)) { + result.data?.let { + val gkPwHandle = it.getLongExtra( + ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, + CredentialModel.INVALID_GK_PW_HANDLE + ) + // Revoke self requested GkPwHandle because it shall only used once inside this + // activity lifecycle. + generateChallenge(gkPwHandle, true, scope) + return true + } + } + return false + } + + val userId: Int + get() = credentialModel.userId + + val token: ByteArray? + get() = credentialModel.token + + @Throws(IllegalStateException::class) + private fun requestGatekeeperHat(gkPwHandle: Long, challenge: Long, userId: Int): ByteArray? { + val response = lockPatternUtils + .verifyGatekeeperPasswordHandle(gkPwHandle, challenge, userId) + if (!response.isMatched) { + throw GatekeeperCredentialNotMatchException("Unable to request Gatekeeper HAT") + } + return response.gatekeeperHAT + } + + /** Create Intent for choosing lock */ + fun createChooseLockIntent( + context: Context, isSuw: Boolean, + suwExtras: Bundle + ): Intent { + val 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) + if (credentialModel.isValidUserId) { + intent.putExtra(Intent.EXTRA_USER_ID, credentialModel.userId) + } + return intent + } + + /** Create ConfirmLockLauncher */ + fun createConfirmLockLauncher( + activity: Activity, + requestCode: Int, title: String + ): ChooseLockSettingsHelper { + val builder = ChooseLockSettingsHelper.Builder(activity) + builder.setRequestCode(requestCode) + .setTitle(title) + .setRequestGatekeeperPasswordHandle(true) + .setForegroundOnly(true) + .setReturnCredentials(true) + if (credentialModel.isValidUserId) { + builder.setUserId(credentialModel.userId) + } + return builder.build() + } + + companion object { + private const val TAG = "AutoCredentialViewModel" + } +} + +enum class CredentialAction { + + CREDENTIAL_VALID, + + /** Valid credential, activity does nothing. */ + IS_GENERATING_CHALLENGE, + + /** This credential looks good, but still need to run generateChallenge(). */ + FAIL_NEED_TO_CHOOSE_LOCK, + + /** Need activity to run confirm lock */ + FAIL_NEED_TO_CONFIRM_LOCK +} diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModel.kt b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModel.kt index 33e1bb6f8ff..37b0052231f 100644 --- a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModel.kt +++ b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModel.kt @@ -23,8 +23,6 @@ import android.os.Bundle import android.util.Log import androidx.activity.result.ActivityResult import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import com.android.settings.biometrics.BiometricEnrollBase import com.android.settings.biometrics.fingerprint.FingerprintEnrollFinish.FINGERPRINT_SUGGESTION_ACTIVITY import com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollIntroduction @@ -32,6 +30,11 @@ import com.android.settings.biometrics2.data.repository.FingerprintRepository import com.android.settings.biometrics2.ui.model.EnrollmentRequest import kotlinx.atomicfu.AtomicBoolean import kotlinx.atomicfu.atomic +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch /** * Fingerprint enrollment view model implementation @@ -44,9 +47,9 @@ class FingerprintEnrollmentViewModel( val isWaitingActivityResult: AtomicBoolean = atomic(false) - private val _setResultLiveData = MutableLiveData() - val setResultLiveData: LiveData - get() = _setResultLiveData + private val _setResultFlow = MutableSharedFlow() + val setResultFlow: SharedFlow + get() = _setResultFlow.asSharedFlow() var isNewFingerprintAdded = false set(value) { @@ -94,16 +97,17 @@ class FingerprintEnrollmentViewModel( */ fun checkFinishActivityDuringOnPause( isActivityFinishing: Boolean, - isChangingConfigurations: Boolean + isChangingConfigurations: Boolean, + scope: CoroutineScope ) { if (isChangingConfigurations || isActivityFinishing || request.isSuw || isWaitingActivityResult.value ) { return } - _setResultLiveData.postValue( - ActivityResult(BiometricEnrollBase.RESULT_TIMEOUT, null) - ) + scope.launch { + _setResultFlow.emit(ActivityResult(BiometricEnrollBase.RESULT_TIMEOUT, null)) + } } /** @@ -133,23 +137,23 @@ class FingerprintEnrollmentViewModel( * Update FINGERPRINT_SUGGESTION_ACTIVITY into package manager */ fun updateFingerprintSuggestionEnableState(userId: Int) { - val enrolled = fingerprintRepository.getNumOfEnrolledFingerprintsSize(userId) // Only show "Add another fingerprint" if the user already enrolled one. // "Add fingerprint" will be shown in the main flow if the user hasn't enrolled any // fingerprints. If the user already added more than one fingerprint, they already know // to add multiple fingerprints so we don't show the suggestion. + val state = if (fingerprintRepository.getNumOfEnrolledFingerprintsSize(userId) == 1) + PackageManager.COMPONENT_ENABLED_STATE_ENABLED + else + PackageManager.COMPONENT_ENABLED_STATE_DISABLED getApplication().packageManager.setComponentEnabledSetting( ComponentName( getApplication(), FINGERPRINT_SUGGESTION_ACTIVITY ), - if (enrolled == 1) - PackageManager.COMPONENT_ENABLED_STATE_ENABLED - else - PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + state, PackageManager.DONT_KILL_APP ) - Log.d(TAG, "$FINGERPRINT_SUGGESTION_ACTIVITY enabled state = ${enrolled == 1}") + Log.d(TAG, "$FINGERPRINT_SUGGESTION_ACTIVITY enabled state: $state") } companion object { diff --git a/tests/unit/src/com/android/settings/biometrics2/ui/model/CredentialModelTest.kt b/tests/unit/src/com/android/settings/biometrics2/ui/model/CredentialModelTest.kt index d718db6ce2a..ac622324ad1 100644 --- a/tests/unit/src/com/android/settings/biometrics2/ui/model/CredentialModelTest.kt +++ b/tests/unit/src/com/android/settings/biometrics2/ui/model/CredentialModelTest.kt @@ -38,22 +38,6 @@ class CredentialModelTest { Truth.assertThat(credentialModel.userId).isEqualTo(UserHandle.myUserId()) } - @Test - fun testSameValueFromBundle() { - val bundle = newCredentialModelIntentExtras(1234, 6677L, byteArrayOf(33, 44, 55), 987654321) - val model1 = CredentialModel(bundle, clock) - val model2 = CredentialModel(model1.bundle, clock) - verifySameCredentialModels(model1, model2) - } - - @Test - fun testSameValueFromBundle_nullToken() { - val bundle = newCredentialModelIntentExtras(22, 33L, null, 21L) - val model1 = CredentialModel(bundle, clock) - val model2 = CredentialModel(model1.bundle, clock) - verifySameCredentialModels(model1, model2) - } - companion object { @JvmStatic fun newCredentialModelIntentExtras( @@ -148,36 +132,5 @@ class CredentialModelTest { } } } - - fun verifySameCredentialModels( - model1: CredentialModel, - model2: CredentialModel - ) { - Truth.assertThat(model1.userId).isEqualTo(model2.userId) - Truth.assertThat(model1.challenge).isEqualTo(model2.challenge) - Truth.assertThat(model1.gkPwHandle).isEqualTo(model2.gkPwHandle) - val token1 = model1.token - val token2 = model2.token - if (token1 == null) { - Truth.assertThat(token2).isNull() - } else { - Truth.assertThat(token2).isNotNull() - Truth.assertThat(token1.size).isEqualTo(token2!!.size) - for (i in token1.indices) { - Truth.assertThat(token1[i]).isEqualTo( - token2[i] - ) - } - } - val bundle1 = model1.bundle - val bundle2 = model2.bundle - val keySet1 = bundle1.keySet() - Truth.assertThat(keySet1 == bundle2.keySet()).isTrue() - checkBundleIntValue(bundle1, bundle2, Intent.EXTRA_USER_ID) - checkBundleIntValue(bundle1, bundle2, BiometricEnrollBase.EXTRA_KEY_SENSOR_ID) - checkBundleLongValue(bundle1, bundle2, BiometricEnrollBase.EXTRA_KEY_CHALLENGE) - checkBundleByteArrayValue(bundle1, bundle2, BiometricEnrollBase.EXTRA_KEY_CHALLENGE) - checkBundleLongValue(bundle1, bundle2, ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE) - } } } 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 deleted file mode 100644 index 05a7239e95d..00000000000 --- a/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModelTest.java +++ /dev/null @@ -1,596 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings.biometrics2.ui.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.biometrics2.ui.model.CredentialModel.INVALID_CHALLENGE; -import static com.android.settings.biometrics2.ui.model.CredentialModel.INVALID_GK_PW_HANDLE; -import static com.android.settings.biometrics2.ui.model.CredentialModelTest.newCredentialModelIntentExtras; -import static com.android.settings.biometrics2.ui.model.CredentialModelTest.newGkPwHandleCredentialIntentExtras; -import static com.android.settings.biometrics2.ui.model.CredentialModelTest.newOnlySensorValidCredentialIntentExtras; -import static com.android.settings.biometrics2.ui.model.CredentialModelTest.newValidTokenCredentialIntentExtras; -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.CREDENTIAL_IS_GENERATING_CHALLENGE; -import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_VALID; -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.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.KEY_CREDENTIAL_MODEL; -import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.KEY_IS_GENERATING_CHALLENGE_DURING_CHECKING_CREDENTIAL; -import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN; -import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE; - -import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth.assertWithMessage; - -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.when; - -import android.annotation.NonNull; -import android.app.Activity; -import android.content.Intent; -import android.os.Bundle; -import android.os.UserHandle; - -import androidx.activity.result.ActivityResult; -import androidx.annotation.Nullable; -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.password.ChooseLockPattern; -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; - -import java.util.concurrent.atomic.AtomicBoolean; - -@RunWith(AndroidJUnit4.class) -public class AutoCredentialViewModelTest { - - @Rule public final MockitoRule mockito = MockitoJUnit.rule(); - @Rule public final InstantTaskExecutorRule mTaskExecutorRule = new InstantTaskExecutorRule(); - - @Mock private LockPatternUtils mLockPatternUtils; - private TestChallengeGenerator mChallengeGenerator = null; - private AutoCredentialViewModel mViewModel; - - @Before - public void setUp() { - mChallengeGenerator = new TestChallengeGenerator(); - mViewModel = new AutoCredentialViewModel( - ApplicationProvider.getApplicationContext(), - mLockPatternUtils, - mChallengeGenerator); - } - - private void setupGenerateChallenge(int userId, int newSensorId, long newChallenge) { - when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn( - PASSWORD_QUALITY_SOMETHING); - mChallengeGenerator.mUserId = userId; - mChallengeGenerator.mSensorId = newSensorId; - mChallengeGenerator.mChallenge = newChallenge; - } - - @Test - public void testSetCredentialModel_sameResultFromSavedInstanceOrIntent() { - final Bundle extras = newCredentialModelIntentExtras(12, 33, new byte[] { 2, 3 }, 3L); - - AutoCredentialViewModel viewModel2 = new AutoCredentialViewModel( - ApplicationProvider.getApplicationContext(), - mLockPatternUtils, - mChallengeGenerator); - - mViewModel.setCredentialModel(null, new Intent().putExtras(extras)); - final Bundle savedInstance = new Bundle(); - mViewModel.onSaveInstanceState(savedInstance); - viewModel2.setCredentialModel(savedInstance, new Intent()); - - assertThat(mViewModel.getUserId()).isEqualTo(viewModel2.getUserId()); - final byte[] token1 = mViewModel.getToken(); - final byte[] token2 = viewModel2.getToken(); - assertThat(token1).isNotNull(); - assertThat(token2).isNotNull(); - assertThat(token1.length).isEqualTo(token2.length); - for (int i = 0; i < token2.length; ++i) { - assertThat(token1[i]).isEqualTo(token2[i]); - } - } - - @Test - public void testSetCredentialModel_sameResultFromSavedInstanceOrIntent_invalidValues() { - final Bundle extras = newCredentialModelIntentExtras(UserHandle.USER_NULL, - INVALID_CHALLENGE, null, INVALID_GK_PW_HANDLE); - - AutoCredentialViewModel viewModel2 = new AutoCredentialViewModel( - ApplicationProvider.getApplicationContext(), - mLockPatternUtils, - mChallengeGenerator); - - mViewModel.setCredentialModel(null, new Intent().putExtras(extras)); - final Bundle savedInstance = new Bundle(); - mViewModel.onSaveInstanceState(savedInstance); - viewModel2.setCredentialModel(savedInstance, new Intent()); - - assertThat(mViewModel.getUserId()).isEqualTo(UserHandle.USER_NULL); - assertThat(viewModel2.getUserId()).isEqualTo(UserHandle.USER_NULL); - assertThat(mViewModel.getToken()).isNull(); - assertThat(viewModel2.getToken()).isNull(); - } - - @Test - public void testCheckCredential_validCredentialCase() { - final int userId = 99; - mViewModel.setCredentialModel(null, - new Intent().putExtras(newValidTokenCredentialIntentExtras(userId))); - when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn( - PASSWORD_QUALITY_SOMETHING); - - // Run credential check - @CredentialAction final int action = mViewModel.checkCredential(); - - // Check viewModel behavior - assertThat(action).isEqualTo(CREDENTIAL_VALID); - assertThat(mViewModel.getGenerateChallengeFailedLiveData().getValue()).isNull(); - - // Check createGeneratingChallengeExtras() - assertThat(mViewModel.createGeneratingChallengeExtras()).isNull(); - - // Check onSaveInstanceState() - final Bundle actualBundle = new Bundle(); - mViewModel.onSaveInstanceState(actualBundle); - assertThat(actualBundle.getBoolean(KEY_IS_GENERATING_CHALLENGE_DURING_CHECKING_CREDENTIAL)) - .isFalse(); - } - - @Test - public void testCheckCredential_needToChooseLock() { - final int userId = 100; - mViewModel.setCredentialModel(null, - new Intent().putExtras(newOnlySensorValidCredentialIntentExtras(userId))); - when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn( - PASSWORD_QUALITY_UNSPECIFIED); - - // Run credential check - @CredentialAction final int action = mViewModel.checkCredential(); - - // Check viewModel behavior - assertThat(action).isEqualTo(CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK); - assertThat(mViewModel.getGenerateChallengeFailedLiveData().getValue()).isNull(); - - // Check createGeneratingChallengeExtras() - assertThat(mViewModel.createGeneratingChallengeExtras()).isNull(); - - // Check onSaveInstanceState() - final Bundle actualBundle = new Bundle(); - mViewModel.onSaveInstanceState(actualBundle); - assertThat(actualBundle.getBoolean(KEY_IS_GENERATING_CHALLENGE_DURING_CHECKING_CREDENTIAL)) - .isFalse(); - } - - @Test - public void testCheckCredential_needToConfirmLockForSomething() { - final int userId = 101; - mViewModel.setCredentialModel(null, - new Intent().putExtras(newOnlySensorValidCredentialIntentExtras(userId))); - when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn( - PASSWORD_QUALITY_SOMETHING); - - // Run credential check - @CredentialAction final int action = mViewModel.checkCredential(); - - // Check viewModel behavior - assertThat(action).isEqualTo(CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK); - assertThat(mViewModel.getGenerateChallengeFailedLiveData().getValue()).isNull(); - - // Check createGeneratingChallengeExtras() - assertThat(mViewModel.createGeneratingChallengeExtras()).isNull(); - - // Check onSaveInstanceState() - final Bundle actualBundle = new Bundle(); - mViewModel.onSaveInstanceState(actualBundle); - assertThat(actualBundle.getBoolean(KEY_IS_GENERATING_CHALLENGE_DURING_CHECKING_CREDENTIAL)) - .isFalse(); - } - - @Test - public void testCheckCredential_needToConfirmLockForNumeric() { - final int userId = 102; - mViewModel.setCredentialModel(null, - new Intent().putExtras(newOnlySensorValidCredentialIntentExtras(userId))); - when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn( - PASSWORD_QUALITY_NUMERIC); - - // Run credential check - @CredentialAction final int action = mViewModel.checkCredential(); - - // Check viewModel behavior - assertThat(action).isEqualTo(CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK); - assertThat(mViewModel.getGenerateChallengeFailedLiveData().getValue()).isNull(); - - // Check createGeneratingChallengeExtras() - assertThat(mViewModel.createGeneratingChallengeExtras()).isNull(); - - // Check onSaveInstanceState() - final Bundle actualBundle = new Bundle(); - mViewModel.onSaveInstanceState(actualBundle); - assertThat(actualBundle.getBoolean(KEY_IS_GENERATING_CHALLENGE_DURING_CHECKING_CREDENTIAL)) - .isFalse(); - } - - @Test - public void testCheckCredential_needToConfirmLockForAlphabetic() { - final int userId = 103; - mViewModel.setCredentialModel(null, - new Intent().putExtras(newOnlySensorValidCredentialIntentExtras(userId))); - when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn( - PASSWORD_QUALITY_ALPHABETIC); - - // Run credential check - @CredentialAction final int action = mViewModel.checkCredential(); - - // Check viewModel behavior - assertThat(action).isEqualTo(CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK); - assertThat(mViewModel.getGenerateChallengeFailedLiveData().getValue()).isNull(); - - // Check createGeneratingChallengeExtras() - assertThat(mViewModel.createGeneratingChallengeExtras()).isNull(); - - // Check onSaveInstanceState() - final Bundle actualBundle = new Bundle(); - mViewModel.onSaveInstanceState(actualBundle); - assertThat(actualBundle.getBoolean(KEY_IS_GENERATING_CHALLENGE_DURING_CHECKING_CREDENTIAL)) - .isFalse(); - } - - @Test - public void testCheckCredential_generateChallenge() { - final int userId = 104; - final long gkPwHandle = 1111L; - mViewModel.setCredentialModel(null, - new Intent().putExtras(newGkPwHandleCredentialIntentExtras(userId, gkPwHandle))); - when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn( - PASSWORD_QUALITY_SOMETHING); - - final int newSensorId = 10; - final long newChallenge = 20L; - setupGenerateChallenge(userId, newSensorId, newChallenge); - when(mLockPatternUtils.verifyGatekeeperPasswordHandle(gkPwHandle, newChallenge, userId)) - .thenReturn(newGoodCredential(gkPwHandle, new byte[] { 1 })); - - final AtomicBoolean hasCalledRemoveGkPwHandle = new AtomicBoolean(); - doAnswer(invocation -> { - hasCalledRemoveGkPwHandle.set(true); - return null; - }).when(mLockPatternUtils).removeGatekeeperPasswordHandle(gkPwHandle); - - // Run credential check - @CredentialAction final int action = mViewModel.checkCredential(); - - // Check viewModel behavior - assertThat(action).isEqualTo(CREDENTIAL_IS_GENERATING_CHALLENGE); - assertThat(mViewModel.getGenerateChallengeFailedLiveData().getValue()).isNull(); - - // Check data inside CredentialModel - assertThat(mViewModel.getToken()).isNotNull(); - assertThat(mChallengeGenerator.mCallbackRunCount).isEqualTo(1); - assertThat(hasCalledRemoveGkPwHandle.get()).isFalse(); - - // Check createGeneratingChallengeExtras() - final Bundle generatingChallengeExtras = mViewModel.createGeneratingChallengeExtras(); - assertThat(generatingChallengeExtras).isNotNull(); - assertThat(generatingChallengeExtras.getLong(EXTRA_KEY_CHALLENGE)).isEqualTo(newChallenge); - final byte[] tokens = generatingChallengeExtras.getByteArray(EXTRA_KEY_CHALLENGE_TOKEN); - assertThat(tokens).isNotNull(); - assertThat(tokens.length).isEqualTo(1); - assertThat(tokens[0]).isEqualTo(1); - - // Check onSaveInstanceState() - final Bundle actualBundle = new Bundle(); - mViewModel.onSaveInstanceState(actualBundle); - assertThat(actualBundle.getBoolean(KEY_IS_GENERATING_CHALLENGE_DURING_CHECKING_CREDENTIAL)) - .isTrue(); - } - - @Test - public void testCheckCredential_generateChallengeFail() { - final int userId = 104; - final long gkPwHandle = 1111L; - mViewModel.setCredentialModel(null, - new Intent().putExtras(newGkPwHandleCredentialIntentExtras(userId, gkPwHandle))); - when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn( - PASSWORD_QUALITY_SOMETHING); - - final int newSensorId = 10; - final long newChallenge = 20L; - setupGenerateChallenge(userId, newSensorId, newChallenge); - when(mLockPatternUtils.verifyGatekeeperPasswordHandle(gkPwHandle, newChallenge, userId)) - .thenReturn(newBadCredential(0)); - - // Run credential check - @CredentialAction final int action = mViewModel.checkCredential(); - - assertThat(action).isEqualTo(CREDENTIAL_IS_GENERATING_CHALLENGE); - assertThat(mViewModel.getGenerateChallengeFailedLiveData().getValue()).isTrue(); - assertThat(mChallengeGenerator.mCallbackRunCount).isEqualTo(1); - - // Check createGeneratingChallengeExtras() - assertThat(mViewModel.createGeneratingChallengeExtras()).isNull(); - - // Check onSaveInstanceState() - final Bundle actualBundle = new Bundle(); - mViewModel.onSaveInstanceState(actualBundle); - assertThat(actualBundle.getBoolean(KEY_IS_GENERATING_CHALLENGE_DURING_CHECKING_CREDENTIAL)) - .isTrue(); - } - - @Test - public void testGetUserId_fromIntent() { - final int userId = 106; - mViewModel.setCredentialModel(null, - new Intent().putExtras(newOnlySensorValidCredentialIntentExtras(userId))); - - // Get userId - assertThat(mViewModel.getUserId()).isEqualTo(userId); - } - - @Test - public void testGetUserId_fromSavedInstance() { - final int userId = 106; - final Bundle savedInstance = new Bundle(); - savedInstance.putBundle(KEY_CREDENTIAL_MODEL, - newOnlySensorValidCredentialIntentExtras(userId)); - mViewModel.setCredentialModel(savedInstance, new Intent()); - - // Get userId - assertThat(mViewModel.getUserId()).isEqualTo(userId); - } - - @Test - public void testCreateGeneratingChallengeExtras_generateChallenge() { - final Bundle credentialExtras = newValidTokenCredentialIntentExtras(200); - final Bundle savedInstance = new Bundle(); - savedInstance.putBundle(KEY_CREDENTIAL_MODEL, credentialExtras); - savedInstance.putBoolean(KEY_IS_GENERATING_CHALLENGE_DURING_CHECKING_CREDENTIAL, true); - mViewModel.setCredentialModel(savedInstance, new Intent()); - - // Check createGeneratingChallengeExtras() - final Bundle actualExtras = mViewModel.createGeneratingChallengeExtras(); - assertThat(actualExtras).isNotNull(); - assertThat(actualExtras.getLong(EXTRA_KEY_CHALLENGE)) - .isEqualTo(credentialExtras.getLong(EXTRA_KEY_CHALLENGE)); - final byte[] actualToken = actualExtras.getByteArray(EXTRA_KEY_CHALLENGE_TOKEN); - final byte[] expectedToken = credentialExtras.getByteArray(EXTRA_KEY_CHALLENGE_TOKEN); - assertThat(actualToken).isNotNull(); - assertThat(expectedToken).isNotNull(); - assertThat(actualToken.length).isEqualTo(expectedToken.length); - for (int i = 0; i < actualToken.length; ++i) { - assertWithMessage("tokens[" + i + "] not match").that(actualToken[i]) - .isEqualTo(expectedToken[i]); - } - } - - @Test - public void testCreateGeneratingChallengeExtras_notGenerateChallenge() { - final Bundle credentialExtras = newValidTokenCredentialIntentExtras(201); - final Bundle savedInstance = new Bundle(); - savedInstance.putBundle(KEY_CREDENTIAL_MODEL, credentialExtras); - savedInstance.putBoolean(KEY_IS_GENERATING_CHALLENGE_DURING_CHECKING_CREDENTIAL, false); - mViewModel.setCredentialModel(savedInstance, new Intent()); - - // Check createGeneratingChallengeExtras() - assertThat(mViewModel.createGeneratingChallengeExtras()).isNull(); - } - - @Test - public void testCreateGeneratingChallengeExtras_invalidToken() { - final Bundle credentialExtras = newOnlySensorValidCredentialIntentExtras(202); - final Bundle savedInstance = new Bundle(); - savedInstance.putBundle(KEY_CREDENTIAL_MODEL, credentialExtras); - savedInstance.putBoolean(KEY_IS_GENERATING_CHALLENGE_DURING_CHECKING_CREDENTIAL, true); - mViewModel.setCredentialModel(savedInstance, new Intent()); - - // Check createGeneratingChallengeExtras() - assertThat(mViewModel.createGeneratingChallengeExtras()).isNull(); - } - - @Test - public void testCheckNewCredentialFromActivityResult_invalidChooseLock() { - final int userId = 107; - final long gkPwHandle = 3333L; - mViewModel.setCredentialModel(null, - new Intent().putExtras(newGkPwHandleCredentialIntentExtras(userId, gkPwHandle))); - final Intent intent = new Intent(); - intent.putExtra(EXTRA_KEY_GK_PW_HANDLE, gkPwHandle); - - // run checkNewCredentialFromActivityResult() - final boolean ret = mViewModel.checkNewCredentialFromActivityResult(true, - new ActivityResult(ChooseLockPattern.RESULT_FINISHED + 1, intent)); - - assertThat(ret).isFalse(); - assertThat(mViewModel.getGenerateChallengeFailedLiveData().getValue()).isNull(); - } - - @Test - public void testCheckNewCredentialFromActivityResult_invalidConfirmLock() { - final int userId = 107; - final long gkPwHandle = 3333L; - mViewModel.setCredentialModel(null, - new Intent().putExtras(newGkPwHandleCredentialIntentExtras(userId, gkPwHandle))); - final Intent intent = new Intent(); - intent.putExtra(EXTRA_KEY_GK_PW_HANDLE, gkPwHandle); - - // run checkNewCredentialFromActivityResult() - final boolean ret = mViewModel.checkNewCredentialFromActivityResult(false, - new ActivityResult(Activity.RESULT_OK + 1, intent)); - - assertThat(ret).isFalse(); - assertThat(mViewModel.getGenerateChallengeFailedLiveData().getValue()).isNull(); - } - - @Test - public void testCheckNewCredentialFromActivityResult_nullDataChooseLock() { - final int userId = 108; - final long gkPwHandle = 4444L; - mViewModel.setCredentialModel(null, - new Intent().putExtras(newGkPwHandleCredentialIntentExtras(userId, gkPwHandle))); - - // run checkNewCredentialFromActivityResult() - final boolean ret = mViewModel.checkNewCredentialFromActivityResult(true, - new ActivityResult(ChooseLockPattern.RESULT_FINISHED, null)); - - assertThat(ret).isFalse(); - assertThat(mViewModel.getGenerateChallengeFailedLiveData().getValue()).isNull(); - } - - @Test - public void testCheckNewCredentialFromActivityResult_nullDataConfirmLock() { - final int userId = 109; - mViewModel.setCredentialModel(null, - new Intent().putExtras(newOnlySensorValidCredentialIntentExtras(userId))); - - // run checkNewCredentialFromActivityResult() - final boolean ret = mViewModel.checkNewCredentialFromActivityResult(false, - new ActivityResult(Activity.RESULT_OK, null)); - - assertThat(ret).isFalse(); - assertThat(mViewModel.getGenerateChallengeFailedLiveData().getValue()).isNull(); - } - - @Test - public void testCheckNewCredentialFromActivityResult_validChooseLock() { - final int userId = 108; - mViewModel.setCredentialModel(null, - new Intent().putExtras(newOnlySensorValidCredentialIntentExtras(userId))); - when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn( - PASSWORD_QUALITY_SOMETHING); - - final long gkPwHandle = 6666L; - final int newSensorId = 50; - final long newChallenge = 60L; - setupGenerateChallenge(userId, newSensorId, newChallenge); - when(mLockPatternUtils.verifyGatekeeperPasswordHandle(gkPwHandle, newChallenge, userId)) - .thenReturn(newGoodCredential(gkPwHandle, new byte[] { 1 })); - - final AtomicBoolean hasCalledRemoveGkPwHandle = new AtomicBoolean(); - doAnswer(invocation -> { - hasCalledRemoveGkPwHandle.set(true); - return null; - }).when(mLockPatternUtils).removeGatekeeperPasswordHandle(gkPwHandle); - - // Run checkNewCredentialFromActivityResult() - final Intent intent = new Intent().putExtra(EXTRA_KEY_GK_PW_HANDLE, gkPwHandle); - final boolean ret = mViewModel.checkNewCredentialFromActivityResult(true, - new ActivityResult(ChooseLockPattern.RESULT_FINISHED, intent)); - - assertThat(ret).isTrue(); - assertThat(mViewModel.getGenerateChallengeFailedLiveData().getValue()).isNull(); - assertThat(mViewModel.getToken()).isNotNull(); - assertThat(mChallengeGenerator.mCallbackRunCount).isEqualTo(1); - assertThat(hasCalledRemoveGkPwHandle.get()).isTrue(); - } - - @Test - public void testCheckNewCredentialFromActivityResult_validConfirmLock() { - final int userId = 109; - mViewModel.setCredentialModel(null, - new Intent().putExtras(newOnlySensorValidCredentialIntentExtras(userId))); - when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn( - PASSWORD_QUALITY_SOMETHING); - - final long gkPwHandle = 5555L; - final int newSensorId = 80; - final long newChallenge = 90L; - setupGenerateChallenge(userId, newSensorId, newChallenge); - when(mLockPatternUtils.verifyGatekeeperPasswordHandle(gkPwHandle, newChallenge, userId)) - .thenReturn(newGoodCredential(gkPwHandle, new byte[] { 1 })); - - final AtomicBoolean hasCalledRemoveGkPwHandle = new AtomicBoolean(); - doAnswer(invocation -> { - hasCalledRemoveGkPwHandle.set(true); - return null; - }).when(mLockPatternUtils).removeGatekeeperPasswordHandle(gkPwHandle); - - // Run checkNewCredentialFromActivityResult() - final Intent intent = new Intent().putExtra(EXTRA_KEY_GK_PW_HANDLE, gkPwHandle); - final boolean ret = mViewModel.checkNewCredentialFromActivityResult(false, - new ActivityResult(Activity.RESULT_OK, intent)); - - assertThat(ret).isTrue(); - assertThat(mViewModel.getGenerateChallengeFailedLiveData().getValue()).isNull(); - assertThat(mViewModel.getToken()).isNotNull(); - assertThat(mChallengeGenerator.mCallbackRunCount).isEqualTo(1); - assertThat(hasCalledRemoveGkPwHandle.get()).isTrue(); - } - - public static class TestChallengeGenerator implements ChallengeGenerator { - public int mSensorId = -1; - 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(); - } - - private VerifyCredentialResponse newBadCredential(int timeout) { - if (timeout > 0) { - return VerifyCredentialResponse.fromTimeout(timeout); - } else { - return VerifyCredentialResponse.fromError(); - } - } -} diff --git a/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModelTest.kt b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModelTest.kt new file mode 100644 index 00000000000..3ae4951944b --- /dev/null +++ b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModelTest.kt @@ -0,0 +1,541 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.biometrics2.ui.viewmodel + +import android.app.Activity +import android.app.admin.DevicePolicyManager +import android.content.Intent +import android.os.Bundle +import android.os.SystemClock +import android.os.UserHandle +import androidx.activity.result.ActivityResult +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.biometrics.BiometricEnrollBase +import com.android.settings.biometrics2.ui.model.CredentialModel +import com.android.settings.biometrics2.ui.model.CredentialModelTest.Companion.newGkPwHandleCredentialIntentExtras +import com.android.settings.biometrics2.ui.model.CredentialModelTest.Companion.newOnlySensorValidCredentialIntentExtras +import com.android.settings.biometrics2.ui.model.CredentialModelTest.Companion.newValidTokenCredentialIntentExtras +import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.ChallengeGenerator +import com.android.settings.password.ChooseLockPattern +import com.android.settings.password.ChooseLockSettingsHelper +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import java.util.concurrent.atomic.AtomicBoolean +import org.mockito.Mockito.`when` as whenever + +@RunWith(AndroidJUnit4::class) +class AutoCredentialViewModelTest { + + @get:Rule val mockito: MockitoRule = MockitoJUnit.rule() + + @Mock private lateinit var lockPatternUtils: LockPatternUtils + + private var challengeGenerator: TestChallengeGenerator = TestChallengeGenerator() + + private lateinit var viewModel: AutoCredentialViewModel + private fun newAutoCredentialViewModel(bundle: Bundle?): AutoCredentialViewModel { + return AutoCredentialViewModel( + ApplicationProvider.getApplicationContext(), + lockPatternUtils, + challengeGenerator, + CredentialModel(bundle, SystemClock.elapsedRealtimeClock()) + ) + } + + @Before + fun setUp() { + challengeGenerator = TestChallengeGenerator() + } + + private fun setupGenerateChallenge(userId: Int, newSensorId: Int, newChallenge: Long) { + whenever(lockPatternUtils.getActivePasswordQuality(userId)).thenReturn( + DevicePolicyManager.PASSWORD_QUALITY_SOMETHING + ) + challengeGenerator.userId = userId + challengeGenerator.sensorId = newSensorId + challengeGenerator.challenge = newChallenge + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun testCheckCredential_validCredentialCase() = runTest { + val userId = 99 + viewModel = newAutoCredentialViewModel(newValidTokenCredentialIntentExtras(userId)) + whenever(lockPatternUtils.getActivePasswordQuality(userId)).thenReturn( + DevicePolicyManager.PASSWORD_QUALITY_SOMETHING + ) + + val generateFails = listOfGenerateChallengeFailedFlow() + + // Run credential check + val action = viewModel.checkCredential(backgroundScope) + runCurrent() + + // Check viewModel behavior + assertThat(action).isEqualTo(CredentialAction.CREDENTIAL_VALID) + assertThat(generateFails.size).isEqualTo(0) + + // Check createGeneratingChallengeExtras() + assertThat(viewModel.createGeneratingChallengeExtras()).isNull() + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun testCheckCredential_needToChooseLock() = runTest { + val userId = 100 + viewModel = newAutoCredentialViewModel(newOnlySensorValidCredentialIntentExtras(userId)) + whenever(lockPatternUtils.getActivePasswordQuality(userId)).thenReturn( + DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED + ) + + val generateFails = listOfGenerateChallengeFailedFlow() + + // Run credential check + val action = viewModel.checkCredential(backgroundScope) + runCurrent() + + // Check viewModel behavior + assertThat(action).isEqualTo(CredentialAction.FAIL_NEED_TO_CHOOSE_LOCK) + assertThat(generateFails.size).isEqualTo(0) + + // Check createGeneratingChallengeExtras() + assertThat(viewModel.createGeneratingChallengeExtras()).isNull() + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun testCheckCredential_needToConfirmLockForSomething() = runTest { + val userId = 101 + viewModel = + newAutoCredentialViewModel(newOnlySensorValidCredentialIntentExtras(userId)) + whenever(lockPatternUtils.getActivePasswordQuality(userId)).thenReturn( + DevicePolicyManager.PASSWORD_QUALITY_SOMETHING + ) + + val generateFails = listOfGenerateChallengeFailedFlow() + + // Run credential check + val action = viewModel.checkCredential(backgroundScope) + runCurrent() + + // Check viewModel behavior + assertThat(action).isEqualTo(CredentialAction.FAIL_NEED_TO_CONFIRM_LOCK) + assertThat(generateFails.size).isEqualTo(0) + + // Check createGeneratingChallengeExtras() + assertThat(viewModel.createGeneratingChallengeExtras()).isNull() + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun testCheckCredential_needToConfirmLockForNumeric() = runTest { + val userId = 102 + viewModel = + newAutoCredentialViewModel(newOnlySensorValidCredentialIntentExtras(userId)) + whenever(lockPatternUtils.getActivePasswordQuality(userId)).thenReturn( + DevicePolicyManager.PASSWORD_QUALITY_NUMERIC + ) + + val generateFails = listOfGenerateChallengeFailedFlow() + + // Run credential check + val action = viewModel.checkCredential(backgroundScope) + runCurrent() + + // Check viewModel behavior + assertThat(action).isEqualTo(CredentialAction.FAIL_NEED_TO_CONFIRM_LOCK) + assertThat(generateFails.size).isEqualTo(0) + + // Check createGeneratingChallengeExtras() + assertThat(viewModel.createGeneratingChallengeExtras()).isNull() + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun testCheckCredential_needToConfirmLockForAlphabetic() = runTest { + val userId = 103 + viewModel = + newAutoCredentialViewModel(newOnlySensorValidCredentialIntentExtras(userId)) + whenever(lockPatternUtils.getActivePasswordQuality(userId)).thenReturn( + DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC + ) + + val generateFails = listOfGenerateChallengeFailedFlow() + + // Run credential check + val action = viewModel.checkCredential(this) + runCurrent() + + // Check viewModel behavior + assertThat(action).isEqualTo(CredentialAction.FAIL_NEED_TO_CONFIRM_LOCK) + assertThat(generateFails.size).isEqualTo(0) + + // Check createGeneratingChallengeExtras() + assertThat(viewModel.createGeneratingChallengeExtras()).isNull() + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun testCheckCredential_generateChallenge() = runTest { + val userId = 104 + val gkPwHandle = 1111L + viewModel = + newAutoCredentialViewModel(newGkPwHandleCredentialIntentExtras(userId, gkPwHandle)) + whenever(lockPatternUtils.getActivePasswordQuality(userId)).thenReturn( + DevicePolicyManager.PASSWORD_QUALITY_SOMETHING + ) + val newSensorId = 10 + val newChallenge = 20L + setupGenerateChallenge(userId, newSensorId, newChallenge) + whenever( + lockPatternUtils.verifyGatekeeperPasswordHandle( + gkPwHandle, + newChallenge, + userId + ) + ) + .thenReturn(newGoodCredential(gkPwHandle, byteArrayOf(1))) + val hasCalledRemoveGkPwHandle = AtomicBoolean() + Mockito.doAnswer { + hasCalledRemoveGkPwHandle.set(true) + null + }.`when`(lockPatternUtils).removeGatekeeperPasswordHandle(gkPwHandle) + + val generateFails = listOfGenerateChallengeFailedFlow() + + // Run credential check + val action = viewModel.checkCredential(backgroundScope) + runCurrent() + + // Check viewModel behavior + assertThat(action).isEqualTo(CredentialAction.IS_GENERATING_CHALLENGE) + assertThat(generateFails.size).isEqualTo(0) + + // Check data inside CredentialModel + assertThat(viewModel.token).isNotNull() + assertThat(challengeGenerator.callbackRunCount).isEqualTo(1) + assertThat(hasCalledRemoveGkPwHandle.get()).isFalse() + + // Check createGeneratingChallengeExtras() + val generatingChallengeExtras = viewModel.createGeneratingChallengeExtras() + assertThat(generatingChallengeExtras).isNotNull() + assertThat(generatingChallengeExtras!!.getLong(BiometricEnrollBase.EXTRA_KEY_CHALLENGE)) + .isEqualTo(newChallenge) + val tokens = + generatingChallengeExtras.getByteArray(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN) + assertThat(tokens).isNotNull() + assertThat(tokens!!.size).isEqualTo(1) + assertThat(tokens[0]).isEqualTo(1) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun testCheckCredential_generateChallengeFail() = runTest { + backgroundScope.launch { + val userId = 104 + val gkPwHandle = 1111L + viewModel = + newAutoCredentialViewModel(newGkPwHandleCredentialIntentExtras(userId, gkPwHandle)) + whenever(lockPatternUtils.getActivePasswordQuality(userId)).thenReturn( + DevicePolicyManager.PASSWORD_QUALITY_SOMETHING + ) + val newSensorId = 10 + val newChallenge = 20L + setupGenerateChallenge(userId, newSensorId, newChallenge) + whenever( + lockPatternUtils.verifyGatekeeperPasswordHandle( + gkPwHandle, + newChallenge, + userId + ) + ) + .thenReturn(newBadCredential(0)) + + val generateFails = listOfGenerateChallengeFailedFlow() + + // Run credential check + val action = viewModel.checkCredential(this) + runCurrent() + + assertThat(action).isEqualTo(CredentialAction.IS_GENERATING_CHALLENGE) + assertThat(generateFails.size).isEqualTo(1) + assertThat(generateFails[0]).isEqualTo(true) + assertThat(challengeGenerator.callbackRunCount).isEqualTo(1) + + // Check createGeneratingChallengeExtras() + assertThat(viewModel.createGeneratingChallengeExtras()).isNull() + } + } + + @Test + fun testGetUserId_fromIntent() { + val userId = 106 + viewModel = newAutoCredentialViewModel(newOnlySensorValidCredentialIntentExtras(userId)) + + // Get userId + assertThat(viewModel.userId).isEqualTo(userId) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun testGenerateChallengeAsCredentialActivityResult_invalidChooseLock() = runTest { + backgroundScope.launch { + val userId = 107 + val gkPwHandle = 3333L + viewModel = + newAutoCredentialViewModel(newGkPwHandleCredentialIntentExtras(userId, gkPwHandle)) + val intent = Intent() + intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gkPwHandle) + + val generateFails = listOfGenerateChallengeFailedFlow() + + // Run generateChallengeAsCredentialActivityResult() + val ret = viewModel.generateChallengeAsCredentialActivityResult( + true, + ActivityResult(ChooseLockPattern.RESULT_FINISHED + 1, intent), + backgroundScope + ) + runCurrent() + + assertThat(ret).isFalse() + assertThat(generateFails.size).isEqualTo(0) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun testGenerateChallengeAsCredentialActivityResult_invalidConfirmLock() = runTest { + backgroundScope.launch { + val userId = 107 + val gkPwHandle = 3333L + viewModel = + newAutoCredentialViewModel(newGkPwHandleCredentialIntentExtras(userId, gkPwHandle)) + val intent = Intent() + intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gkPwHandle) + + val generateFails = listOfGenerateChallengeFailedFlow() + + // Run generateChallengeAsCredentialActivityResult() + val ret = viewModel.generateChallengeAsCredentialActivityResult( + false, + ActivityResult(Activity.RESULT_OK + 1, intent), + backgroundScope + ) + runCurrent() + + assertThat(ret).isFalse() + assertThat(generateFails.size).isEqualTo(0) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun testGenerateChallengeAsCredentialActivityResult_nullDataChooseLock() = runTest { + val userId = 108 + val gkPwHandle = 4444L + viewModel = + newAutoCredentialViewModel(newGkPwHandleCredentialIntentExtras(userId, gkPwHandle)) + + val generateFails = listOfGenerateChallengeFailedFlow() + + // Run generateChallengeAsCredentialActivityResult() + val ret = viewModel.generateChallengeAsCredentialActivityResult( + true, + ActivityResult(ChooseLockPattern.RESULT_FINISHED, null), + backgroundScope + ) + runCurrent() + + assertThat(ret).isFalse() + assertThat(generateFails.size).isEqualTo(0) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun testGenerateChallengeAsCredentialActivityResult_nullDataConfirmLock() = runTest { + val userId = 109 + viewModel = + newAutoCredentialViewModel(newOnlySensorValidCredentialIntentExtras(userId)) + + val generateFails = listOfGenerateChallengeFailedFlow() + + // Run generateChallengeAsCredentialActivityResult() + val ret = viewModel.generateChallengeAsCredentialActivityResult( + false, + ActivityResult(Activity.RESULT_OK, null), + backgroundScope + ) + runCurrent() + + assertThat(ret).isFalse() + assertThat(generateFails.size).isEqualTo(0) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun testGenerateChallengeAsCredentialActivityResult_validChooseLock() = runTest { + val userId = 108 + viewModel = + newAutoCredentialViewModel(newOnlySensorValidCredentialIntentExtras(userId)) + whenever(lockPatternUtils.getActivePasswordQuality(userId)).thenReturn( + DevicePolicyManager.PASSWORD_QUALITY_SOMETHING + ) + val gkPwHandle = 6666L + val newSensorId = 50 + val newChallenge = 60L + setupGenerateChallenge(userId, newSensorId, newChallenge) + whenever( + lockPatternUtils.verifyGatekeeperPasswordHandle( + gkPwHandle, + newChallenge, + userId + ) + ) + .thenReturn(newGoodCredential(gkPwHandle, byteArrayOf(1))) + val hasCalledRemoveGkPwHandle = AtomicBoolean() + Mockito.doAnswer { + hasCalledRemoveGkPwHandle.set(true) + null + }.`when`(lockPatternUtils).removeGatekeeperPasswordHandle(gkPwHandle) + + val generateFails = listOfGenerateChallengeFailedFlow() + + // Run generateChallengeAsCredentialActivityResult() + val intent = + Intent().putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gkPwHandle) + val ret = viewModel.generateChallengeAsCredentialActivityResult( + true, + ActivityResult(ChooseLockPattern.RESULT_FINISHED, intent), + backgroundScope + ) + runCurrent() + + assertThat(ret).isTrue() + assertThat(generateFails.size).isEqualTo(0) + assertThat(viewModel.token).isNotNull() + assertThat(challengeGenerator.callbackRunCount).isEqualTo(1) + assertThat(hasCalledRemoveGkPwHandle.get()).isTrue() + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun testGenerateChallengeAsCredentialActivityResult_validConfirmLock() = runTest { + val userId = 109 + viewModel = + newAutoCredentialViewModel(newOnlySensorValidCredentialIntentExtras(userId)) + whenever(lockPatternUtils.getActivePasswordQuality(userId)).thenReturn( + DevicePolicyManager.PASSWORD_QUALITY_SOMETHING + ) + val gkPwHandle = 5555L + val newSensorId = 80 + val newChallenge = 90L + setupGenerateChallenge(userId, newSensorId, newChallenge) + whenever( + lockPatternUtils.verifyGatekeeperPasswordHandle( + gkPwHandle, + newChallenge, + userId + ) + ) + .thenReturn(newGoodCredential(gkPwHandle, byteArrayOf(1))) + val hasCalledRemoveGkPwHandle = AtomicBoolean() + Mockito.doAnswer { + hasCalledRemoveGkPwHandle.set(true) + null + }.`when`(lockPatternUtils).removeGatekeeperPasswordHandle(gkPwHandle) + + val generateFails = listOfGenerateChallengeFailedFlow() + + // Run generateChallengeAsCredentialActivityResult() + val intent = + Intent().putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gkPwHandle) + val ret = viewModel.generateChallengeAsCredentialActivityResult( + false, + ActivityResult(Activity.RESULT_OK, intent), + backgroundScope + ) + runCurrent() + + assertThat(ret).isTrue() + assertThat(generateFails.size).isEqualTo(0) + assertThat(viewModel.token).isNotNull() + assertThat(challengeGenerator.callbackRunCount).isEqualTo(1) + assertThat(hasCalledRemoveGkPwHandle.get()).isTrue() + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun TestScope.listOfGenerateChallengeFailedFlow(): List = + mutableListOf().also { + backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { + viewModel.generateChallengeFailedFlow.toList(it) + } + } + + class TestChallengeGenerator : ChallengeGenerator { + var sensorId = -1 + var userId = UserHandle.myUserId() + var challenge = CredentialModel.INVALID_CHALLENGE + var callbackRunCount = 0 + + private var _callback: AutoCredentialViewModel.GenerateChallengeCallback? = null + + override fun getCallback(): AutoCredentialViewModel.GenerateChallengeCallback? { + return _callback + } + + override fun setCallback(callback: AutoCredentialViewModel.GenerateChallengeCallback?) { + _callback = callback + } + + override fun generateChallenge(userId: Int) { + val callback = _callback ?: return + callback.onChallengeGenerated(sensorId, this.userId, challenge) + ++callbackRunCount + } + } + + private fun newGoodCredential(gkPwHandle: Long, hat: ByteArray): VerifyCredentialResponse { + return VerifyCredentialResponse.Builder() + .setGatekeeperPasswordHandle(gkPwHandle) + .setGatekeeperHAT(hat) + .build() + } + + private fun newBadCredential(timeout: Int): VerifyCredentialResponse { + return if (timeout > 0) { + VerifyCredentialResponse.fromTimeout(timeout) + } else { + VerifyCredentialResponse.fromError() + } + } +} diff --git a/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModelTest.kt b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModelTest.kt index 9f339de4375..bee91c9bf6e 100644 --- a/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModelTest.kt +++ b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModelTest.kt @@ -23,12 +23,20 @@ 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.biometrics.BiometricEnrollBase import com.android.settings.biometrics2.data.repository.FingerprintRepository import com.android.settings.biometrics2.utils.EnrollmentRequestUtils.newAllFalseRequest +import com.android.settings.biometrics2.utils.EnrollmentRequestUtils.newIsSuwRequest import com.android.settings.biometrics2.utils.FingerprintRepositoryUtils.newFingerprintRepository import com.android.settings.biometrics2.utils.FingerprintRepositoryUtils.setupFingerprintEnrolledFingerprints -import com.android.settings.testutils.InstantTaskExecutorRule -import com.google.common.truth.Truth +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule import org.junit.Test @@ -42,8 +50,6 @@ class FingerprintEnrollmentViewModelTest { @get:Rule val mockito: MockitoRule = MockitoJUnit.rule() - @get:Rule val taskExecutorRule = InstantTaskExecutorRule() - private val application: Application get() = ApplicationProvider.getApplicationContext() @@ -69,12 +75,12 @@ class FingerprintEnrollmentViewModelTest { @Test fun testGetRequest() { - Truth.assertThat(viewModel.request).isNotNull() + assertThat(viewModel.request).isNotNull() } @Test fun testIsWaitingActivityResultDefaultFalse() { - Truth.assertThat(viewModel.isWaitingActivityResult.value).isFalse() + assertThat(viewModel.isWaitingActivityResult.value).isFalse() } @@ -83,8 +89,8 @@ class FingerprintEnrollmentViewModelTest { val retResult = viewModel.getOverrideActivityResult( ActivityResult(22, null), null ) - Truth.assertThat(retResult).isNotNull() - Truth.assertThat(retResult.data).isNull() + assertThat(retResult).isNotNull() + assertThat(retResult.data).isNull() } @Test @@ -93,8 +99,8 @@ class FingerprintEnrollmentViewModelTest { val retResult = viewModel.getOverrideActivityResult( ActivityResult(33, intent), null ) - Truth.assertThat(retResult).isNotNull() - Truth.assertThat(retResult.data).isEqualTo(intent) + assertThat(retResult).isNotNull() + assertThat(retResult.data).isEqualTo(intent) } @Test @@ -106,8 +112,8 @@ class FingerprintEnrollmentViewModelTest { ActivityResult(33, null), extra ) - Truth.assertThat(retResult).isNotNull() - Truth.assertThat(retResult.data).isNull() + assertThat(retResult).isNotNull() + assertThat(retResult.data).isNull() } @Test @@ -124,16 +130,16 @@ class FingerprintEnrollmentViewModelTest { val retResult = viewModel.getOverrideActivityResult( ActivityResult(33, null), extra ) - Truth.assertThat(retResult).isNotNull() + assertThat(retResult).isNotNull() val retIntent = retResult.data - Truth.assertThat(retIntent).isNotNull() + assertThat(retIntent).isNotNull() val retExtra = retIntent!!.extras - Truth.assertThat(retExtra).isNotNull() - Truth.assertThat(retExtra!!.size).isEqualTo(extra.size) - Truth.assertThat(retExtra.getString(key1)).isEqualTo(extra.getString(key1)) - Truth.assertThat(retExtra.getInt(key2)).isEqualTo(extra.getInt(key2)) + assertThat(retExtra).isNotNull() + assertThat(retExtra!!.size).isEqualTo(extra.size) + assertThat(retExtra.getString(key1)).isEqualTo(extra.getString(key1)) + assertThat(retExtra.getInt(key2)).isEqualTo(extra.getInt(key2)) } @Test @@ -149,15 +155,15 @@ class FingerprintEnrollmentViewModelTest { val retResult = viewModel.getOverrideActivityResult(ActivityResult(33, intent), extra) - Truth.assertThat(retResult).isNotNull() + assertThat(retResult).isNotNull() val retIntent = retResult.data - Truth.assertThat(retIntent).isNotNull() + assertThat(retIntent).isNotNull() val retExtra = retIntent!!.extras - Truth.assertThat(retExtra).isNotNull() - Truth.assertThat(retExtra!!.size).isEqualTo(intent.extras!!.size) - Truth.assertThat(retExtra.getString(key2)).isEqualTo(intent.extras!!.getString(key2)) + assertThat(retExtra).isNotNull() + assertThat(retExtra!!.size).isEqualTo(intent.extras!!.size) + assertThat(retExtra.getString(key2)).isEqualTo(intent.extras!!.getString(key2)) } @Test @@ -177,17 +183,17 @@ class FingerprintEnrollmentViewModelTest { viewModel.isNewFingerprintAdded = true val retResult = viewModel.getOverrideActivityResult(ActivityResult(33, intent), extra) - Truth.assertThat(retResult).isNotNull() + assertThat(retResult).isNotNull() val retIntent = retResult.data - Truth.assertThat(retIntent).isNotNull() + assertThat(retIntent).isNotNull() val retExtra = retIntent!!.extras - Truth.assertThat(retExtra).isNotNull() - Truth.assertThat(retExtra!!.size).isEqualTo(extra.size + intent.extras!!.size) - Truth.assertThat(retExtra.getString(key1)).isEqualTo(extra.getString(key1)) - Truth.assertThat(retExtra.getInt(key2)).isEqualTo(extra.getInt(key2)) - Truth.assertThat(retExtra.getLong(key3)).isEqualTo(intent.extras!!.getLong(key3)) + assertThat(retExtra).isNotNull() + assertThat(retExtra!!.size).isEqualTo(extra.size + intent.extras!!.size) + assertThat(retExtra.getString(key1)).isEqualTo(extra.getString(key1)) + assertThat(retExtra.getInt(key2)).isEqualTo(extra.getInt(key2)) + assertThat(retExtra.getLong(key3)).isEqualTo(intent.extras!!.getLong(key3)) } @Test @@ -205,18 +211,120 @@ class FingerprintEnrollmentViewModelTest { ) setupFingerprintEnrolledFingerprints(fingerprintManager, uid, 0) - Truth.assertThat(viewModel.isMaxEnrolledReached(uid)).isFalse() + assertThat(viewModel.isMaxEnrolledReached(uid)).isFalse() setupFingerprintEnrolledFingerprints(fingerprintManager, uid, 1) - Truth.assertThat(viewModel.isMaxEnrolledReached(uid)).isFalse() + assertThat(viewModel.isMaxEnrolledReached(uid)).isFalse() setupFingerprintEnrolledFingerprints(fingerprintManager, uid, 2) - Truth.assertThat(viewModel.isMaxEnrolledReached(uid)).isFalse() + assertThat(viewModel.isMaxEnrolledReached(uid)).isFalse() setupFingerprintEnrolledFingerprints(fingerprintManager, uid, 3) - Truth.assertThat(viewModel.isMaxEnrolledReached(uid)).isTrue() + assertThat(viewModel.isMaxEnrolledReached(uid)).isTrue() setupFingerprintEnrolledFingerprints(fingerprintManager, uid, 4) - Truth.assertThat(viewModel.isMaxEnrolledReached(uid)).isTrue() + assertThat(viewModel.isMaxEnrolledReached(uid)).isTrue() } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun testSetResultFlow_defaultEmpty() = runTest { + val activityResults = listOfSetResultFlow() + + runCurrent() + + assertThat(activityResults.size).isEqualTo(0) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun testCheckFinishActivityDuringOnPause_doNothingIfIsSuw() = runTest { + viewModel = FingerprintEnrollmentViewModel( + application, + fingerprintRepository, + newIsSuwRequest(application) + ) + + val activityResults = listOfSetResultFlow() + + viewModel.checkFinishActivityDuringOnPause( + isActivityFinishing = false, + isChangingConfigurations = false, + scope = this + ) + runCurrent() + + assertThat(activityResults.size).isEqualTo(0) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun testCheckFinishActivityDuringOnPause_doNothingIfIsWaitingActivity() = runTest { + val activityResults = listOfSetResultFlow() + + viewModel.isWaitingActivityResult.value = true + viewModel.checkFinishActivityDuringOnPause( + isActivityFinishing = false, + isChangingConfigurations = false, + scope = this + ) + runCurrent() + + assertThat(activityResults.size).isEqualTo(0) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun testCheckFinishActivityDuringOnPause_doNothingIfIsActivityFinishing() = runTest { + val activityResults = listOfSetResultFlow() + + viewModel.checkFinishActivityDuringOnPause( + isActivityFinishing = true, + isChangingConfigurations = false, + scope = this + ) + runCurrent() + + assertThat(activityResults.size).isEqualTo(0) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun testCheckFinishActivityDuringOnPause_doNothingIfIsChangingConfigurations() = runTest { + val activityResults = listOfSetResultFlow() + + viewModel.checkFinishActivityDuringOnPause( + isActivityFinishing = false, + isChangingConfigurations = true, + scope = this + ) + runCurrent() + + assertThat(activityResults.size).isEqualTo(0) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun testCheckFinishActivityDuringOnPause_defaultFinishSelf() = runTest { + val activityResults = listOfSetResultFlow() + + viewModel.checkFinishActivityDuringOnPause( + isActivityFinishing = false, + isChangingConfigurations = false, + scope = backgroundScope + ) + runCurrent() + + assertThat(activityResults.size).isEqualTo(1) + assertThat(activityResults[0].resultCode).isEqualTo(BiometricEnrollBase.RESULT_TIMEOUT) + assertThat(activityResults[0].data).isEqualTo(null) + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun TestScope.listOfSetResultFlow(): List = + mutableListOf().also { + backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { + viewModel.setResultFlow.toList(it) + } + } }