Merge pull request #6784 from vector-im/fix/jorgem/lockscreen-device-locked
Fix lockscreen's 'device locked' crash on Android 12 and 12L devices
This commit is contained in:
commit
fe61fa844e
|
@ -0,0 +1 @@
|
||||||
|
Fix crash when biometric key is used when coming back to foreground and KeyStore reports that the device is still locked.
|
|
@ -31,7 +31,6 @@ import androidx.test.filters.SdkSuppress
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import im.vector.app.TestBuildVersionSdkIntProvider
|
import im.vector.app.TestBuildVersionSdkIntProvider
|
||||||
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration
|
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration
|
||||||
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguratorProvider
|
|
||||||
import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode
|
import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode
|
||||||
import im.vector.app.features.pin.lockscreen.crypto.LockScreenCryptoConstants
|
import im.vector.app.features.pin.lockscreen.crypto.LockScreenCryptoConstants
|
||||||
import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeyRepository
|
import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeyRepository
|
||||||
|
@ -40,6 +39,7 @@ import im.vector.app.features.pin.lockscreen.ui.fallbackprompt.FallbackBiometric
|
||||||
import im.vector.app.features.pin.lockscreen.utils.DevicePromptCheck
|
import im.vector.app.features.pin.lockscreen.utils.DevicePromptCheck
|
||||||
import io.mockk.clearAllMocks
|
import io.mockk.clearAllMocks
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
|
import io.mockk.justRun
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.mockkObject
|
import io.mockk.mockkObject
|
||||||
import io.mockk.mockkStatic
|
import io.mockk.mockkStatic
|
||||||
|
@ -54,8 +54,10 @@ import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.amshove.kluent.coInvoking
|
||||||
import org.amshove.kluent.shouldBeFalse
|
import org.amshove.kluent.shouldBeFalse
|
||||||
import org.amshove.kluent.shouldBeTrue
|
import org.amshove.kluent.shouldBeTrue
|
||||||
|
import org.amshove.kluent.shouldThrow
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Ignore
|
import org.junit.Ignore
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
@ -239,36 +241,35 @@ class BiometricHelperTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R) // Due to some issues with mockk and CryptoObject initialization
|
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R) // Due to some issues with mockk and CryptoObject initialization
|
||||||
fun authenticateCreatesSystemKeyIfNeededOnSuccessOnAndroidM() = runTest {
|
fun enableAuthenticationDeletesSystemKeyOnFailure() = runTest {
|
||||||
buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
|
buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
|
||||||
every { lockScreenKeyRepository.isSystemKeyValid() } returns true
|
|
||||||
val mockAuthChannel = Channel<Boolean>(capacity = 1)
|
val mockAuthChannel = Channel<Boolean>(capacity = 1)
|
||||||
val biometricUtils = spyk(createBiometricHelper(createDefaultConfiguration(isBiometricsEnabled = true))) {
|
val biometricUtils = spyk(createBiometricHelper(createDefaultConfiguration(isBiometricsEnabled = true))) {
|
||||||
every { createAuthChannel() } returns mockAuthChannel
|
every { createAuthChannel() } returns mockAuthChannel
|
||||||
every { authenticateWithPromptInternal(any(), any(), any()) } returns mockk()
|
every { authenticateWithPromptInternal(any(), any(), any()) } returns mockk()
|
||||||
}
|
}
|
||||||
|
justRun { lockScreenKeyRepository.deleteSystemKey() }
|
||||||
|
|
||||||
val latch = CountDownLatch(1)
|
val latch = CountDownLatch(1)
|
||||||
val intent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, LockScreenTestActivity::class.java)
|
val intent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, LockScreenTestActivity::class.java)
|
||||||
ActivityScenario.launch<LockScreenTestActivity>(intent).onActivity { activity ->
|
ActivityScenario.launch<LockScreenTestActivity>(intent).onActivity { activity ->
|
||||||
activity.lifecycleScope.launch {
|
activity.lifecycleScope.launch {
|
||||||
|
val exception = IllegalStateException("Some error")
|
||||||
launch {
|
launch {
|
||||||
mockAuthChannel.send(true)
|
mockAuthChannel.close(exception)
|
||||||
mockAuthChannel.close()
|
|
||||||
}
|
}
|
||||||
biometricUtils.authenticate(activity).collect()
|
coInvoking { biometricUtils.enableAuthentication(activity).collect() } shouldThrow exception
|
||||||
latch.countDown()
|
latch.countDown()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
latch.await(1, TimeUnit.SECONDS)
|
latch.await(1, TimeUnit.SECONDS)
|
||||||
verify { lockScreenKeyRepository.ensureSystemKey() }
|
verify { lockScreenKeyRepository.deleteSystemKey() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createBiometricHelper(configuration: LockScreenConfiguration): BiometricHelper {
|
private fun createBiometricHelper(configuration: LockScreenConfiguration): BiometricHelper {
|
||||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
val configProvider = LockScreenConfiguratorProvider(configuration)
|
return BiometricHelper(configuration, context, lockScreenKeyRepository, biometricManager, buildVersionSdkIntProvider)
|
||||||
return BiometricHelper(context, lockScreenKeyRepository, configProvider, biometricManager, buildVersionSdkIntProvider)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createDefaultConfiguration(
|
private fun createDefaultConfiguration(
|
||||||
|
|
|
@ -17,8 +17,6 @@
|
||||||
package im.vector.app.features.pin.lockscreen.crypto
|
package im.vector.app.features.pin.lockscreen.crypto
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import im.vector.app.features.pin.lockscreen.crypto.migrations.LegacyPinCodeMigrator
|
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
|
||||||
import io.mockk.clearAllMocks
|
import io.mockk.clearAllMocks
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
|
@ -44,8 +42,6 @@ class LockScreenKeyRepositoryTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var lockScreenKeyRepository: LockScreenKeyRepository
|
private lateinit var lockScreenKeyRepository: LockScreenKeyRepository
|
||||||
private val legacyPinCodeMigrator: LegacyPinCodeMigrator = mockk(relaxed = true)
|
|
||||||
private val vectorPreferences: VectorPreferences = mockk(relaxed = true)
|
|
||||||
|
|
||||||
private val keyStore: KeyStore by lazy {
|
private val keyStore: KeyStore by lazy {
|
||||||
KeyStore.getInstance(LockScreenCryptoConstants.ANDROID_KEY_STORE).also { it.load(null) }
|
KeyStore.getInstance(LockScreenCryptoConstants.ANDROID_KEY_STORE).also { it.load(null) }
|
||||||
|
|
|
@ -24,6 +24,7 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.airbnb.mvrx.args
|
import com.airbnb.mvrx.args
|
||||||
|
import com.airbnb.mvrx.asMavericksArgs
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.replaceFragment
|
import im.vector.app.core.extensions.replaceFragment
|
||||||
|
@ -33,7 +34,7 @@ import im.vector.app.databinding.FragmentPinBinding
|
||||||
import im.vector.app.features.MainActivity
|
import im.vector.app.features.MainActivity
|
||||||
import im.vector.app.features.MainActivityArgs
|
import im.vector.app.features.MainActivityArgs
|
||||||
import im.vector.app.features.pin.lockscreen.biometrics.BiometricAuthError
|
import im.vector.app.features.pin.lockscreen.biometrics.BiometricAuthError
|
||||||
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguratorProvider
|
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration
|
||||||
import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode
|
import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode
|
||||||
import im.vector.app.features.pin.lockscreen.ui.AuthMethod
|
import im.vector.app.features.pin.lockscreen.ui.AuthMethod
|
||||||
import im.vector.app.features.pin.lockscreen.ui.LockScreenFragment
|
import im.vector.app.features.pin.lockscreen.ui.LockScreenFragment
|
||||||
|
@ -51,7 +52,7 @@ data class PinArgs(
|
||||||
class PinFragment @Inject constructor(
|
class PinFragment @Inject constructor(
|
||||||
private val pinCodeStore: PinCodeStore,
|
private val pinCodeStore: PinCodeStore,
|
||||||
private val vectorPreferences: VectorPreferences,
|
private val vectorPreferences: VectorPreferences,
|
||||||
private val configuratorProvider: LockScreenConfiguratorProvider,
|
private val defaultConfiguration: LockScreenConfiguration,
|
||||||
) : VectorBaseFragment<FragmentPinBinding>() {
|
) : VectorBaseFragment<FragmentPinBinding>() {
|
||||||
|
|
||||||
private val fragmentArgs: PinArgs by args()
|
private val fragmentArgs: PinArgs by args()
|
||||||
|
@ -81,21 +82,17 @@ class PinFragment @Inject constructor(
|
||||||
vectorBaseActivity.finish()
|
vectorBaseActivity.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
createFragment.arguments = defaultConfiguration.copy(
|
||||||
configuratorProvider.updateDefaultConfiguration {
|
mode = LockScreenMode.CREATE,
|
||||||
copy(
|
title = getString(R.string.create_pin_title),
|
||||||
mode = LockScreenMode.CREATE,
|
needsNewCodeValidation = true,
|
||||||
title = getString(R.string.create_pin_title),
|
newCodeConfirmationTitle = getString(R.string.create_pin_confirm_title),
|
||||||
needsNewCodeValidation = true,
|
).asMavericksArgs()
|
||||||
newCodeConfirmationTitle = getString(R.string.create_pin_confirm_title),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
replaceFragment(R.id.pinFragmentContainer, createFragment)
|
replaceFragment(R.id.pinFragmentContainer, createFragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showAuthFragment() {
|
private fun showAuthFragment() {
|
||||||
val authFragment = LockScreenFragment()
|
val authFragment = LockScreenFragment()
|
||||||
val canUseBiometrics = vectorPreferences.useBiometricsToUnlock()
|
|
||||||
authFragment.onLeftButtonClickedListener = View.OnClickListener { displayForgotPinWarningDialog() }
|
authFragment.onLeftButtonClickedListener = View.OnClickListener { displayForgotPinWarningDialog() }
|
||||||
authFragment.lockScreenListener = object : LockScreenListener {
|
authFragment.lockScreenListener = object : LockScreenListener {
|
||||||
override fun onAuthenticationFailure(authMethod: AuthMethod) {
|
override fun onAuthenticationFailure(authMethod: AuthMethod) {
|
||||||
|
@ -133,18 +130,12 @@ class PinFragment @Inject constructor(
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
configuratorProvider.updateDefaultConfiguration {
|
authFragment.arguments = defaultConfiguration.copy(
|
||||||
copy(
|
mode = LockScreenMode.VERIFY,
|
||||||
mode = LockScreenMode.VERIFY,
|
title = getString(R.string.auth_pin_title),
|
||||||
title = getString(R.string.auth_pin_title),
|
leftButtonTitle = getString(R.string.auth_pin_forgot),
|
||||||
isStrongBiometricsEnabled = isStrongBiometricsEnabled && canUseBiometrics,
|
clearCodeOnError = true,
|
||||||
isWeakBiometricsEnabled = isWeakBiometricsEnabled && canUseBiometrics,
|
).asMavericksArgs()
|
||||||
isDeviceCredentialUnlockEnabled = isDeviceCredentialUnlockEnabled && canUseBiometrics,
|
|
||||||
autoStartBiometric = canUseBiometrics,
|
|
||||||
leftButtonTitle = getString(R.string.auth_pin_forgot),
|
|
||||||
clearCodeOnError = true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
replaceFragment(R.id.pinFragmentContainer, authFragment)
|
replaceFragment(R.id.pinFragmentContainer, authFragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,10 +31,12 @@ import androidx.biometric.BiometricPrompt
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration
|
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration
|
||||||
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguratorProvider
|
|
||||||
import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeyRepository
|
import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeyRepository
|
||||||
import im.vector.app.features.pin.lockscreen.ui.fallbackprompt.FallbackBiometricDialogFragment
|
import im.vector.app.features.pin.lockscreen.ui.fallbackprompt.FallbackBiometricDialogFragment
|
||||||
import im.vector.app.features.pin.lockscreen.utils.DevicePromptCheck
|
import im.vector.app.features.pin.lockscreen.utils.DevicePromptCheck
|
||||||
|
@ -54,22 +56,24 @@ import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
|
import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
|
||||||
import java.security.KeyStore
|
import java.security.KeyStore
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a helper to manage system authentication (biometric and other types) and the system key.
|
* This is a helper to manage system authentication (biometric and other types) and the system key.
|
||||||
*/
|
*/
|
||||||
class BiometricHelper @Inject constructor(
|
class BiometricHelper @AssistedInject constructor(
|
||||||
|
@Assisted private val configuration: LockScreenConfiguration,
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val lockScreenKeyRepository: LockScreenKeyRepository,
|
private val lockScreenKeyRepository: LockScreenKeyRepository,
|
||||||
private val configurationProvider: LockScreenConfiguratorProvider,
|
|
||||||
private val biometricManager: BiometricManager,
|
private val biometricManager: BiometricManager,
|
||||||
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
|
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
|
||||||
) {
|
) {
|
||||||
private var prompt: BiometricPrompt? = null
|
private var prompt: BiometricPrompt? = null
|
||||||
|
|
||||||
private val configuration: LockScreenConfiguration get() = configurationProvider.currentConfiguration
|
@AssistedFactory
|
||||||
|
interface BiometricHelperFactory {
|
||||||
|
fun create(configuration: LockScreenConfiguration): BiometricHelper
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if a weak biometric method (i.e.: some face or iris unlock implementations) can be used.
|
* Returns true if a weak biometric method (i.e.: some face or iris unlock implementations) can be used.
|
||||||
|
@ -174,16 +178,18 @@ class BiometricHelper @Inject constructor(
|
||||||
when (val exception = result.exceptionOrNull()) {
|
when (val exception = result.exceptionOrNull()) {
|
||||||
null -> result.getOrNull()?.let { emit(it) }
|
null -> result.getOrNull()?.let { emit(it) }
|
||||||
else -> {
|
else -> {
|
||||||
// Exception found, stop collecting, throw it and remove the prompt reference
|
// Exception found:
|
||||||
|
// 1. Stop collecting.
|
||||||
|
// 2. Remove the system key if we were creating it.
|
||||||
|
// 3. Throw the exception and remove the prompt reference
|
||||||
|
if (!checkSystemKeyExists) {
|
||||||
|
lockScreenKeyRepository.deleteSystemKey()
|
||||||
|
}
|
||||||
prompt = null
|
prompt = null
|
||||||
throw exception
|
throw exception
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Generates the system key on successful authentication
|
|
||||||
if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M) {
|
|
||||||
lockScreenKeyRepository.ensureSystemKey()
|
|
||||||
}
|
|
||||||
// Channel is closed, remove prompt reference
|
// Channel is closed, remove prompt reference
|
||||||
prompt = null
|
prompt = null
|
||||||
}
|
}
|
||||||
|
@ -213,11 +219,11 @@ class BiometricHelper @Inject constructor(
|
||||||
.setAllowedAuthenticators(authenticators)
|
.setAllowedAuthenticators(authenticators)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
return BiometricPrompt(activity, executor, callback).also {
|
return BiometricPrompt(activity, executor, callback).also { prompt ->
|
||||||
showFallbackFragmentIfNeeded(activity, channel.receiveAsFlow(), executor.asCoroutineDispatcher()) {
|
showFallbackFragmentIfNeeded(activity, channel.receiveAsFlow(), executor.asCoroutineDispatcher()) {
|
||||||
// For some reason this seems to be needed unless we want to receive a fragment transaction exception
|
// For some reason this seems to be needed unless we want to receive a fragment transaction exception
|
||||||
delay(1L)
|
delay(1L)
|
||||||
it.authenticate(promptInfo, cryptoObject)
|
prompt.authenticate(promptInfo, cryptoObject)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -253,11 +259,9 @@ class BiometricHelper @Inject constructor(
|
||||||
): BiometricPrompt.AuthenticationCallback = object : BiometricPrompt.AuthenticationCallback() {
|
): BiometricPrompt.AuthenticationCallback = object : BiometricPrompt.AuthenticationCallback() {
|
||||||
private val scope = CoroutineScope(coroutineContext)
|
private val scope = CoroutineScope(coroutineContext)
|
||||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||||
scope.launch {
|
// Error is a terminal event, should close both the Channel and the CoroutineScope to free resources.
|
||||||
// Error is a terminal event, should close both the Channel and the CoroutineScope to free resources.
|
channel.close(BiometricAuthError(errorCode, errString.toString()))
|
||||||
channel.close(BiometricAuthError(errorCode, errString.toString()))
|
scope.cancel()
|
||||||
scope.cancel()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAuthenticationFailed() {
|
override fun onAuthenticationFailed() {
|
||||||
|
@ -274,10 +278,8 @@ class BiometricHelper @Inject constructor(
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
scope.launch {
|
channel.close(IllegalStateException("System key was not valid after authentication."))
|
||||||
channel.close(IllegalStateException("System key was not valid after authentication."))
|
scope.cancel()
|
||||||
scope.cancel()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,9 +16,13 @@
|
||||||
|
|
||||||
package im.vector.app.features.pin.lockscreen.configuration
|
package im.vector.app.features.pin.lockscreen.configuration
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration to be used by the lockscreen feature.
|
* Configuration to be used by the lockscreen feature.
|
||||||
*/
|
*/
|
||||||
|
@Parcelize
|
||||||
data class LockScreenConfiguration(
|
data class LockScreenConfiguration(
|
||||||
/** Which mode should the UI display, [LockScreenMode.VERIFY] or [LockScreenMode.CREATE]. */
|
/** Which mode should the UI display, [LockScreenMode.VERIFY] or [LockScreenMode.CREATE]. */
|
||||||
val mode: LockScreenMode,
|
val mode: LockScreenMode,
|
||||||
|
@ -56,4 +60,4 @@ data class LockScreenConfiguration(
|
||||||
val biometricSubtitle: String? = null,
|
val biometricSubtitle: String? = null,
|
||||||
/** Text for the cancel button of the Biometric prompt dialog. Optional. */
|
/** Text for the cancel button of the Biometric prompt dialog. Optional. */
|
||||||
val biometricCancelButtonTitle: String? = null,
|
val biometricCancelButtonTitle: String? = null,
|
||||||
)
|
) : Parcelable
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2022 New Vector Ltd
|
|
||||||
*
|
|
||||||
* 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 im.vector.app.features.pin.lockscreen.configuration
|
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class used to hold both the [defaultConfiguration] and an updated version in [currentConfiguration].
|
|
||||||
*/
|
|
||||||
@Singleton
|
|
||||||
class LockScreenConfiguratorProvider @Inject constructor(
|
|
||||||
/** Default [LockScreenConfiguration], any derived configuration created using [updateDefaultConfiguration] will use this as a base. */
|
|
||||||
val defaultConfiguration: LockScreenConfiguration,
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val mutableConfigurationFlow = MutableStateFlow(defaultConfiguration)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A [Flow] that emits any changes in configuration.
|
|
||||||
*/
|
|
||||||
val configurationFlow: Flow<LockScreenConfiguration> = mutableConfigurationFlow
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current configuration to be read and used.
|
|
||||||
*/
|
|
||||||
val currentConfiguration get() = mutableConfigurationFlow.value
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies the changes in [block] to the [defaultConfiguration] to generate a new [currentConfiguration].
|
|
||||||
*/
|
|
||||||
fun updateDefaultConfiguration(block: LockScreenConfiguration.() -> LockScreenConfiguration) {
|
|
||||||
mutableConfigurationFlow.value = defaultConfiguration.block()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets the [currentConfiguration] to the [defaultConfiguration].
|
|
||||||
*/
|
|
||||||
fun reset() {
|
|
||||||
mutableConfigurationFlow.value = defaultConfiguration
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -20,13 +20,13 @@ import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.security.keystore.KeyPermanentlyInvalidatedException
|
import android.security.keystore.KeyPermanentlyInvalidatedException
|
||||||
import android.security.keystore.UserNotAuthenticatedException
|
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.biometric.BiometricPrompt
|
import androidx.biometric.BiometricPrompt
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.securestorage.SecretStoringUtils
|
import org.matrix.android.sdk.api.securestorage.SecretStoringUtils
|
||||||
import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
|
import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
|
||||||
import java.security.Key
|
import java.security.Key
|
||||||
|
@ -113,14 +113,8 @@ class KeyStoreCrypto @AssistedInject constructor(
|
||||||
fun hasValidKey(): Boolean {
|
fun hasValidKey(): Boolean {
|
||||||
val keyExists = hasKey()
|
val keyExists = hasKey()
|
||||||
return if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M && keyExists) {
|
return if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M && keyExists) {
|
||||||
try {
|
val initializedKey = tryOrNull("Error validating lockscreen system key.") { ensureKey() }
|
||||||
ensureKey()
|
initializedKey != null
|
||||||
true
|
|
||||||
} catch (e: KeyPermanentlyInvalidatedException) {
|
|
||||||
false
|
|
||||||
} catch (e: UserNotAuthenticatedException) {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
keyExists
|
keyExists
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,8 +16,10 @@
|
||||||
|
|
||||||
package im.vector.app.features.pin.lockscreen.di
|
package im.vector.app.features.pin.lockscreen.di
|
||||||
|
|
||||||
|
import android.app.KeyguardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
|
@ -83,6 +85,9 @@ object LockScreenModule {
|
||||||
SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider),
|
SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider),
|
||||||
buildVersionSdkIntProvider,
|
buildVersionSdkIntProvider,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun provideKeyguardManager(context: Context): KeyguardManager = context.getSystemService()!!
|
||||||
}
|
}
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
|
|
@ -22,4 +22,5 @@ import im.vector.app.core.platform.VectorViewModelAction
|
||||||
sealed class LockScreenAction : VectorViewModelAction {
|
sealed class LockScreenAction : VectorViewModelAction {
|
||||||
data class PinCodeEntered(val value: String) : LockScreenAction()
|
data class PinCodeEntered(val value: String) : LockScreenAction()
|
||||||
data class ShowBiometricPrompt(val callingActivity: FragmentActivity) : LockScreenAction()
|
data class ShowBiometricPrompt(val callingActivity: FragmentActivity) : LockScreenAction()
|
||||||
|
object OnUIReady : LockScreenAction()
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,6 @@ import android.view.ViewGroup
|
||||||
import android.view.animation.AnimationUtils
|
import android.view.animation.AnimationUtils
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
@ -55,22 +54,7 @@ class LockScreenFragment : VectorBaseFragment<FragmentLockScreenBinding>() {
|
||||||
handleEvent(it)
|
handleEvent(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
withState(viewModel) { state ->
|
viewModel.handle(LockScreenAction.OnUIReady)
|
||||||
if (state.lockScreenConfiguration.mode == LockScreenMode.CREATE) return@withState
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launchWhenResumed {
|
|
||||||
if (state.canUseBiometricAuth && state.isBiometricKeyInvalidated) {
|
|
||||||
lockScreenListener?.onBiometricKeyInvalidated()
|
|
||||||
} else if (state.showBiometricPromptAutomatically) {
|
|
||||||
showBiometricPrompt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
viewModel.reset()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun invalidate() = withState(viewModel) { state ->
|
override fun invalidate() = withState(viewModel) { state ->
|
||||||
|
@ -83,6 +67,7 @@ class LockScreenFragment : VectorBaseFragment<FragmentLockScreenBinding>() {
|
||||||
setupTitleView(views.titleTextView, false, state.lockScreenConfiguration)
|
setupTitleView(views.titleTextView, false, state.lockScreenConfiguration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderDeleteOrFingerprintButtons(views, views.codeView.enteredDigits)
|
renderDeleteOrFingerprintButtons(views, views.codeView.enteredDigits)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,6 +108,8 @@ class LockScreenFragment : VectorBaseFragment<FragmentLockScreenBinding>() {
|
||||||
is LockScreenViewEvent.AuthSuccessful -> lockScreenListener?.onAuthenticationSuccess(viewEvent.method)
|
is LockScreenViewEvent.AuthSuccessful -> lockScreenListener?.onAuthenticationSuccess(viewEvent.method)
|
||||||
is LockScreenViewEvent.AuthFailure -> onAuthFailure(viewEvent.method)
|
is LockScreenViewEvent.AuthFailure -> onAuthFailure(viewEvent.method)
|
||||||
is LockScreenViewEvent.AuthError -> onAuthError(viewEvent.method, viewEvent.throwable)
|
is LockScreenViewEvent.AuthError -> onAuthError(viewEvent.method, viewEvent.throwable)
|
||||||
|
is LockScreenViewEvent.ShowBiometricKeyInvalidatedMessage -> lockScreenListener?.onBiometricKeyInvalidated()
|
||||||
|
is LockScreenViewEvent.ShowBiometricPromptAutomatically -> showBiometricPrompt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,4 +24,6 @@ sealed class LockScreenViewEvent : VectorViewEvents {
|
||||||
data class AuthSuccessful(val method: AuthMethod) : LockScreenViewEvent()
|
data class AuthSuccessful(val method: AuthMethod) : LockScreenViewEvent()
|
||||||
data class AuthFailure(val method: AuthMethod) : LockScreenViewEvent()
|
data class AuthFailure(val method: AuthMethod) : LockScreenViewEvent()
|
||||||
data class AuthError(val method: AuthMethod, val throwable: Throwable) : LockScreenViewEvent()
|
data class AuthError(val method: AuthMethod, val throwable: Throwable) : LockScreenViewEvent()
|
||||||
|
object ShowBiometricKeyInvalidatedMessage : LockScreenViewEvent()
|
||||||
|
object ShowBiometricPromptAutomatically : LockScreenViewEvent()
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,12 +17,11 @@
|
||||||
package im.vector.app.features.pin.lockscreen.ui
|
package im.vector.app.features.pin.lockscreen.ui
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.KeyguardManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.security.keystore.KeyPermanentlyInvalidatedException
|
import android.security.keystore.KeyPermanentlyInvalidatedException
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.airbnb.mvrx.MavericksViewModelFactory
|
import com.airbnb.mvrx.MavericksViewModelFactory
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
|
||||||
import com.airbnb.mvrx.withState
|
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
@ -31,26 +30,29 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
import im.vector.app.features.pin.lockscreen.biometrics.BiometricAuthError
|
import im.vector.app.features.pin.lockscreen.biometrics.BiometricAuthError
|
||||||
import im.vector.app.features.pin.lockscreen.biometrics.BiometricHelper
|
import im.vector.app.features.pin.lockscreen.biometrics.BiometricHelper
|
||||||
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration
|
|
||||||
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguratorProvider
|
|
||||||
import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode
|
import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode
|
||||||
import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeysMigrator
|
import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeysMigrator
|
||||||
import im.vector.app.features.pin.lockscreen.pincode.PinCodeHelper
|
import im.vector.app.features.pin.lockscreen.pincode.PinCodeHelper
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.emitAll
|
import kotlinx.coroutines.flow.emitAll
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
|
import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
class LockScreenViewModel @AssistedInject constructor(
|
class LockScreenViewModel @AssistedInject constructor(
|
||||||
@Assisted val initialState: LockScreenViewState,
|
@Assisted val initialState: LockScreenViewState,
|
||||||
private val pinCodeHelper: PinCodeHelper,
|
private val pinCodeHelper: PinCodeHelper,
|
||||||
private val biometricHelper: BiometricHelper,
|
biometricHelperFactory: BiometricHelper.BiometricHelperFactory,
|
||||||
private val lockScreenKeysMigrator: LockScreenKeysMigrator,
|
private val lockScreenKeysMigrator: LockScreenKeysMigrator,
|
||||||
private val configuratorProvider: LockScreenConfiguratorProvider,
|
private val versionProvider: BuildVersionSdkIntProvider,
|
||||||
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
|
private val keyguardManager: KeyguardManager,
|
||||||
) : VectorViewModel<LockScreenViewState, LockScreenAction, LockScreenViewEvent>(initialState) {
|
) : VectorViewModel<LockScreenViewState, LockScreenAction, LockScreenViewEvent>(initialState) {
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
|
@ -58,27 +60,9 @@ class LockScreenViewModel @AssistedInject constructor(
|
||||||
override fun create(initialState: LockScreenViewState): LockScreenViewModel
|
override fun create(initialState: LockScreenViewState): LockScreenViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object : MavericksViewModelFactory<LockScreenViewModel, LockScreenViewState> by hiltMavericksViewModelFactory() {
|
companion object : MavericksViewModelFactory<LockScreenViewModel, LockScreenViewState> by hiltMavericksViewModelFactory()
|
||||||
|
|
||||||
override fun initialState(viewModelContext: ViewModelContext): LockScreenViewState {
|
private val biometricHelper = biometricHelperFactory.create(initialState.lockScreenConfiguration)
|
||||||
return LockScreenViewState(
|
|
||||||
lockScreenConfiguration = DUMMY_CONFIGURATION,
|
|
||||||
canUseBiometricAuth = false,
|
|
||||||
showBiometricPromptAutomatically = false,
|
|
||||||
pinCodeState = PinCodeState.Idle,
|
|
||||||
isBiometricKeyInvalidated = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val DUMMY_CONFIGURATION = LockScreenConfiguration(
|
|
||||||
mode = LockScreenMode.VERIFY,
|
|
||||||
pinCodeLength = 4,
|
|
||||||
isStrongBiometricsEnabled = false,
|
|
||||||
isDeviceCredentialUnlockEnabled = false,
|
|
||||||
isWeakBiometricsEnabled = false,
|
|
||||||
needsNewCodeValidation = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var firstEnteredCode: String? = null
|
private var firstEnteredCode: String? = null
|
||||||
|
|
||||||
|
@ -86,18 +70,37 @@ class LockScreenViewModel @AssistedInject constructor(
|
||||||
private var isSystemAuthTemporarilyDisabledByBiometricPrompt = false
|
private var isSystemAuthTemporarilyDisabledByBiometricPrompt = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// We need this to run synchronously before we start reading the configurations
|
viewModelScope.launch {
|
||||||
runBlocking { lockScreenKeysMigrator.migrateIfNeeded() }
|
// Wait until the keyguard is unlocked before performing migrations, it might cause crashes otherwise on Android 12 and 12L
|
||||||
|
waitUntilKeyguardIsUnlocked()
|
||||||
|
// Migrate pin code / system keys if needed
|
||||||
|
lockScreenKeysMigrator.migrateIfNeeded()
|
||||||
|
// Update initial state with biometric info
|
||||||
|
updateStateWithBiometricInfo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
configuratorProvider.configurationFlow
|
private fun observeStateChanges() {
|
||||||
.onEach { updateConfiguration(it) }
|
// The first time the state allows it, show the biometric prompt
|
||||||
.launchIn(viewModelScope)
|
viewModelScope.launch {
|
||||||
|
if (stateFlow.firstOrNull { it.showBiometricPromptAutomatically } != null) {
|
||||||
|
_viewEvents.post(LockScreenViewEvent.ShowBiometricPromptAutomatically)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The first time the state allows it, react to biometric key being invalidated
|
||||||
|
viewModelScope.launch {
|
||||||
|
if (stateFlow.firstOrNull { it.isBiometricKeyInvalidated } != null) {
|
||||||
|
onBiometricKeyInvalidated()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handle(action: LockScreenAction) {
|
override fun handle(action: LockScreenAction) {
|
||||||
when (action) {
|
when (action) {
|
||||||
is LockScreenAction.PinCodeEntered -> onPinCodeEntered(action.value)
|
is LockScreenAction.PinCodeEntered -> onPinCodeEntered(action.value)
|
||||||
is LockScreenAction.ShowBiometricPrompt -> showBiometricPrompt(action.callingActivity)
|
is LockScreenAction.ShowBiometricPrompt -> showBiometricPrompt(action.callingActivity)
|
||||||
|
is LockScreenAction.OnUIReady -> observeStateChanges()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,13 +144,18 @@ class LockScreenViewModel @AssistedInject constructor(
|
||||||
private fun showBiometricPrompt(activity: FragmentActivity) = flow {
|
private fun showBiometricPrompt(activity: FragmentActivity) = flow {
|
||||||
emitAll(biometricHelper.authenticate(activity))
|
emitAll(biometricHelper.authenticate(activity))
|
||||||
}.catch { error ->
|
}.catch { error ->
|
||||||
if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M && error is KeyPermanentlyInvalidatedException) {
|
when {
|
||||||
removeBiometricAuthentication()
|
versionProvider.get() >= Build.VERSION_CODES.M && error is KeyPermanentlyInvalidatedException -> {
|
||||||
} else if (error is BiometricAuthError && error.isAuthDisabledError) {
|
onBiometricKeyInvalidated()
|
||||||
isSystemAuthTemporarilyDisabledByBiometricPrompt = true
|
}
|
||||||
updateStateWithBiometricInfo()
|
else -> {
|
||||||
|
if (error is BiometricAuthError && error.isAuthDisabledError) {
|
||||||
|
isSystemAuthTemporarilyDisabledByBiometricPrompt = true
|
||||||
|
updateStateWithBiometricInfo()
|
||||||
|
}
|
||||||
|
_viewEvents.post(LockScreenViewEvent.AuthError(AuthMethod.BIOMETRICS, error))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_viewEvents.post(LockScreenViewEvent.AuthError(AuthMethod.BIOMETRICS, error))
|
|
||||||
}.onEach { success ->
|
}.onEach { success ->
|
||||||
_viewEvents.post(
|
_viewEvents.post(
|
||||||
if (success) LockScreenViewEvent.AuthSuccessful(AuthMethod.BIOMETRICS)
|
if (success) LockScreenViewEvent.AuthSuccessful(AuthMethod.BIOMETRICS)
|
||||||
|
@ -155,24 +163,22 @@ class LockScreenViewModel @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
}.launchIn(viewModelScope)
|
}.launchIn(viewModelScope)
|
||||||
|
|
||||||
fun reset() {
|
private suspend fun onBiometricKeyInvalidated() {
|
||||||
configuratorProvider.reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun removeBiometricAuthentication() {
|
|
||||||
biometricHelper.disableAuthentication()
|
biometricHelper.disableAuthentication()
|
||||||
updateStateWithBiometricInfo()
|
updateStateWithBiometricInfo()
|
||||||
|
_viewEvents.post(LockScreenViewEvent.ShowBiometricKeyInvalidatedMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateStateWithBiometricInfo() {
|
@SuppressLint("NewApi")
|
||||||
val configuration = withState(this) { it.lockScreenConfiguration }
|
private suspend fun updateStateWithBiometricInfo() {
|
||||||
val canUseBiometricAuth = configuration.mode == LockScreenMode.VERIFY &&
|
// This is a terrible hack, but I found no other way to ensure this would be called only after the device is considered unlocked on Android 12+
|
||||||
|
waitUntilKeyguardIsUnlocked()
|
||||||
|
setState {
|
||||||
|
val isBiometricKeyInvalidated = biometricHelper.hasSystemKey && !biometricHelper.isSystemKeyValid
|
||||||
|
val canUseBiometricAuth = lockScreenConfiguration.mode == LockScreenMode.VERIFY &&
|
||||||
!isSystemAuthTemporarilyDisabledByBiometricPrompt &&
|
!isSystemAuthTemporarilyDisabledByBiometricPrompt &&
|
||||||
biometricHelper.isSystemAuthEnabledAndValid
|
biometricHelper.isSystemAuthEnabledAndValid
|
||||||
val isBiometricKeyInvalidated = biometricHelper.hasSystemKey && !biometricHelper.isSystemKeyValid
|
val showBiometricPromptAutomatically = canUseBiometricAuth && lockScreenConfiguration.autoStartBiometric
|
||||||
val showBiometricPromptAutomatically = canUseBiometricAuth &&
|
|
||||||
configuration.autoStartBiometric
|
|
||||||
setState {
|
|
||||||
copy(
|
copy(
|
||||||
canUseBiometricAuth = canUseBiometricAuth,
|
canUseBiometricAuth = canUseBiometricAuth,
|
||||||
showBiometricPromptAutomatically = showBiometricPromptAutomatically,
|
showBiometricPromptAutomatically = showBiometricPromptAutomatically,
|
||||||
|
@ -181,8 +187,18 @@ class LockScreenViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateConfiguration(configuration: LockScreenConfiguration) {
|
/**
|
||||||
setState { copy(lockScreenConfiguration = configuration) }
|
* Wait until the device is unlocked. There seems to be a behavior change on Android 12 that makes [KeyguardManager.isDeviceLocked] return `false` even
|
||||||
updateStateWithBiometricInfo()
|
* after an Activity's `onResume` method. If we mix that with the system keys needing the device to be unlocked before they're used, we get crashes.
|
||||||
|
* See issue [#6768](https://github.com/vector-im/element-android/issues/6768).
|
||||||
|
*/
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
private suspend fun waitUntilKeyguardIsUnlocked() {
|
||||||
|
if (versionProvider.get() < Build.VERSION_CODES.S) return
|
||||||
|
withTimeoutOrNull(5.seconds) {
|
||||||
|
while (keyguardManager.isDeviceLocked) {
|
||||||
|
delay(50.milliseconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,11 @@ data class LockScreenViewState(
|
||||||
val showBiometricPromptAutomatically: Boolean,
|
val showBiometricPromptAutomatically: Boolean,
|
||||||
val pinCodeState: PinCodeState,
|
val pinCodeState: PinCodeState,
|
||||||
val isBiometricKeyInvalidated: Boolean,
|
val isBiometricKeyInvalidated: Boolean,
|
||||||
) : MavericksState
|
) : MavericksState {
|
||||||
|
constructor(lockScreenConfiguration: LockScreenConfiguration) : this(
|
||||||
|
lockScreenConfiguration, false, false, PinCodeState.Idle, false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
sealed class PinCodeState {
|
sealed class PinCodeState {
|
||||||
object Idle : PinCodeState()
|
object Idle : PinCodeState()
|
||||||
|
|
|
@ -28,6 +28,8 @@ import im.vector.app.features.notifications.NotificationDrawerManager
|
||||||
import im.vector.app.features.pin.PinCodeStore
|
import im.vector.app.features.pin.PinCodeStore
|
||||||
import im.vector.app.features.pin.PinMode
|
import im.vector.app.features.pin.PinMode
|
||||||
import im.vector.app.features.pin.lockscreen.biometrics.BiometricHelper
|
import im.vector.app.features.pin.lockscreen.biometrics.BiometricHelper
|
||||||
|
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration
|
||||||
|
import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
|
@ -38,12 +40,15 @@ class VectorSettingsPinFragment @Inject constructor(
|
||||||
private val pinCodeStore: PinCodeStore,
|
private val pinCodeStore: PinCodeStore,
|
||||||
private val navigator: Navigator,
|
private val navigator: Navigator,
|
||||||
private val notificationDrawerManager: NotificationDrawerManager,
|
private val notificationDrawerManager: NotificationDrawerManager,
|
||||||
private val biometricHelper: BiometricHelper,
|
biometricHelperFactory: BiometricHelper.BiometricHelperFactory,
|
||||||
|
defaultLockScreenConfiguration: LockScreenConfiguration,
|
||||||
) : VectorSettingsBaseFragment() {
|
) : VectorSettingsBaseFragment() {
|
||||||
|
|
||||||
override var titleRes = R.string.settings_security_application_protection_screen_title
|
override var titleRes = R.string.settings_security_application_protection_screen_title
|
||||||
override val preferenceXmlRes = R.xml.vector_settings_pin
|
override val preferenceXmlRes = R.xml.vector_settings_pin
|
||||||
|
|
||||||
|
private val biometricHelper = biometricHelperFactory.create(defaultLockScreenConfiguration.copy(mode = LockScreenMode.CREATE))
|
||||||
|
|
||||||
private val usePinCodePref by lazy {
|
private val usePinCodePref by lazy {
|
||||||
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_SECURITY_USE_PIN_CODE_FLAG)!!
|
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_SECURITY_USE_PIN_CODE_FLAG)!!
|
||||||
}
|
}
|
||||||
|
@ -102,9 +107,10 @@ class VectorSettingsPinFragment @Inject constructor(
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
showEnableBiometricErrorMessage()
|
showEnableBiometricErrorMessage()
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBiometricPrefState(isPinCodeChecked = usePinCodePref.isChecked)
|
updateBiometricPrefState(isPinCodeChecked = usePinCodePref.isChecked)
|
||||||
}
|
}
|
||||||
false
|
true
|
||||||
} else {
|
} else {
|
||||||
disableBiometricAuthentication()
|
disableBiometricAuthentication()
|
||||||
true
|
true
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package im.vector.app.features.pin.lockscreen.fragment
|
package im.vector.app.features.pin.lockscreen.fragment
|
||||||
|
|
||||||
|
import android.app.KeyguardManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.security.keystore.KeyPermanentlyInvalidatedException
|
import android.security.keystore.KeyPermanentlyInvalidatedException
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
@ -23,7 +24,6 @@ import com.airbnb.mvrx.test.MvRxTestRule
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import im.vector.app.features.pin.lockscreen.biometrics.BiometricHelper
|
import im.vector.app.features.pin.lockscreen.biometrics.BiometricHelper
|
||||||
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration
|
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration
|
||||||
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguratorProvider
|
|
||||||
import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode
|
import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode
|
||||||
import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeysMigrator
|
import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeysMigrator
|
||||||
import im.vector.app.features.pin.lockscreen.pincode.PinCodeHelper
|
import im.vector.app.features.pin.lockscreen.pincode.PinCodeHelper
|
||||||
|
@ -42,6 +42,7 @@ import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.amshove.kluent.shouldBeEqualTo
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
import org.amshove.kluent.shouldBeFalse
|
import org.amshove.kluent.shouldBeFalse
|
||||||
|
@ -57,7 +58,15 @@ class LockScreenViewModelTests {
|
||||||
|
|
||||||
private val pinCodeHelper = mockk<PinCodeHelper>(relaxed = true)
|
private val pinCodeHelper = mockk<PinCodeHelper>(relaxed = true)
|
||||||
private val biometricHelper = mockk<BiometricHelper>(relaxed = true)
|
private val biometricHelper = mockk<BiometricHelper>(relaxed = true)
|
||||||
|
private val biometricHelperFactory = object : BiometricHelper.BiometricHelperFactory {
|
||||||
|
override fun create(configuration: LockScreenConfiguration): BiometricHelper {
|
||||||
|
return biometricHelper
|
||||||
|
}
|
||||||
|
}
|
||||||
private val keysMigrator = mockk<LockScreenKeysMigrator>(relaxed = true)
|
private val keysMigrator = mockk<LockScreenKeysMigrator>(relaxed = true)
|
||||||
|
private val keyguardManager = mockk<KeyguardManager>(relaxed = true) {
|
||||||
|
every { isDeviceLocked } returns false
|
||||||
|
}
|
||||||
private val versionProvider = TestBuildVersionSdkIntProvider()
|
private val versionProvider = TestBuildVersionSdkIntProvider()
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
|
@ -68,19 +77,36 @@ class LockScreenViewModelTests {
|
||||||
@Test
|
@Test
|
||||||
fun `init migrates old keys to new ones if needed`() {
|
fun `init migrates old keys to new ones if needed`() {
|
||||||
val initialState = createViewState()
|
val initialState = createViewState()
|
||||||
val configProvider = LockScreenConfiguratorProvider(createDefaultConfiguration())
|
LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
|
||||||
LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
|
|
||||||
|
|
||||||
coVerify { keysMigrator.migrateIfNeeded() }
|
coVerify { keysMigrator.migrateIfNeeded() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `init updates the initial state with biometric info`() = runTest {
|
||||||
|
every { biometricHelper.isSystemAuthEnabledAndValid } returns true
|
||||||
|
val initialState = createViewState()
|
||||||
|
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
|
||||||
|
advanceUntilIdle()
|
||||||
|
val newState = viewModel.awaitState()
|
||||||
|
newState shouldNotBeEqualTo initialState
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Updating the initial state with biometric info waits until device is unlocked on Android 12+`() = runTest {
|
||||||
|
val initialState = createViewState()
|
||||||
|
versionProvider.value = Build.VERSION_CODES.S
|
||||||
|
LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
|
||||||
|
advanceUntilIdle()
|
||||||
|
verify { keyguardManager.isDeviceLocked }
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when ViewModel is instantiated initialState is updated with biometric info`() {
|
fun `when ViewModel is instantiated initialState is updated with biometric info`() {
|
||||||
val initialState = createViewState()
|
val initialState = createViewState()
|
||||||
val configProvider = LockScreenConfiguratorProvider(createDefaultConfiguration())
|
|
||||||
// This should set canUseBiometricAuth to true
|
// This should set canUseBiometricAuth to true
|
||||||
every { biometricHelper.isSystemAuthEnabledAndValid } returns true
|
every { biometricHelper.isSystemAuthEnabledAndValid } returns true
|
||||||
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
|
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
|
||||||
val newState = withState(viewModel) { it }
|
val newState = withState(viewModel) { it }
|
||||||
initialState shouldNotBeEqualTo newState
|
initialState shouldNotBeEqualTo newState
|
||||||
}
|
}
|
||||||
|
@ -88,8 +114,7 @@ class LockScreenViewModelTests {
|
||||||
@Test
|
@Test
|
||||||
fun `when onPinCodeEntered is called in VERIFY mode, the code is verified and the result is emitted as a ViewEvent`() = runTest {
|
fun `when onPinCodeEntered is called in VERIFY mode, the code is verified and the result is emitted as a ViewEvent`() = runTest {
|
||||||
val initialState = createViewState()
|
val initialState = createViewState()
|
||||||
val configProvider = LockScreenConfiguratorProvider(createDefaultConfiguration())
|
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
|
||||||
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
|
|
||||||
coEvery { pinCodeHelper.verifyPinCode(any()) } returns true
|
coEvery { pinCodeHelper.verifyPinCode(any()) } returns true
|
||||||
|
|
||||||
val events = viewModel.test().viewEvents
|
val events = viewModel.test().viewEvents
|
||||||
|
@ -113,8 +138,7 @@ class LockScreenViewModelTests {
|
||||||
fun `when onPinCodeEntered is called in CREATE mode with no confirmation needed it creates the pin code`() = runTest {
|
fun `when onPinCodeEntered is called in CREATE mode with no confirmation needed it creates the pin code`() = runTest {
|
||||||
val configuration = createDefaultConfiguration(mode = LockScreenMode.CREATE, needsNewCodeValidation = false)
|
val configuration = createDefaultConfiguration(mode = LockScreenMode.CREATE, needsNewCodeValidation = false)
|
||||||
val initialState = createViewState(lockScreenConfiguration = configuration)
|
val initialState = createViewState(lockScreenConfiguration = configuration)
|
||||||
val configProvider = LockScreenConfiguratorProvider(configuration)
|
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
|
||||||
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
|
|
||||||
|
|
||||||
val events = viewModel.test().viewEvents
|
val events = viewModel.test().viewEvents
|
||||||
events.assertNoValues()
|
events.assertNoValues()
|
||||||
|
@ -128,9 +152,8 @@ class LockScreenViewModelTests {
|
||||||
@Test
|
@Test
|
||||||
fun `when onPinCodeEntered is called twice in CREATE mode with confirmation needed it verifies and creates the pin code`() = runTest {
|
fun `when onPinCodeEntered is called twice in CREATE mode with confirmation needed it verifies and creates the pin code`() = runTest {
|
||||||
val configuration = createDefaultConfiguration(mode = LockScreenMode.CREATE, needsNewCodeValidation = true)
|
val configuration = createDefaultConfiguration(mode = LockScreenMode.CREATE, needsNewCodeValidation = true)
|
||||||
val configProvider = LockScreenConfiguratorProvider(configuration)
|
|
||||||
val initialState = createViewState(lockScreenConfiguration = configuration)
|
val initialState = createViewState(lockScreenConfiguration = configuration)
|
||||||
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
|
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
|
||||||
|
|
||||||
val events = viewModel.test().viewEvents
|
val events = viewModel.test().viewEvents
|
||||||
events.assertNoValues()
|
events.assertNoValues()
|
||||||
|
@ -149,8 +172,7 @@ class LockScreenViewModelTests {
|
||||||
fun `when onPinCodeEntered is called in CREATE mode with incorrect confirmation it clears the pin code`() = runTest {
|
fun `when onPinCodeEntered is called in CREATE mode with incorrect confirmation it clears the pin code`() = runTest {
|
||||||
val configuration = createDefaultConfiguration(mode = LockScreenMode.CREATE, needsNewCodeValidation = true)
|
val configuration = createDefaultConfiguration(mode = LockScreenMode.CREATE, needsNewCodeValidation = true)
|
||||||
val initialState = createViewState(lockScreenConfiguration = configuration)
|
val initialState = createViewState(lockScreenConfiguration = configuration)
|
||||||
val configProvider = LockScreenConfiguratorProvider(configuration)
|
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
|
||||||
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
|
|
||||||
|
|
||||||
val events = viewModel.test().viewEvents
|
val events = viewModel.test().viewEvents
|
||||||
events.assertNoValues()
|
events.assertNoValues()
|
||||||
|
@ -170,8 +192,7 @@ class LockScreenViewModelTests {
|
||||||
@Test
|
@Test
|
||||||
fun `onPinCodeEntered handles exceptions`() = runTest {
|
fun `onPinCodeEntered handles exceptions`() = runTest {
|
||||||
val initialState = createViewState()
|
val initialState = createViewState()
|
||||||
val configProvider = LockScreenConfiguratorProvider(createDefaultConfiguration())
|
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
|
||||||
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
|
|
||||||
val exception = IllegalStateException("Something went wrong")
|
val exception = IllegalStateException("Something went wrong")
|
||||||
coEvery { pinCodeHelper.verifyPinCode(any()) } throws exception
|
coEvery { pinCodeHelper.verifyPinCode(any()) } throws exception
|
||||||
|
|
||||||
|
@ -187,39 +208,34 @@ class LockScreenViewModelTests {
|
||||||
fun `when showBiometricPrompt catches a KeyPermanentlyInvalidatedException it disables biometric authentication`() = runTest {
|
fun `when showBiometricPrompt catches a KeyPermanentlyInvalidatedException it disables biometric authentication`() = runTest {
|
||||||
versionProvider.value = Build.VERSION_CODES.M
|
versionProvider.value = Build.VERSION_CODES.M
|
||||||
|
|
||||||
every { biometricHelper.isSystemAuthEnabledAndValid } returns true
|
every { biometricHelper.isSystemKeyValid } returns false
|
||||||
every { biometricHelper.isSystemKeyValid } returns true
|
|
||||||
val exception = KeyPermanentlyInvalidatedException()
|
val exception = KeyPermanentlyInvalidatedException()
|
||||||
coEvery { biometricHelper.authenticate(any<FragmentActivity>()) } throws exception
|
coEvery { biometricHelper.authenticate(any<FragmentActivity>()) } throws exception
|
||||||
coEvery { biometricHelper.disableAuthentication() } coAnswers {
|
|
||||||
every { biometricHelper.isSystemAuthEnabledAndValid } returns false
|
|
||||||
}
|
|
||||||
val configuration = createDefaultConfiguration(mode = LockScreenMode.VERIFY, needsNewCodeValidation = true, isBiometricsEnabled = true)
|
val configuration = createDefaultConfiguration(mode = LockScreenMode.VERIFY, needsNewCodeValidation = true, isBiometricsEnabled = true)
|
||||||
val configProvider = LockScreenConfiguratorProvider(configuration)
|
|
||||||
val initialState = createViewState(
|
val initialState = createViewState(
|
||||||
canUseBiometricAuth = true,
|
canUseBiometricAuth = true,
|
||||||
isBiometricKeyInvalidated = false,
|
isBiometricKeyInvalidated = false,
|
||||||
lockScreenConfiguration = configuration
|
lockScreenConfiguration = configuration
|
||||||
)
|
)
|
||||||
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
|
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
|
||||||
|
|
||||||
val events = viewModel.test().viewEvents
|
val events = viewModel.test().viewEvents
|
||||||
events.assertNoValues()
|
events.assertNoValues()
|
||||||
|
|
||||||
viewModel.handle(LockScreenAction.ShowBiometricPrompt(mockk()))
|
viewModel.handle(LockScreenAction.ShowBiometricPrompt(mockk()))
|
||||||
|
|
||||||
events.assertValues(LockScreenViewEvent.AuthError(AuthMethod.BIOMETRICS, exception))
|
events.assertValues(LockScreenViewEvent.ShowBiometricKeyInvalidatedMessage)
|
||||||
verify { biometricHelper.disableAuthentication() }
|
verify { biometricHelper.disableAuthentication() }
|
||||||
|
|
||||||
// System key was deleted, biometric auth should be disabled
|
// System key was deleted, biometric auth should be disabled
|
||||||
|
every { biometricHelper.isSystemAuthEnabledAndValid } returns false
|
||||||
val newState = viewModel.awaitState()
|
val newState = viewModel.awaitState()
|
||||||
newState.canUseBiometricAuth.shouldBeFalse()
|
newState.canUseBiometricAuth.shouldBeFalse()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when showBiometricPrompt receives an event it propagates it as a ViewEvent`() = runTest {
|
fun `when showBiometricPrompt receives an event it propagates it as a ViewEvent`() = runTest {
|
||||||
val configProvider = LockScreenConfiguratorProvider(createDefaultConfiguration())
|
val viewModel = LockScreenViewModel(createViewState(), pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
|
||||||
val viewModel = LockScreenViewModel(createViewState(), pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
|
|
||||||
coEvery { biometricHelper.authenticate(any<FragmentActivity>()) } returns flowOf(false, true)
|
coEvery { biometricHelper.authenticate(any<FragmentActivity>()) } returns flowOf(false, true)
|
||||||
|
|
||||||
val events = viewModel.test().viewEvents
|
val events = viewModel.test().viewEvents
|
||||||
|
@ -232,8 +248,7 @@ class LockScreenViewModelTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `showBiometricPrompt handles exceptions`() = runTest {
|
fun `showBiometricPrompt handles exceptions`() = runTest {
|
||||||
val configProvider = LockScreenConfiguratorProvider(createDefaultConfiguration())
|
val viewModel = LockScreenViewModel(createViewState(), pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
|
||||||
val viewModel = LockScreenViewModel(createViewState(), pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
|
|
||||||
val exception = IllegalStateException("Something went wrong")
|
val exception = IllegalStateException("Something went wrong")
|
||||||
coEvery { biometricHelper.authenticate(any<FragmentActivity>()) } throws exception
|
coEvery { biometricHelper.authenticate(any<FragmentActivity>()) } throws exception
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue