[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:
Milton Wu
2023-07-24 16:29:20 +08:00
parent 78f3760d26
commit acb8be5d25
10 changed files with 1067 additions and 1164 deletions

View File

@@ -28,6 +28,7 @@ import androidx.lifecycle.viewmodel.CreationExtras;
import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.LockPatternUtils;
import com.android.settings.biometrics.fingerprint.FingerprintUpdater; import com.android.settings.biometrics.fingerprint.FingerprintUpdater;
import com.android.settings.biometrics2.data.repository.FingerprintRepository; 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.model.EnrollmentRequest;
import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel; import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel;
import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.ChallengeGenerator; import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.ChallengeGenerator;
@@ -54,8 +55,8 @@ public class BiometricsViewModelFactory implements ViewModelProvider.Factory {
new CreationExtras.Key<ChallengeGenerator>() {}; new CreationExtras.Key<ChallengeGenerator>() {};
public static final CreationExtras.Key<EnrollmentRequest> ENROLLMENT_REQUEST_KEY = public static final CreationExtras.Key<EnrollmentRequest> ENROLLMENT_REQUEST_KEY =
new CreationExtras.Key<EnrollmentRequest>() {}; new CreationExtras.Key<EnrollmentRequest>() {};
public static final CreationExtras.Key<Integer> USER_ID_KEY = public static final CreationExtras.Key<CredentialModel> CREDENTIAL_MODEL_KEY =
new CreationExtras.Key<Integer>() {}; new CreationExtras.Key<CredentialModel>() {};
@NonNull @NonNull
@Override @Override
@@ -76,9 +77,10 @@ public class BiometricsViewModelFactory implements ViewModelProvider.Factory {
final LockPatternUtils lockPatternUtils = final LockPatternUtils lockPatternUtils =
featureFactory.getSecurityFeatureProvider().getLockPatternUtils(application); featureFactory.getSecurityFeatureProvider().getLockPatternUtils(application);
final ChallengeGenerator challengeGenerator = extras.get(CHALLENGE_GENERATOR_KEY); 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, return (T) new AutoCredentialViewModel(application, lockPatternUtils,
challengeGenerator); challengeGenerator, credentialModel);
} }
} else if (modelClass.isAssignableFrom(DeviceFoldedViewModel.class)) { } else if (modelClass.isAssignableFrom(DeviceFoldedViewModel.class)) {
return (T) new DeviceFoldedViewModel(new ScreenSizeFoldProvider(application), return (T) new DeviceFoldedViewModel(new ScreenSizeFoldProvider(application),
@@ -93,10 +95,10 @@ public class BiometricsViewModelFactory implements ViewModelProvider.Factory {
} else if (modelClass.isAssignableFrom(FingerprintEnrollIntroViewModel.class)) { } else if (modelClass.isAssignableFrom(FingerprintEnrollIntroViewModel.class)) {
final FingerprintRepository repository = provider.getFingerprintRepository(application); final FingerprintRepository repository = provider.getFingerprintRepository(application);
final EnrollmentRequest request = extras.get(ENROLLMENT_REQUEST_KEY); final EnrollmentRequest request = extras.get(ENROLLMENT_REQUEST_KEY);
final Integer userId = extras.get(USER_ID_KEY); final CredentialModel credentialModel = extras.get(CREDENTIAL_MODEL_KEY);
if (repository != null && request != null && userId != null) { if (repository != null && request != null && credentialModel != null) {
return (T) new FingerprintEnrollIntroViewModel(application, repository, request, return (T) new FingerprintEnrollIntroViewModel(application, repository, request,
userId); credentialModel.getUserId());
} }
} else if (modelClass.isAssignableFrom(FingerprintEnrollmentViewModel.class)) { } else if (modelClass.isAssignableFrom(FingerprintEnrollmentViewModel.class)) {
final FingerprintRepository repository = provider.getFingerprintRepository(application); final FingerprintRepository repository = provider.getFingerprintRepository(application);
@@ -105,27 +107,27 @@ public class BiometricsViewModelFactory implements ViewModelProvider.Factory {
return (T) new FingerprintEnrollmentViewModel(application, repository, request); return (T) new FingerprintEnrollmentViewModel(application, repository, request);
} }
} else if (modelClass.isAssignableFrom(FingerprintEnrollProgressViewModel.class)) { } else if (modelClass.isAssignableFrom(FingerprintEnrollProgressViewModel.class)) {
final Integer userId = extras.get(USER_ID_KEY); final CredentialModel credentialModel = extras.get(CREDENTIAL_MODEL_KEY);
if (userId != null) { if (credentialModel != null) {
return (T) new FingerprintEnrollProgressViewModel(application, return (T) new FingerprintEnrollProgressViewModel(application,
new FingerprintUpdater(application), userId); new FingerprintUpdater(application), credentialModel.getUserId());
} }
} else if (modelClass.isAssignableFrom(FingerprintEnrollEnrollingViewModel.class)) { } 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( final FingerprintRepository fingerprint = provider.getFingerprintRepository(
application); application);
if (fingerprint != null && userId != null) { if (fingerprint != null && credentialModel != null) {
return (T) new FingerprintEnrollEnrollingViewModel(application, userId, return (T) new FingerprintEnrollEnrollingViewModel(application,
fingerprint); credentialModel.getUserId(), fingerprint);
} }
} else if (modelClass.isAssignableFrom(FingerprintEnrollFinishViewModel.class)) { } 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 EnrollmentRequest request = extras.get(ENROLLMENT_REQUEST_KEY);
final FingerprintRepository fingerprint = provider.getFingerprintRepository( final FingerprintRepository fingerprint = provider.getFingerprintRepository(
application); application);
if (fingerprint != null && userId != null && request != null) { if (fingerprint != null && credentialModel != null && request != null) {
return (T) new FingerprintEnrollFinishViewModel(application, userId, request, return (T) new FingerprintEnrollFinishViewModel(application,
fingerprint); credentialModel.getUserId(), request, fingerprint);
} }
} else if (modelClass.isAssignableFrom(FingerprintEnrollErrorDialogViewModel.class)) { } else if (modelClass.isAssignableFrom(FingerprintEnrollErrorDialogViewModel.class)) {
final EnrollmentRequest request = extras.get(ENROLLMENT_REQUEST_KEY); final EnrollmentRequest request = extras.get(ENROLLMENT_REQUEST_KEY);

View File

@@ -80,20 +80,6 @@ class CredentialModel(bundle: Bundle?, private val clock: Clock) {
val isValidToken: Boolean val isValidToken: Boolean
get() = token != null 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 */ /** Returns a string representation of the object */
override fun toString(): String { override fun toString(): String {
val gkPwHandleLen = "$gkPwHandle".length val gkPwHandleLen = "$gkPwHandle".length

View File

@@ -44,16 +44,13 @@ import com.android.settings.Utils
import com.android.settings.biometrics.BiometricEnrollBase import com.android.settings.biometrics.BiometricEnrollBase
import com.android.settings.biometrics2.factory.BiometricsViewModelFactory import com.android.settings.biometrics2.factory.BiometricsViewModelFactory
import com.android.settings.biometrics2.factory.BiometricsViewModelFactory.CHALLENGE_GENERATOR_KEY 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.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.CredentialModel
import com.android.settings.biometrics2.ui.model.EnrollmentRequest import com.android.settings.biometrics2.ui.model.EnrollmentRequest
import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel 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.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.DeviceFoldedViewModel
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel.FINGERPRINT_ENROLL_ENROLLING_ACTION_DONE 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
autoCredentialViewModel.setCredentialModel(savedInstanceState, intent)
// Theme // Theme
setTheme(viewModel.request.theme) setTheme(viewModel.request.theme)
@@ -219,14 +215,23 @@ open class FingerprintEnrollmentActivity : FragmentActivity() {
} }
} }
// observe LiveData collectFlows()
viewModel.setResultLiveData.observe(this) {
result: ActivityResult -> onSetActivityResult(result)
}
autoCredentialViewModel.generateChallengeFailedLiveData.observe(this) {
_: Boolean -> onGenerateChallengeFailed()
} }
private fun collectFlows() {
lifecycleScope.launch { 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) { repeatOnLifecycle(Lifecycle.State.STARTED) {
errorDialogViewModel.newDialogFlow.collect { errorDialogViewModel.newDialogFlow.collect {
Log.d(TAG, "newErrorDialogFlow($it)") Log.d(TAG, "newErrorDialogFlow($it)")
@@ -236,8 +241,6 @@ open class FingerprintEnrollmentActivity : FragmentActivity() {
) )
} }
} }
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { repeatOnLifecycle(Lifecycle.State.STARTED) {
errorDialogViewModel.setResultFlow.collect { errorDialogViewModel.setResultFlow.collect {
Log.d(TAG, "errorDialogSetResultFlow($it)") 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) { private fun onSetActivityResult(result: ActivityResult) {
val challengeExtras: Bundle? = autoCredentialViewModel.createGeneratingChallengeExtras() val challengeExtras: Bundle? = autoCredentialViewModel.createGeneratingChallengeExtras()
val overrideResult: ActivityResult = viewModel.getOverrideActivityResult( val overrideResult: ActivityResult = viewModel.getOverrideActivityResult(
@@ -428,8 +427,8 @@ open class FingerprintEnrollmentActivity : FragmentActivity() {
} }
private fun checkCredential() { private fun checkCredential() {
when (autoCredentialViewModel.checkCredential()) { when (autoCredentialViewModel.checkCredential(lifecycleScope)) {
CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK -> { CredentialAction.FAIL_NEED_TO_CHOOSE_LOCK -> {
val intent: Intent = autoCredentialViewModel.createChooseLockIntent( val intent: Intent = autoCredentialViewModel.createChooseLockIntent(
this, this,
viewModel.request.isSuw, viewModel.request.isSuw,
@@ -442,7 +441,7 @@ open class FingerprintEnrollmentActivity : FragmentActivity() {
return return
} }
CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK -> { CredentialAction.FAIL_NEED_TO_CONFIRM_LOCK -> {
val launched: Boolean = autoCredentialViewModel.createConfirmLockLauncher( val launched: Boolean = autoCredentialViewModel.createConfirmLockLauncher(
this, this,
LAUNCH_CONFIRM_LOCK_ACTIVITY, LAUNCH_CONFIRM_LOCK_ACTIVITY,
@@ -459,21 +458,24 @@ open class FingerprintEnrollmentActivity : FragmentActivity() {
return return
} }
CREDENTIAL_VALID, CredentialAction.CREDENTIAL_VALID,
CREDENTIAL_IS_GENERATING_CHALLENGE -> {} CredentialAction.IS_GENERATING_CHALLENGE -> {}
} }
} }
private fun onChooseOrConfirmLockResult(isChooseLock: Boolean, activityResult: ActivityResult) { private fun onChooseOrConfirmLockResult(
isChooseLock: Boolean,
activityResult: ActivityResult
) {
if (!viewModel.isWaitingActivityResult.compareAndSet(true, false)) { if (!viewModel.isWaitingActivityResult.compareAndSet(true, false)) {
Log.w(TAG, "isChooseLock:$isChooseLock, fail to unset waiting flag") Log.w(TAG, "isChooseLock:$isChooseLock, fail to unset waiting flag")
} }
if (autoCredentialViewModel.checkNewCredentialFromActivityResult( if (!autoCredentialViewModel.generateChallengeAsCredentialActivityResult(
isChooseLock, activityResult isChooseLock,
activityResult,
lifecycleScope
) )
) { ) {
overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out)
} else {
onSetActivityResult(activityResult) onSetActivityResult(activityResult)
} }
} }
@@ -573,7 +575,11 @@ open class FingerprintEnrollmentActivity : FragmentActivity() {
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
viewModel.checkFinishActivityDuringOnPause(isFinishing, isChangingConfigurations) viewModel.checkFinishActivityDuringOnPause(
isFinishing,
isChangingConfigurations,
lifecycleScope
)
} }
override fun onDestroy() { override fun onDestroy() {
@@ -596,17 +602,14 @@ open class FingerprintEnrollmentActivity : FragmentActivity() {
} }
override val defaultViewModelCreationExtras: CreationExtras override val defaultViewModelCreationExtras: CreationExtras
get() { get() = MutableCreationExtras(super.defaultViewModelCreationExtras).also {
val fingerprintRepository = featureFactory.biometricsRepositoryProvider it[CHALLENGE_GENERATOR_KEY] = FingerprintChallengeGenerator(
.getFingerprintRepository(application)!! 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] = it[ENROLLMENT_REQUEST_KEY] =
EnrollmentRequest(intent, applicationContext, this is SetupActivity) EnrollmentRequest(intent, applicationContext, this is SetupActivity)
it[USER_ID_KEY] = credentialModel.userId it[CREDENTIAL_MODEL_KEY] =
} CredentialModel(intent.extras, SystemClock.elapsedRealtimeClock())
} }
override val defaultViewModelProviderFactory: ViewModelProvider.Factory override val defaultViewModelProviderFactory: ViewModelProvider.Factory
@@ -630,11 +633,6 @@ open class FingerprintEnrollmentActivity : FragmentActivity() {
super.onConfigurationChanged(newConfig) super.onConfigurationChanged(newConfig)
} }
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
autoCredentialViewModel.onSaveInstanceState(outState)
}
companion object { companion object {
private const val DEBUG = false private const val DEBUG = false
private const val TAG = "FingerprintEnrollmentActivity" private const val TAG = "FingerprintEnrollmentActivity"

View File

@@ -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();
}
}

View File

@@ -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
}

View File

@@ -23,8 +23,6 @@ import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.android.settings.biometrics.BiometricEnrollBase import com.android.settings.biometrics.BiometricEnrollBase
import com.android.settings.biometrics.fingerprint.FingerprintEnrollFinish.FINGERPRINT_SUGGESTION_ACTIVITY import com.android.settings.biometrics.fingerprint.FingerprintEnrollFinish.FINGERPRINT_SUGGESTION_ACTIVITY
import com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollIntroduction 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 com.android.settings.biometrics2.ui.model.EnrollmentRequest
import kotlinx.atomicfu.AtomicBoolean import kotlinx.atomicfu.AtomicBoolean
import kotlinx.atomicfu.atomic 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 * Fingerprint enrollment view model implementation
@@ -44,9 +47,9 @@ class FingerprintEnrollmentViewModel(
val isWaitingActivityResult: AtomicBoolean = atomic(false) val isWaitingActivityResult: AtomicBoolean = atomic(false)
private val _setResultLiveData = MutableLiveData<ActivityResult>() private val _setResultFlow = MutableSharedFlow<ActivityResult>()
val setResultLiveData: LiveData<ActivityResult> val setResultFlow: SharedFlow<ActivityResult>
get() = _setResultLiveData get() = _setResultFlow.asSharedFlow()
var isNewFingerprintAdded = false var isNewFingerprintAdded = false
set(value) { set(value) {
@@ -94,16 +97,17 @@ class FingerprintEnrollmentViewModel(
*/ */
fun checkFinishActivityDuringOnPause( fun checkFinishActivityDuringOnPause(
isActivityFinishing: Boolean, isActivityFinishing: Boolean,
isChangingConfigurations: Boolean isChangingConfigurations: Boolean,
scope: CoroutineScope
) { ) {
if (isChangingConfigurations || isActivityFinishing || request.isSuw if (isChangingConfigurations || isActivityFinishing || request.isSuw
|| isWaitingActivityResult.value || isWaitingActivityResult.value
) { ) {
return return
} }
_setResultLiveData.postValue( scope.launch {
ActivityResult(BiometricEnrollBase.RESULT_TIMEOUT, null) _setResultFlow.emit(ActivityResult(BiometricEnrollBase.RESULT_TIMEOUT, null))
) }
} }
/** /**
@@ -133,23 +137,23 @@ class FingerprintEnrollmentViewModel(
* Update FINGERPRINT_SUGGESTION_ACTIVITY into package manager * Update FINGERPRINT_SUGGESTION_ACTIVITY into package manager
*/ */
fun updateFingerprintSuggestionEnableState(userId: Int) { fun updateFingerprintSuggestionEnableState(userId: Int) {
val enrolled = fingerprintRepository.getNumOfEnrolledFingerprintsSize(userId)
// Only show "Add another fingerprint" if the user already enrolled one. // 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 // "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 // fingerprints. If the user already added more than one fingerprint, they already know
// to add multiple fingerprints so we don't show the suggestion. // 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( getApplication<Application>().packageManager.setComponentEnabledSetting(
ComponentName( ComponentName(
getApplication(), getApplication(),
FINGERPRINT_SUGGESTION_ACTIVITY FINGERPRINT_SUGGESTION_ACTIVITY
), ),
if (enrolled == 1) state,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
else
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP 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 { companion object {

View File

@@ -38,22 +38,6 @@ class CredentialModelTest {
Truth.assertThat(credentialModel.userId).isEqualTo(UserHandle.myUserId()) 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 { companion object {
@JvmStatic @JvmStatic
fun newCredentialModelIntentExtras( 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)
}
} }
} }

View File

@@ -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();
}
}
}

View File

@@ -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()
}
}
}

View File

@@ -23,12 +23,20 @@ import android.os.Bundle
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4 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.data.repository.FingerprintRepository
import com.android.settings.biometrics2.utils.EnrollmentRequestUtils.newAllFalseRequest 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.newFingerprintRepository
import com.android.settings.biometrics2.utils.FingerprintRepositoryUtils.setupFingerprintEnrolledFingerprints import com.android.settings.biometrics2.utils.FingerprintRepositoryUtils.setupFingerprintEnrolledFingerprints
import com.android.settings.testutils.InstantTaskExecutorRule import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth 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.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@@ -42,8 +50,6 @@ class FingerprintEnrollmentViewModelTest {
@get:Rule val mockito: MockitoRule = MockitoJUnit.rule() @get:Rule val mockito: MockitoRule = MockitoJUnit.rule()
@get:Rule val taskExecutorRule = InstantTaskExecutorRule()
private val application: Application private val application: Application
get() = ApplicationProvider.getApplicationContext() get() = ApplicationProvider.getApplicationContext()
@@ -69,12 +75,12 @@ class FingerprintEnrollmentViewModelTest {
@Test @Test
fun testGetRequest() { fun testGetRequest() {
Truth.assertThat(viewModel.request).isNotNull() assertThat(viewModel.request).isNotNull()
} }
@Test @Test
fun testIsWaitingActivityResultDefaultFalse() { fun testIsWaitingActivityResultDefaultFalse() {
Truth.assertThat(viewModel.isWaitingActivityResult.value).isFalse() assertThat(viewModel.isWaitingActivityResult.value).isFalse()
} }
@@ -83,8 +89,8 @@ class FingerprintEnrollmentViewModelTest {
val retResult = viewModel.getOverrideActivityResult( val retResult = viewModel.getOverrideActivityResult(
ActivityResult(22, null), null ActivityResult(22, null), null
) )
Truth.assertThat(retResult).isNotNull() assertThat(retResult).isNotNull()
Truth.assertThat(retResult.data).isNull() assertThat(retResult.data).isNull()
} }
@Test @Test
@@ -93,8 +99,8 @@ class FingerprintEnrollmentViewModelTest {
val retResult = viewModel.getOverrideActivityResult( val retResult = viewModel.getOverrideActivityResult(
ActivityResult(33, intent), null ActivityResult(33, intent), null
) )
Truth.assertThat(retResult).isNotNull() assertThat(retResult).isNotNull()
Truth.assertThat(retResult.data).isEqualTo(intent) assertThat(retResult.data).isEqualTo(intent)
} }
@Test @Test
@@ -106,8 +112,8 @@ class FingerprintEnrollmentViewModelTest {
ActivityResult(33, null), extra ActivityResult(33, null), extra
) )
Truth.assertThat(retResult).isNotNull() assertThat(retResult).isNotNull()
Truth.assertThat(retResult.data).isNull() assertThat(retResult.data).isNull()
} }
@Test @Test
@@ -124,16 +130,16 @@ class FingerprintEnrollmentViewModelTest {
val retResult = viewModel.getOverrideActivityResult( val retResult = viewModel.getOverrideActivityResult(
ActivityResult(33, null), extra ActivityResult(33, null), extra
) )
Truth.assertThat(retResult).isNotNull() assertThat(retResult).isNotNull()
val retIntent = retResult.data val retIntent = retResult.data
Truth.assertThat(retIntent).isNotNull() assertThat(retIntent).isNotNull()
val retExtra = retIntent!!.extras val retExtra = retIntent!!.extras
Truth.assertThat(retExtra).isNotNull() assertThat(retExtra).isNotNull()
Truth.assertThat(retExtra!!.size).isEqualTo(extra.size) assertThat(retExtra!!.size).isEqualTo(extra.size)
Truth.assertThat(retExtra.getString(key1)).isEqualTo(extra.getString(key1)) assertThat(retExtra.getString(key1)).isEqualTo(extra.getString(key1))
Truth.assertThat(retExtra.getInt(key2)).isEqualTo(extra.getInt(key2)) assertThat(retExtra.getInt(key2)).isEqualTo(extra.getInt(key2))
} }
@Test @Test
@@ -149,15 +155,15 @@ class FingerprintEnrollmentViewModelTest {
val retResult = viewModel.getOverrideActivityResult(ActivityResult(33, intent), extra) val retResult = viewModel.getOverrideActivityResult(ActivityResult(33, intent), extra)
Truth.assertThat(retResult).isNotNull() assertThat(retResult).isNotNull()
val retIntent = retResult.data val retIntent = retResult.data
Truth.assertThat(retIntent).isNotNull() assertThat(retIntent).isNotNull()
val retExtra = retIntent!!.extras val retExtra = retIntent!!.extras
Truth.assertThat(retExtra).isNotNull() assertThat(retExtra).isNotNull()
Truth.assertThat(retExtra!!.size).isEqualTo(intent.extras!!.size) assertThat(retExtra!!.size).isEqualTo(intent.extras!!.size)
Truth.assertThat(retExtra.getString(key2)).isEqualTo(intent.extras!!.getString(key2)) assertThat(retExtra.getString(key2)).isEqualTo(intent.extras!!.getString(key2))
} }
@Test @Test
@@ -177,17 +183,17 @@ class FingerprintEnrollmentViewModelTest {
viewModel.isNewFingerprintAdded = true viewModel.isNewFingerprintAdded = true
val retResult = viewModel.getOverrideActivityResult(ActivityResult(33, intent), extra) val retResult = viewModel.getOverrideActivityResult(ActivityResult(33, intent), extra)
Truth.assertThat(retResult).isNotNull() assertThat(retResult).isNotNull()
val retIntent = retResult.data val retIntent = retResult.data
Truth.assertThat(retIntent).isNotNull() assertThat(retIntent).isNotNull()
val retExtra = retIntent!!.extras val retExtra = retIntent!!.extras
Truth.assertThat(retExtra).isNotNull() assertThat(retExtra).isNotNull()
Truth.assertThat(retExtra!!.size).isEqualTo(extra.size + intent.extras!!.size) assertThat(retExtra!!.size).isEqualTo(extra.size + intent.extras!!.size)
Truth.assertThat(retExtra.getString(key1)).isEqualTo(extra.getString(key1)) assertThat(retExtra.getString(key1)).isEqualTo(extra.getString(key1))
Truth.assertThat(retExtra.getInt(key2)).isEqualTo(extra.getInt(key2)) assertThat(retExtra.getInt(key2)).isEqualTo(extra.getInt(key2))
Truth.assertThat(retExtra.getLong(key3)).isEqualTo(intent.extras!!.getLong(key3)) assertThat(retExtra.getLong(key3)).isEqualTo(intent.extras!!.getLong(key3))
} }
@Test @Test
@@ -205,18 +211,120 @@ class FingerprintEnrollmentViewModelTest {
) )
setupFingerprintEnrolledFingerprints(fingerprintManager, uid, 0) setupFingerprintEnrolledFingerprints(fingerprintManager, uid, 0)
Truth.assertThat(viewModel.isMaxEnrolledReached(uid)).isFalse() assertThat(viewModel.isMaxEnrolledReached(uid)).isFalse()
setupFingerprintEnrolledFingerprints(fingerprintManager, uid, 1) setupFingerprintEnrolledFingerprints(fingerprintManager, uid, 1)
Truth.assertThat(viewModel.isMaxEnrolledReached(uid)).isFalse() assertThat(viewModel.isMaxEnrolledReached(uid)).isFalse()
setupFingerprintEnrolledFingerprints(fingerprintManager, uid, 2) setupFingerprintEnrolledFingerprints(fingerprintManager, uid, 2)
Truth.assertThat(viewModel.isMaxEnrolledReached(uid)).isFalse() assertThat(viewModel.isMaxEnrolledReached(uid)).isFalse()
setupFingerprintEnrolledFingerprints(fingerprintManager, uid, 3) setupFingerprintEnrolledFingerprints(fingerprintManager, uid, 3)
Truth.assertThat(viewModel.isMaxEnrolledReached(uid)).isTrue() assertThat(viewModel.isMaxEnrolledReached(uid)).isTrue()
setupFingerprintEnrolledFingerprints(fingerprintManager, uid, 4) 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)
}
} }
} }