diff --git a/Android.bp b/Android.bp index c55814bac49..1351d031b73 100644 --- a/Android.bp +++ b/Android.bp @@ -69,6 +69,7 @@ android_library { "androidx.appcompat_appcompat", "androidx.cardview_cardview", "androidx.compose.runtime_runtime-livedata", + "androidx.activity_activity-ktx", "androidx.preference_preference", "androidx.recyclerview_recyclerview", "androidx.window_window", diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 024c377a716..5cae1edd9d0 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -4914,6 +4914,20 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java index a8fa5273b56..3efa18f5f63 100644 --- a/src/com/android/settings/Settings.java +++ b/src/com/android/settings/Settings.java @@ -73,6 +73,7 @@ public class Settings extends SettingsActivity { } public static class FingerprintSettingsActivity extends SettingsActivity { /* empty */ } + public static class FingerprintSettingsActivityV2 extends SettingsActivity { /* empty */ } public static class CombinedBiometricSettingsActivity extends SettingsActivity { /* empty */ } public static class CombinedBiometricProfileSettingsActivity extends SettingsActivity { /* empty */ } public static class TetherSettingsActivity extends SettingsActivity { diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/binder/FingerprintViewBinder.kt b/src/com/android/settings/biometrics/fingerprint2/ui/binder/FingerprintViewBinder.kt new file mode 100644 index 00000000000..d4249ffa127 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/ui/binder/FingerprintViewBinder.kt @@ -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) + } + } + } + +} \ No newline at end of file diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsV2Fragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsV2Fragment.kt new file mode 100644 index 00000000000..9b85564fe3e --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsV2Fragment.kt @@ -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) + } + +} \ No newline at end of file diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintSettingsViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintSettingsViewModel.kt new file mode 100644 index 00000000000..6cddb24d5eb --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintSettingsViewModel.kt @@ -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 = 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 create( + modelClass: Class, + ): T { + + return FingerprintSettingsViewModel( + userId, null, null, null, fingerprintManager + ) as T + } + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/NextStepViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/NextStepViewModel.kt new file mode 100644 index 00000000000..1046f51b7f2 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/NextStepViewModel.kt @@ -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() + diff --git a/src/com/android/settings/core/gateway/SettingsGateway.java b/src/com/android/settings/core/gateway/SettingsGateway.java index b5d12a5258d..3100706bf26 100644 --- a/src/com/android/settings/core/gateway/SettingsGateway.java +++ b/src/com/android/settings/core/gateway/SettingsGateway.java @@ -72,6 +72,7 @@ import com.android.settings.biometrics.combination.CombinedBiometricProfileSetti import com.android.settings.biometrics.combination.CombinedBiometricSettings; import com.android.settings.biometrics.face.FaceSettings; 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.BluetoothDeviceDetailsFragment; import com.android.settings.bluetooth.BluetoothFindBroadcastsFragment; @@ -266,6 +267,7 @@ public class SettingsGateway { AssistGestureSettings.class.getName(), FaceSettings.class.getName(), FingerprintSettings.FingerprintSettingsFragment.class.getName(), + FingerprintSettingsV2Fragment.class.getName(), CombinedBiometricSettings.class.getName(), CombinedBiometricProfileSettings.class.getName(), SwipeToNotificationSettings.class.getName(), diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp index 8dfb52e669d..cb57bc7dd7d 100644 --- a/tests/unit/Android.bp +++ b/tests/unit/Android.bp @@ -32,6 +32,7 @@ android_test { "platform-test-annotations", "truth-prebuilt", "androidx.test.uiautomator_uiautomator", + "kotlinx_coroutines_test", // Don't add SettingsLib libraries here - you can use them directly as they are in the // instrumented Settings app. ], @@ -39,8 +40,7 @@ android_test { errorprone: { javacflags: ["-Xep:CheckReturnValue:WARN"] }, - - // Include all test java files. + // Include all test java/kotlin files. srcs: [ "src/**/*.java", "src/**/*.kt", diff --git a/tests/unit/src/com/android/settings/fingerprint2/viewmodel/FingerprintSettingsViewModelTest.kt b/tests/unit/src/com/android/settings/fingerprint2/viewmodel/FingerprintSettingsViewModelTest.kt new file mode 100644 index 00000000000..738954339d3 --- /dev/null +++ b/tests/unit/src/com/android/settings/fingerprint2/viewmodel/FingerprintSettingsViewModelTest.kt @@ -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() + } +} \ No newline at end of file