Merge "Moving FingerprintSettings to Kotlin"

This commit is contained in:
Joshua Mccloskey
2023-06-29 12:31:22 +00:00
committed by Android (Google) Code Review
11 changed files with 985 additions and 2 deletions

View File

@@ -69,6 +69,7 @@ android_library {
"androidx.appcompat_appcompat", "androidx.appcompat_appcompat",
"androidx.cardview_cardview", "androidx.cardview_cardview",
"androidx.compose.runtime_runtime-livedata", "androidx.compose.runtime_runtime-livedata",
"androidx.activity_activity-ktx",
"androidx.preference_preference", "androidx.preference_preference",
"androidx.recyclerview_recyclerview", "androidx.recyclerview_recyclerview",
"androidx.window_window", "androidx.window_window",

View File

@@ -4914,6 +4914,20 @@
<activity android:name=".spa.SpaBridgeActivity" android:exported="false"/> <activity android:name=".spa.SpaBridgeActivity" android:exported="false"/>
<activity android:name=".spa.SpaAppBridgeActivity" android:exported="false"/> <activity android:name=".spa.SpaAppBridgeActivity" android:exported="false"/>
<activity android:name=".Settings$FingerprintSettingsActivityV2"
android:label="@string/security_settings_fingerprint_preference_title"
android:exported="false"
android:icon="@drawable/ic_fingerprint_header">
<intent-filter>
<action android:name="android.settings.FINGERPRINT_SETTINGS_V2" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data android:name="com.android.settings.FRAGMENT_CLASS"
android:value="com.android.settings.biometrics.fingerprint2.ui.fragment.FingerprintSettingsV2Fragment" />
<meta-data android:name="com.android.settings.HIGHLIGHT_MENU_KEY"
android:value="@string/menu_key_security"/>
</activity>
<activity-alias android:name="UsageStatsActivity" <activity-alias android:name="UsageStatsActivity"
android:exported="true" android:exported="true"
android:label="@string/testing_usage_stats" android:label="@string/testing_usage_stats"

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"/>

View File

@@ -73,6 +73,7 @@ public class Settings extends SettingsActivity {
} }
public static class FingerprintSettingsActivity extends SettingsActivity { /* empty */ } public static class FingerprintSettingsActivity extends SettingsActivity { /* empty */ }
public static class FingerprintSettingsActivityV2 extends SettingsActivity { /* empty */ }
public static class CombinedBiometricSettingsActivity extends SettingsActivity { /* empty */ } public static class CombinedBiometricSettingsActivity extends SettingsActivity { /* empty */ }
public static class CombinedBiometricProfileSettingsActivity extends SettingsActivity { /* empty */ } public static class CombinedBiometricProfileSettingsActivity extends SettingsActivity { /* empty */ }
public static class TetherSettingsActivity extends SettingsActivity { public static class TetherSettingsActivity extends SettingsActivity {

View File

@@ -0,0 +1,128 @@
/*
* 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.biometrics.fingerprint2.ui.binder
import androidx.lifecycle.LifecycleCoroutineScope
import com.android.settings.biometrics.fingerprint2.ui.fragment.FingerprintSettingsV2Fragment
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.EnrollAdditionalFingerprint
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.EnrollFirstFingerprint
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsViewModel
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettings
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettingsWithResult
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.LaunchConfirmDeviceCredential
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.ShowSettings
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch
/**
* Binds a [FingerprintSettingsViewModel] to a [FingerprintSettingsV2Fragment]
*/
object FingerprintViewBinder {
interface Binding {
fun onConfirmDevice(wasSuccessful: Boolean, theGateKeeperPasswordHandle: Long?)
fun onEnrollSuccess()
fun onEnrollAdditionalFailure()
fun onEnrollFirstFailure(reason: String)
fun onEnrollFirstFailure(reason: String, resultCode: Int)
fun onEnrollFirst(token: ByteArray?, keyChallenge: Long?)
}
/** Initial listener for the first enrollment request */
fun bind(
viewModel: FingerprintSettingsViewModel,
lifecycleScope: LifecycleCoroutineScope,
token: ByteArray?,
challenge: Long?,
launchFullFingerprintEnrollment: (
userId: Int,
gateKeeperPasswordHandle: Long?,
challenge: Long?,
challengeToken: ByteArray?
) -> Unit,
launchAddFingerprint: (userId: Int, challengeToken: ByteArray?) -> Unit,
launchConfirmOrChooseLock: (userId: Int) -> Unit,
finish: () -> Unit,
setResultExternal: (resultCode: Int) -> Unit,
): Binding {
lifecycleScope.launch {
viewModel.nextStep.filterNotNull().collect { nextStep ->
when (nextStep) {
is EnrollFirstFingerprint -> launchFullFingerprintEnrollment(
nextStep.userId,
nextStep.gateKeeperPasswordHandle,
nextStep.challenge,
nextStep.challengeToken
)
is EnrollAdditionalFingerprint -> launchAddFingerprint(
nextStep.userId, nextStep.challengeToken
)
is LaunchConfirmDeviceCredential -> launchConfirmOrChooseLock(nextStep.userId)
is FinishSettings -> {
println("Finishing due to ${nextStep.reason}")
finish()
}
is FinishSettingsWithResult -> {
println("Finishing with result ${nextStep.result} due to ${nextStep.reason}")
setResultExternal(nextStep.result)
finish()
}
is ShowSettings -> println("show settings")
}
viewModel.onUiCommandExecuted()
}
}
viewModel.updateTokenAndChallenge(token, if (challenge == -1L) null else challenge)
return object : Binding {
override fun onConfirmDevice(
wasSuccessful: Boolean, theGateKeeperPasswordHandle: Long?
) {
viewModel.onConfirmDevice(wasSuccessful, theGateKeeperPasswordHandle)
}
override fun onEnrollSuccess() {
viewModel.onEnrollSuccess()
}
override fun onEnrollAdditionalFailure() {
viewModel.onEnrollAdditionalFailure()
}
override fun onEnrollFirstFailure(reason: String) {
viewModel.onEnrollFirstFailure(reason)
}
override fun onEnrollFirstFailure(reason: String, resultCode: Int) {
viewModel.onEnrollFirstFailure(reason, resultCode)
}
override fun onEnrollFirst(token: ByteArray?, keyChallenge: Long?) {
viewModel.onEnrollFirst(token, keyChallenge)
}
}
}
}

View File

@@ -0,0 +1,252 @@
/*
* 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.biometrics.fingerprint2.ui.fragment
import android.app.Activity
import android.app.settings.SettingsEnums
import android.content.Context.FINGERPRINT_SERVICE
import android.content.Intent
import android.hardware.fingerprint.FingerprintManager
import android.os.Bundle
import android.util.FeatureFlagUtils
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import com.android.settings.R
import com.android.settings.Utils
import com.android.settings.biometrics.BiometricEnrollBase
import com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling
import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroductionInternal
import com.android.settings.biometrics.fingerprint2.ui.binder.FingerprintViewBinder
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsViewModel
import com.android.settings.core.SettingsBaseActivity
import com.android.settings.dashboard.DashboardFragment
import com.android.settings.password.ChooseLockGeneric
import com.android.settings.password.ChooseLockSettingsHelper
import com.android.settingslib.transition.SettingsTransitionHelper
const val TAG = "FingerprintSettingsV2Fragment"
/**
* A class responsible for showing FingerprintSettings. Typical activity Flows are
* 1. Settings > FingerprintSettings > PIN/PATTERN/PASS -> FingerprintSettings
* 2. FingerprintSettings -> FingerprintEnrollment fow
*
* This page typically allows for
* 1. Fingerprint deletion
* 2. Fingerprint enrollment
* 3. Renaming a fingerprint
* 4. Enabling/Disabling a feature
*/
class FingerprintSettingsV2Fragment : DashboardFragment() {
private lateinit var binding: FingerprintViewBinder.Binding
private val launchFirstEnrollmentListener =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val resultCode = result.resultCode
val data = result.data
Log.d(
TAG, "onEnrollFirstFingerprint($resultCode, $data)"
)
if (resultCode != BiometricEnrollBase.RESULT_FINISHED || data == null) {
if (resultCode == BiometricEnrollBase.RESULT_TIMEOUT) {
binding.onEnrollFirstFailure(
"Received RESULT_TIMEOUT when enrolling", resultCode
)
} else {
binding.onEnrollFirstFailure("Incorrect resultCode or data was null")
}
} else {
val token =
data.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN)
val keyChallenge = data.getExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE) as Long?
binding.onEnrollFirst(token, keyChallenge)
}
}
/** Result listener for launching enrollments **after** a user has reached the settings page. */
private val launchAdditionalFingerprintListener =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val resultCode = result.resultCode
Log.d(
TAG, "onEnrollAdditionalFingerprint($resultCode)"
)
if (resultCode == BiometricEnrollBase.RESULT_TIMEOUT) {
binding.onEnrollAdditionalFailure()
} else {
binding.onEnrollSuccess()
}
}
/** Result listener for ChooseLock activity flow. */
private val confirmDeviceResultListener =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val resultCode = result.resultCode
val data = result.data
onConfirmDevice(resultCode, data)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
// This is needed to support ChooseLockSettingBuilder...show(). All other activity
// calls should use the registerForActivity method call.
super.onActivityResult(requestCode, resultCode, data)
val wasSuccessful =
resultCode == BiometricEnrollBase.RESULT_FINISHED || resultCode == Activity.RESULT_OK
val gateKeeperPasswordHandle =
data?.getExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE) as Long?
binding.onConfirmDevice(wasSuccessful, gateKeeperPasswordHandle)
}
override fun onCreate(icicle: Bundle?) {
super.onCreate(icicle)
if (!FeatureFlagUtils.isEnabled(
context, FeatureFlagUtils.SETTINGS_BIOMETRICS2_FINGERPRINT_SETTINGS
)
) {
Log.d(
TAG, "Finishing due to feature not being enabled"
)
finish()
return
}
val viewModel = ViewModelProvider(
this, FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
requireContext().applicationContext.userId, requireContext().getSystemService(
FINGERPRINT_SERVICE
) as FingerprintManager
)
)[FingerprintSettingsViewModel::class.java]
val token = intent.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN)
val challenge = intent.getLongExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, -1L)
binding = FingerprintViewBinder.bind(
viewModel,
lifecycleScope,
token,
challenge,
::launchFullFingerprintEnrollment,
::launchAddFingerprint,
::launchConfirmOrChooseLock,
::finish,
::setResultExternal,
)
}
override fun getMetricsCategory(): Int {
return SettingsEnums.FINGERPRINT
}
override fun getPreferenceScreenResId(): Int {
return R.xml.security_settings_fingerprint_limbo
}
override fun getLogTag(): String {
return TAG
}
/**
* Helper function that will try and launch confirm lock, if that fails we will prompt user
* to choose a PIN/PATTERN/PASS.
*/
private fun launchConfirmOrChooseLock(userId: Int) {
val intent = Intent()
val builder = ChooseLockSettingsHelper.Builder(requireActivity(), this)
val launched = builder.setRequestCode(BiometricEnrollBase.CONFIRM_REQUEST)
.setTitle(getString(R.string.security_settings_fingerprint_preference_title))
.setRequestGatekeeperPasswordHandle(true).setUserId(userId).setForegroundOnly(true)
.setReturnCredentials(true).show()
if (!launched) {
intent.setClassName(
Utils.SETTINGS_PACKAGE_NAME, ChooseLockGeneric::class.java.name
)
intent.putExtra(
ChooseLockGeneric.ChooseLockGenericFragment.HIDE_INSECURE_OPTIONS, true
)
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, true)
intent.putExtra(Intent.EXTRA_USER_ID, userId)
confirmDeviceResultListener.launch(intent)
}
}
/**
* Helper for confirming a PIN/PATTERN/PASS
*/
private fun onConfirmDevice(resultCode: Int, data: Intent?) {
val wasSuccessful =
resultCode == BiometricEnrollBase.RESULT_FINISHED || resultCode == Activity.RESULT_OK
val gateKeeperPasswordHandle =
data?.getExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE) as Long?
binding.onConfirmDevice(wasSuccessful, gateKeeperPasswordHandle)
}
/**
* Helper function to launch fingerprint enrollment(This should be the default behavior
* when a user enters their PIN/PATTERN/PASS and no fingerprints are enrolled.
*/
private fun launchFullFingerprintEnrollment(
userId: Int,
gateKeeperPasswordHandle: Long?,
challenge: Long?,
challengeToken: ByteArray?,
) {
val intent = Intent()
intent.setClassName(
Utils.SETTINGS_PACKAGE_NAME, FingerprintEnrollIntroductionInternal::class.java.name
)
intent.putExtra(BiometricEnrollBase.EXTRA_FROM_SETTINGS_SUMMARY, true)
intent.putExtra(
SettingsBaseActivity.EXTRA_PAGE_TRANSITION_TYPE,
SettingsTransitionHelper.TransitionType.TRANSITION_SLIDE
)
intent.putExtra(Intent.EXTRA_USER_ID, userId)
if (gateKeeperPasswordHandle != null) {
intent.putExtra(
ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gateKeeperPasswordHandle
)
} else {
intent.putExtra(
ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, challengeToken
)
intent.putExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, challenge)
}
launchFirstEnrollmentListener.launch(intent)
}
private fun setResultExternal(resultCode: Int) {
setResult(resultCode)
}
/** Helper to launch an add fingerprint request */
private fun launchAddFingerprint(userId: Int, challengeToken: ByteArray?) {
val intent = Intent()
intent.setClassName(
Utils.SETTINGS_PACKAGE_NAME, FingerprintEnrollEnrolling::class.qualifiedName.toString()
)
intent.putExtra(Intent.EXTRA_USER_ID, userId)
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, challengeToken)
launchAdditionalFingerprintListener.launch(intent)
}
}

