Merge "[BiometricsV2] Refactor AutoCredentialViewModel" into main

This commit is contained in:
Treehugger Robot
2023-08-07 05:22:13 +00:00
committed by Android (Google) Code Review
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.settings.biometrics.fingerprint.FingerprintUpdater;
import com.android.settings.biometrics2.data.repository.FingerprintRepository;
import com.android.settings.biometrics2.ui.model.CredentialModel;
import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel;
import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.ChallengeGenerator;
@@ -54,8 +55,8 @@ public class BiometricsViewModelFactory implements ViewModelProvider.Factory {
new CreationExtras.Key<ChallengeGenerator>() {};
public static final CreationExtras.Key<EnrollmentRequest> ENROLLMENT_REQUEST_KEY =
new CreationExtras.Key<EnrollmentRequest>() {};
public static final CreationExtras.Key<Integer> USER_ID_KEY =
new CreationExtras.Key<Integer>() {};
public static final CreationExtras.Key<CredentialModel> CREDENTIAL_MODEL_KEY =
new CreationExtras.Key<CredentialModel>() {};
@NonNull
@Override
@@ -76,9 +77,10 @@ public class BiometricsViewModelFactory implements ViewModelProvider.Factory {
final LockPatternUtils lockPatternUtils =
featureFactory.getSecurityFeatureProvider().getLockPatternUtils(application);
final ChallengeGenerator challengeGenerator = extras.get(CHALLENGE_GENERATOR_KEY);
if (challengeGenerator != null) {
final CredentialModel credentialModel = extras.get(CREDENTIAL_MODEL_KEY);
if (challengeGenerator != null && credentialModel != null) {
return (T) new AutoCredentialViewModel(application, lockPatternUtils,
challengeGenerator);
challengeGenerator, credentialModel);
}
} else if (modelClass.isAssignableFrom(DeviceFoldedViewModel.class)) {
return (T) new DeviceFoldedViewModel(new ScreenSizeFoldProvider(application),
@@ -93,10 +95,10 @@ public class BiometricsViewModelFactory implements ViewModelProvider.Factory {
} else if (modelClass.isAssignableFrom(FingerprintEnrollIntroViewModel.class)) {
final FingerprintRepository repository = provider.getFingerprintRepository(application);
final EnrollmentRequest request = extras.get(ENROLLMENT_REQUEST_KEY);
final Integer userId = extras.get(USER_ID_KEY);
if (repository != null && request != null && userId != null) {
final CredentialModel credentialModel = extras.get(CREDENTIAL_MODEL_KEY);
if (repository != null && request != null && credentialModel != null) {
return (T) new FingerprintEnrollIntroViewModel(application, repository, request,
userId);
credentialModel.getUserId());
}
} else if (modelClass.isAssignableFrom(FingerprintEnrollmentViewModel.class)) {
final FingerprintRepository repository = provider.getFingerprintRepository(application);
@@ -105,27 +107,27 @@ public class BiometricsViewModelFactory implements ViewModelProvider.Factory {
return (T) new FingerprintEnrollmentViewModel(application, repository, request);
}
} else if (modelClass.isAssignableFrom(FingerprintEnrollProgressViewModel.class)) {
final Integer userId = extras.get(USER_ID_KEY);
if (userId != null) {
final CredentialModel credentialModel = extras.get(CREDENTIAL_MODEL_KEY);
if (credentialModel != null) {
return (T) new FingerprintEnrollProgressViewModel(application,
new FingerprintUpdater(application), userId);
new FingerprintUpdater(application), credentialModel.getUserId());
}
} else if (modelClass.isAssignableFrom(FingerprintEnrollEnrollingViewModel.class)) {
final Integer userId = extras.get(USER_ID_KEY);
final CredentialModel credentialModel = extras.get(CREDENTIAL_MODEL_KEY);
final FingerprintRepository fingerprint = provider.getFingerprintRepository(
application);
if (fingerprint != null && userId != null) {
return (T) new FingerprintEnrollEnrollingViewModel(application, userId,
fingerprint);
if (fingerprint != null && credentialModel != null) {
return (T) new FingerprintEnrollEnrollingViewModel(application,
credentialModel.getUserId(), fingerprint);
}
} else if (modelClass.isAssignableFrom(FingerprintEnrollFinishViewModel.class)) {
final Integer userId = extras.get(USER_ID_KEY);
final CredentialModel credentialModel = extras.get(CREDENTIAL_MODEL_KEY);
final EnrollmentRequest request = extras.get(ENROLLMENT_REQUEST_KEY);
final FingerprintRepository fingerprint = provider.getFingerprintRepository(
application);
if (fingerprint != null && userId != null && request != null) {
return (T) new FingerprintEnrollFinishViewModel(application, userId, request,
fingerprint);
if (fingerprint != null && credentialModel != null && request != null) {
return (T) new FingerprintEnrollFinishViewModel(application,
credentialModel.getUserId(), request, fingerprint);
}
} else if (modelClass.isAssignableFrom(FingerprintEnrollErrorDialogViewModel.class)) {
final EnrollmentRequest request = extras.get(ENROLLMENT_REQUEST_KEY);

View File

@@ -80,20 +80,6 @@ class CredentialModel(bundle: Bundle?, private val clock: Clock) {
val isValidToken: Boolean
get() = token != null
val bundle: Bundle
/**
* Get a bundle which can be used to recreate CredentialModel
*/
get() {
val bundle = Bundle()
bundle.putInt(EXTRA_USER_ID, userId)
bundle.putLong(EXTRA_KEY_CHALLENGE, challenge)
bundle.putByteArray(EXTRA_KEY_CHALLENGE_TOKEN, token)
bundle.putLong(EXTRA_KEY_GK_PW_HANDLE, gkPwHandle)
return bundle
}
/** Returns a string representation of the object */
override fun toString(): String {
val gkPwHandleLen = "$gkPwHandle".length

View File

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

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