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
					
				
							
								
								
									
										1
									
								
								changelog.d/6768.bugfix
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								changelog.d/6768.bugfix
									
									
									
									
									
										Normal file
									
								
							| @ -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 { |  | ||||||
|             copy( |  | ||||||
|                 mode = LockScreenMode.CREATE, |                 mode = LockScreenMode.CREATE, | ||||||
|                 title = getString(R.string.create_pin_title), |                 title = getString(R.string.create_pin_title), | ||||||
|                 needsNewCodeValidation = true, |                 needsNewCodeValidation = true, | ||||||
|                 newCodeConfirmationTitle = getString(R.string.create_pin_confirm_title), |                 newCodeConfirmationTitle = getString(R.string.create_pin_confirm_title), | ||||||
|             ) |         ).asMavericksArgs() | ||||||
|         } |  | ||||||
|         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), | ||||||
|                     isStrongBiometricsEnabled = isStrongBiometricsEnabled && canUseBiometrics, |  | ||||||
|                     isWeakBiometricsEnabled = isWeakBiometricsEnabled && canUseBiometrics, |  | ||||||
|                     isDeviceCredentialUnlockEnabled = isDeviceCredentialUnlockEnabled && canUseBiometrics, |  | ||||||
|                     autoStartBiometric = canUseBiometrics, |  | ||||||
|                 leftButtonTitle = getString(R.string.auth_pin_forgot), |                 leftButtonTitle = getString(R.string.auth_pin_forgot), | ||||||
|                 clearCodeOnError = true, |                 clearCodeOnError = true, | ||||||
|             ) |         ).asMavericksArgs() | ||||||
|         } |  | ||||||
|         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,12 +259,10 @@ 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() { | ||||||
|             scope.launch { channel.send(false) } |             scope.launch { channel.send(false) } | ||||||
| @ -274,12 +278,10 @@ 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() | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         private fun isCipherValid(cipher: Cipher?): Boolean { |         private fun isCipherValid(cipher: Cipher?): Boolean { | ||||||
|             if (cipher == null) return false |             if (cipher == null) return false | ||||||
|  | |||||||
| @ -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() | ||||||
|  |             } | ||||||
|  |             else -> { | ||||||
|  |                 if (error is BiometricAuthError && error.isAuthDisabledError) { | ||||||
|                     isSystemAuthTemporarilyDisabledByBiometricPrompt = true |                     isSystemAuthTemporarilyDisabledByBiometricPrompt = true | ||||||
|                     updateStateWithBiometricInfo() |                     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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user