Files
app_Settings/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModel.kt
Milton Wu f8cdda45aa [BiometricsV2] Refactor AutoCredentialViewModel
Fix previous review comments on
I84bab0b46e023303c0046a6ae6886ab1cf9458b8

Bug: 286197659
Test: atest AutoCredentialViewModelTest
Test: atest biometrics-enrollment-test
Change-Id: I9b43a637439e941bdb884e3ae2f656d573364123
2023-08-09 06:39:48 +00:00

284 lines
11 KiB
Kotlin

/*
* 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 {
/** Callback that will be called later after challenge generated */
var 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 {
override var callback: GenerateChallengeCallback? = null
override fun generateChallenge(userId: Int) {
callback?.let {
fingerprintRepository.generateChallenge(userId) {
sensorId: Int, uid: Int, challenge: Long ->
it.onChallengeGenerated(sensorId, uid, challenge)
}
} ?:run {
Log.e(TAG, "generateChallenge, null callback")
}
}
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.callback = 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
}