View File

@@ -0,0 +1,187 @@
/*
* 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.biometrics.fingerprint2.ui.viewmodel
import android.hardware.fingerprint.FingerprintManager
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
/**
* Models the UI state for fingerprint settings.
*/
class FingerprintSettingsViewModel(
private val userId: Int,
gateKeeperPassword: Long?,
theChallenge: Long?,
theChallengeToken: ByteArray?,
private val fingerprintManager: FingerprintManager
) : ViewModel() {
private val _nextStep: MutableStateFlow<NextStepViewModel?> = MutableStateFlow(null)
/**
* This flow represents the high level state for the FingerprintSettingsV2Fragment. The
* consumer of this flow should call [onUiCommandExecuted] which will set the state to null,
* confirming that the UI has consumed the last command and is ready to consume another
* command.
*/
val nextStep = _nextStep.asStateFlow()
private var gateKeeperPasswordHandle: Long? = gateKeeperPassword
private var challenge: Long? = theChallenge
private var challengeToken: ByteArray? = theChallengeToken
/**
* Indicates to the view model that a confirm device credential action has been completed
* with a [theGateKeeperPasswordHandle] which will be used for [FingerprintManager]
* operations such as [FingerprintManager.enroll].
*/
fun onConfirmDevice(wasSuccessful: Boolean, theGateKeeperPasswordHandle: Long?) {
if (!wasSuccessful) {
launchFinishSettings("ConfirmDeviceCredential was unsuccessful")
return
}
if (theGateKeeperPasswordHandle == null) {
launchFinishSettings("ConfirmDeviceCredential gatekeeper password was null")
return
}
gateKeeperPasswordHandle = theGateKeeperPasswordHandle
launchEnrollNextStep()
}
/**
* Notifies that enrollment was successful.
*/
fun onEnrollSuccess() {
_nextStep.update {
ShowSettings(userId)
}
}
/**
* Notifies that an additional enrollment failed.
*/
fun onEnrollAdditionalFailure() {
launchFinishSettings("Failed to enroll additional fingerprint")
}
/**
* Notifies that the first enrollment failed.
*/
fun onEnrollFirstFailure(reason: String) {
launchFinishSettings(reason)
}
/**
* Notifies that first enrollment failed (with resultCode)
*/
fun onEnrollFirstFailure(reason: String, resultCode: Int) {
launchFinishSettings(reason, resultCode)
}
/**
* Notifies that a users first enrollment succeeded.
*/
fun onEnrollFirst(token: ByteArray?, keyChallenge: Long?) {
if (token == null) {
launchFinishSettings("Error, empty token")
return
}
if (keyChallenge == null) {
launchFinishSettings("Error, empty keyChallenge")
return
}
challengeToken = token
challenge = keyChallenge
_nextStep.update {
ShowSettings(userId)
}
}
/**
* Indicates if this settings activity has been called with correct token and challenge
* and that we do not need to launch confirm device credential.
*/
fun updateTokenAndChallenge(token: ByteArray?, theChallenge: Long?) {
challengeToken = token
challenge = theChallenge
if (challengeToken == null) {
_nextStep.update {
LaunchConfirmDeviceCredential(userId)
}
} else {
launchEnrollNextStep()
}
}
/**
* Indicates a UI command has been consumed by the UI, and the logic can send another
* UI command.
*/
fun onUiCommandExecuted() {
_nextStep.update {
null
}
}
private fun launchEnrollNextStep() {
if (fingerprintManager.getEnrolledFingerprints(userId).isEmpty()) {
_nextStep.update {
EnrollFirstFingerprint(userId, gateKeeperPasswordHandle, challenge, challengeToken)
}
} else {
_nextStep.update {
ShowSettings(userId)
}
}
}
private fun launchFinishSettings(reason: String) {
_nextStep.update {
FinishSettings(reason)
}
}
private fun launchFinishSettings(reason: String, errorCode: Int) {
_nextStep.update {
FinishSettingsWithResult(errorCode, reason)
}
}
class FingerprintSettingsViewModelFactory(
private val userId: Int,
private val fingerprintManager: FingerprintManager,
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(
modelClass: Class<T>,
): T {
return FingerprintSettingsViewModel(
userId, null, null, null, fingerprintManager
) as T
}
}
}

