[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
This commit is contained in:
@@ -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<ChallengeGenerator>() {};
|
||||
public static final CreationExtras.Key<EnrollmentRequest> ENROLLMENT_REQUEST_KEY =
|
||||
new CreationExtras.Key<EnrollmentRequest>() {};
|
||||
public static final CreationExtras.Key<Integer> USER_ID_KEY =
|
||||
new CreationExtras.Key<Integer>() {};
|
||||
public static final CreationExtras.Key<CredentialModel> CREDENTIAL_MODEL_KEY =
|
||||
new CreationExtras.Key<CredentialModel>() {};
|
||||
|
||||
@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);
|
||||
|
@@ -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
|
||||
|
@@ -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"
|
||||
|
@@ -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<Boolean> 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<Boolean> 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();
|
||||
}
|
||||
|
||||
}
|
@@ -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<Boolean>()
|
||||
val generateChallengeFailedFlow: SharedFlow<Boolean>
|
||||
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
|
||||
}
|
@@ -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<ActivityResult>()
|
||||
val setResultLiveData: LiveData<ActivityResult>
|
||||
get() = _setResultLiveData
|
||||
private val _setResultFlow = MutableSharedFlow<ActivityResult>()
|
||||
val setResultFlow: SharedFlow<ActivityResult>
|
||||
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<Application>().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 {
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -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<Boolean> =
|
||||
mutableListOf<Boolean>().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()
|
||||
}
|
||||
}
|
||||
}
|
@@ -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<ActivityResult> =
|
||||
mutableListOf<ActivityResult>().also {
|
||||
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
|
||||
viewModel.setResultFlow.toList(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user