View File

@@ -0,0 +1,48 @@
/*
* 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.biometrics.fingerprint2.ui.viewmodel
/**
* A class to represent a next step for FingerprintSettings. This is typically to perform an action
* such that launches another activity such as EnrollFirstFingerprint() or
* LaunchConfirmDeviceCredential().
*/
sealed class NextStepViewModel
data class EnrollFirstFingerprint(
val userId: Int, val gateKeeperPasswordHandle: Long?,
val challenge: Long?,
val challengeToken: ByteArray?,
) : NextStepViewModel()
data class EnrollAdditionalFingerprint(
val userId: Int,
val challengeToken: ByteArray?,
) : NextStepViewModel()
data class FinishSettings(
val reason: String
) : NextStepViewModel()
data class FinishSettingsWithResult(
val result: Int, val reason: String
) : NextStepViewModel()
data class ShowSettings(val userId: Int) : NextStepViewModel()
data class LaunchConfirmDeviceCredential(val userId: Int) : NextStepViewModel()

View File

@@ -72,6 +72,7 @@ import com.android.settings.biometrics.combination.CombinedBiometricProfileSetti
import com.android.settings.biometrics.combination.CombinedBiometricSettings; import com.android.settings.biometrics.combination.CombinedBiometricSettings;
import com.android.settings.biometrics.face.FaceSettings; import com.android.settings.biometrics.face.FaceSettings;
import com.android.settings.biometrics.fingerprint.FingerprintSettings; import com.android.settings.biometrics.fingerprint.FingerprintSettings;
import com.android.settings.biometrics.fingerprint2.ui.fragment.FingerprintSettingsV2Fragment;
import com.android.settings.bluetooth.BluetoothBroadcastDialog; import com.android.settings.bluetooth.BluetoothBroadcastDialog;
import com.android.settings.bluetooth.BluetoothDeviceDetailsFragment; import com.android.settings.bluetooth.BluetoothDeviceDetailsFragment;
import com.android.settings.bluetooth.BluetoothFindBroadcastsFragment; import com.android.settings.bluetooth.BluetoothFindBroadcastsFragment;
@@ -266,6 +267,7 @@ public class SettingsGateway {
AssistGestureSettings.class.getName(), AssistGestureSettings.class.getName(),
FaceSettings.class.getName(), FaceSettings.class.getName(),
FingerprintSettings.FingerprintSettingsFragment.class.getName(), FingerprintSettings.FingerprintSettingsFragment.class.getName(),
FingerprintSettingsV2Fragment.class.getName(),
CombinedBiometricSettings.class.getName(), CombinedBiometricSettings.class.getName(),
CombinedBiometricProfileSettings.class.getName(), CombinedBiometricProfileSettings.class.getName(),
SwipeToNotificationSettings.class.getName(), SwipeToNotificationSettings.class.getName(),

View File

@@ -32,6 +32,7 @@ android_test {
"platform-test-annotations", "platform-test-annotations",
"truth-prebuilt", "truth-prebuilt",
"androidx.test.uiautomator_uiautomator", "androidx.test.uiautomator_uiautomator",
"kotlinx_coroutines_test",
// Don't add SettingsLib libraries here - you can use them directly as they are in the // Don't add SettingsLib libraries here - you can use them directly as they are in the
// instrumented Settings app. // instrumented Settings app.
], ],
@@ -39,8 +40,7 @@ android_test {
errorprone: { errorprone: {
javacflags: ["-Xep:CheckReturnValue:WARN"] javacflags: ["-Xep:CheckReturnValue:WARN"]
}, },
// Include all test java/kotlin files.
// Include all test java files.
srcs: [ srcs: [
"src/**/*.java", "src/**/*.java",
"src/**/*.kt", "src/**/*.kt",

View File

@@ -0,0 +1,332 @@
/*
* 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.fingerprint2.viewmodel
import android.hardware.fingerprint.Fingerprint
import android.hardware.fingerprint.FingerprintManager
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.EnrollFirstFingerprint
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettings
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettingsWithResult
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.LaunchConfirmDeviceCredential
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.NextStepViewModel
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.ShowSettings
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsViewModel
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.launch
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.anyInt
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoJUnitRunner
import org.mockito.Mockito.`when` as whenever
@RunWith(MockitoJUnitRunner::class)
class FingerprintSettingsViewModelTest {
@JvmField
@Rule
var rule = MockitoJUnit.rule()
@Mock
private lateinit var fingerprintManager: FingerprintManager
private lateinit var underTest: FingerprintSettingsViewModel
private val defaultUserId = 0
@Before
fun setup() {
// @formatter:off
underTest = FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
defaultUserId,
fingerprintManager,
).create(FingerprintSettingsViewModel::class.java)
// @formatter:on
}
@Test
fun testNoGateKeeper_launchesConfirmDeviceCredential() = runTest {
var nextStep: NextStepViewModel? = null
val job = launch {
underTest.nextStep.collect {
nextStep = it
}
}
underTest.updateTokenAndChallenge(null, null)
runCurrent()
assertThat(nextStep).isEqualTo(LaunchConfirmDeviceCredential(defaultUserId))
job.cancel()
}
@Test
fun testConfirmDevice_fails() = runTest {
var nextStep: NextStepViewModel? = null
val job = launch {
underTest.nextStep.collect {
nextStep = it
}
}
underTest.updateTokenAndChallenge(null, null)
underTest.onConfirmDevice(false, null)
runCurrent()
assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
job.cancel()
}
@Test
fun confirmDeviceSuccess_noGateKeeper() = runTest {
var nextStep: NextStepViewModel? = null
val job = launch {
underTest.nextStep.collect {
nextStep = it
}
}
underTest.updateTokenAndChallenge(null, null)
underTest.onConfirmDevice(true, null)
runCurrent()
assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
job.cancel()
}
@Test
fun confirmDeviceSuccess_launchesEnrollment_ifNoPreviousEnrollments() = runTest {
whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(emptyList())
var nextStep: NextStepViewModel? = null
val job = launch {
underTest.nextStep.collect {
nextStep = it
}
}
underTest.updateTokenAndChallenge(null, null)
underTest.onConfirmDevice(true, 10L)
runCurrent()
assertThat(nextStep).isEqualTo(EnrollFirstFingerprint(defaultUserId, 10L, null, null))
job.cancel()
}
@Test
fun firstEnrollment_fails() = runTest {
whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(emptyList())
var nextStep: NextStepViewModel? = null
val job = launch {
underTest.nextStep.collect {
nextStep = it
}
}
underTest.updateTokenAndChallenge(null, null)
underTest.onConfirmDevice(true, 10L)
underTest.onEnrollFirstFailure("We failed!!")
runCurrent()
assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
job.cancel()
}
@Test
fun firstEnrollment_failsWithReason() = runTest {
whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(emptyList())
var nextStep: NextStepViewModel? = null
val job = launch {
underTest.nextStep.collect {
nextStep = it
}
}
val failStr = "We failed!!"
val failReason = 101
underTest.updateTokenAndChallenge(null, null)
underTest.onConfirmDevice(true, 10L)
underTest.onEnrollFirstFailure(failStr, failReason)
runCurrent()
assertThat(nextStep).isEqualTo(FinishSettingsWithResult(failReason, failStr))
job.cancel()
}
@Test
fun firstEnrollmentSucceeds_noToken() = runTest {
whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(emptyList())
var nextStep: NextStepViewModel? = null
val job = launch {
underTest.nextStep.collect {
nextStep = it
}
}
underTest.updateTokenAndChallenge(null, null)
underTest.onConfirmDevice(true, 10L)
underTest.onEnrollFirst(null, null)
runCurrent()
assertThat(nextStep).isEqualTo(FinishSettings("Error, empty token"))
job.cancel()
}
@Test
fun firstEnrollmentSucceeds_noKeyChallenge() = runTest {
whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(emptyList())
var nextStep: NextStepViewModel? = null
val job = launch {
underTest.nextStep.collect {
nextStep = it
}
}
val byteArray = ByteArray(1) {
3
}
underTest.updateTokenAndChallenge(null, null)
underTest.onConfirmDevice(true, 10L)
underTest.onEnrollFirst(byteArray, null)
runCurrent()
assertThat(nextStep).isEqualTo(FinishSettings("Error, empty keyChallenge"))
job.cancel()
}
@Test
fun firstEnrollment_succeeds() = runTest {
whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(emptyList())
var nextStep: NextStepViewModel? = null
val job = launch {
underTest.nextStep.collect {
nextStep = it
}
}
val byteArray = ByteArray(1) {
3
}
val keyChallenge = 89L
underTest.updateTokenAndChallenge(null, null)
underTest.onConfirmDevice(true, 10L)
underTest.onEnrollFirst(byteArray, keyChallenge)
runCurrent()
assertThat(nextStep).isEqualTo(ShowSettings(defaultUserId))
job.cancel()
}
@Test
fun confirmDeviceCredential_withEnrolledFingerprint_showsSettings() = runTest {
whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(
listOf(
Fingerprint(
"a", 1, 2, 3L
)
)
)
var nextStep: NextStepViewModel? = null
val job = launch {
underTest.nextStep.collect {
nextStep = it
}
}
underTest.updateTokenAndChallenge(null, null)
underTest.onConfirmDevice(true, 10L)
runCurrent()
assertThat(nextStep).isEqualTo(ShowSettings(defaultUserId))
job.cancel()
}
@Test
fun enrollAdditionalFingerprints_fails() = runTest {
whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(
listOf(
Fingerprint(
"a", 1, 2, 3L
)
)
)
var nextStep: NextStepViewModel? = null
val job = launch {
underTest.nextStep.collect {
nextStep = it
}
}
underTest.updateTokenAndChallenge(null, null)
underTest.onConfirmDevice(true, 10L)
underTest.onEnrollAdditionalFailure()
runCurrent()
assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
job.cancel()
}
@Test
fun enrollAdditional_success() = runTest {
whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(
listOf(
Fingerprint(
"a", 1, 2, 3L
)
)
)
var nextStep: NextStepViewModel? = null
val job = launch {
underTest.nextStep.collect {
nextStep = it
}
}
underTest.updateTokenAndChallenge(null, null)
underTest.onConfirmDevice(true, 10L)
underTest.onEnrollSuccess()
runCurrent()
assertThat(nextStep).isEqualTo(ShowSettings(defaultUserId))
job.cancel()
}
}