diff --git a/app/src/main/kotlin/io/element/android/x/MainActivity.kt b/app/src/main/kotlin/io/element/android/x/MainActivity.kt
index 3b35a0ebf4..7109743052 100644
--- a/app/src/main/kotlin/io/element/android/x/MainActivity.kt
+++ b/app/src/main/kotlin/io/element/android/x/MainActivity.kt
@@ -30,7 +30,7 @@ import androidx.compose.ui.platform.LocalUriHandler
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import com.bumble.appyx.core.integration.NodeHost
-import com.bumble.appyx.core.integrationpoint.NodeComponentActivity
+import com.bumble.appyx.core.integrationpoint.NodeActivity
import com.bumble.appyx.core.plugin.NodeReadyObserver
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.core.log.logger.LoggerTag
@@ -42,7 +42,7 @@ import timber.log.Timber
private val loggerTag = LoggerTag("MainActivity")
-class MainActivity : NodeComponentActivity() {
+class MainActivity : NodeActivity() {
private lateinit var mainNode: MainNode
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
index a6572451d7..fad45b6def 100644
--- a/app/src/main/res/values-night/themes.xml
+++ b/app/src/main/res/values-night/themes.xml
@@ -22,5 +22,5 @@
- @style/Theme.ElementX
-
+
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 530821a92b..e7bafa39fe 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -21,5 +21,5 @@
- @drawable/transparent
- @style/Theme.ElementX
-
+
diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt
index 5f72bc6f86..04446072b6 100644
--- a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt
+++ b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt
@@ -20,6 +20,8 @@ import com.squareup.anvil.annotations.ContributesTo
import dagger.Module
import dagger.Provides
import io.element.android.libraries.di.AppScope
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
/**
* Configuration for the lock screen feature.
@@ -48,7 +50,16 @@ data class LockScreenConfig(
/**
* Time period before locking the app once backgrounded.
*/
- val gracePeriodInMillis: Long
+ val gracePeriod: Duration,
+
+ /**
+ * Authentication with strong methods (fingerprint, some face/iris unlock implementations) is supported.
+ */
+ val isStrongBiometricsEnabled: Boolean,
+ /**
+ * Authentication with weak methods (most face/iris unlock implementations) is supported.
+ */
+ val isWeakBiometricsEnabled: Boolean,
)
@ContributesTo(AppScope::class)
@@ -61,6 +72,8 @@ object LockScreenConfigModule {
pinBlacklist = setOf("0000", "1234"),
pinSize = 4,
maxPinCodeAttemptsBeforeLogout = 3,
- gracePeriodInMillis = 90_000L
+ gracePeriod = 90.seconds,
+ isStrongBiometricsEnabled = true,
+ isWeakBiometricsEnabled = true,
)
}
diff --git a/features/lockscreen/impl/build.gradle.kts b/features/lockscreen/impl/build.gradle.kts
index fdb9dcd178..168e72ba3d 100644
--- a/features/lockscreen/impl/build.gradle.kts
+++ b/features/lockscreen/impl/build.gradle.kts
@@ -46,6 +46,7 @@ dependencies {
implementation(projects.libraries.sessionStorage.api)
implementation(projects.services.appnavstate.api)
implementation(libs.androidx.datastore.preferences)
+ implementation(libs.androidx.biometric)
testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test)
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt
index af1c40ffb7..f4cc699907 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt
@@ -20,8 +20,11 @@ import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.appconfig.LockScreenConfig
import io.element.android.features.lockscreen.api.LockScreenLockState
import io.element.android.features.lockscreen.api.LockScreenService
+import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockManager
+import io.element.android.features.lockscreen.impl.biometric.DefaultBiometricUnlockCallback
import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
+import io.element.android.features.lockscreen.impl.storage.LockScreenStore
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.featureflag.api.FeatureFlagService
@@ -36,16 +39,19 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
+import kotlin.time.Duration
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class DefaultLockScreenService @Inject constructor(
private val lockScreenConfig: LockScreenConfig,
private val featureFlagService: FeatureFlagService,
+ private val lockScreenStore: LockScreenStore,
private val pinCodeManager: PinCodeManager,
private val coroutineScope: CoroutineScope,
private val sessionObserver: SessionObserver,
private val appForegroundStateService: AppForegroundStateService,
+ private val biometricUnlockManager: BiometricUnlockManager,
) : LockScreenService {
private val _lockScreenState = MutableStateFlow(LockScreenLockState.Unlocked)
@@ -63,6 +69,14 @@ class DefaultLockScreenService @Inject constructor(
_lockScreenState.value = LockScreenLockState.Unlocked
}
})
+ biometricUnlockManager.addCallback(object : DefaultBiometricUnlockCallback() {
+ override fun onBiometricUnlockSuccess() {
+ _lockScreenState.value = LockScreenLockState.Unlocked
+ coroutineScope.launch {
+ lockScreenStore.resetCounter()
+ }
+ }
+ })
coroutineScope.lockIfNeeded()
observeAppForegroundState()
observeSessionsState()
@@ -93,7 +107,7 @@ class DefaultLockScreenService @Inject constructor(
if (isInForeground) {
lockJob?.cancel()
} else {
- lockJob = lockIfNeeded(delayInMillis = lockScreenConfig.gracePeriodInMillis)
+ lockJob = lockIfNeeded(gracePeriod = lockScreenConfig.gracePeriod)
}
}
}
@@ -105,9 +119,9 @@ class DefaultLockScreenService @Inject constructor(
&& !pinCodeManager.isPinCodeAvailable()
}
- private fun CoroutineScope.lockIfNeeded(delayInMillis: Long = 0L) = launch {
+ private fun CoroutineScope.lockIfNeeded(gracePeriod: Duration = Duration.ZERO) = launch {
if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock) && pinCodeManager.isPinCodeAvailable()) {
- delay(delayInMillis)
+ delay(gracePeriod)
_lockScreenState.value = LockScreenLockState.Locked
}
}
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt
index a5560a5179..bf729a5d84 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt
@@ -33,7 +33,7 @@ import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
import io.element.android.features.lockscreen.impl.settings.LockScreenSettingsFlowNode
-import io.element.android.features.lockscreen.impl.setup.SetupPinNode
+import io.element.android.features.lockscreen.impl.setup.LockScreenSetupFlowNode
import io.element.android.features.lockscreen.impl.unlock.PinUnlockNode
import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.NodeInputs
@@ -97,7 +97,7 @@ class LockScreenFlowNode @AssistedInject constructor(
createNode(buildContext)
}
NavTarget.Setup -> {
- createNode(buildContext)
+ createNode(buildContext)
}
NavTarget.Settings -> {
createNode(buildContext)
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricUnlock.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricUnlock.kt
new file mode 100644
index 0000000000..e21b8e235c
--- /dev/null
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricUnlock.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (c) 2023 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 io.element.android.features.lockscreen.impl.biometric
+
+import android.security.keystore.KeyPermanentlyInvalidatedException
+import androidx.biometric.BiometricPrompt
+import androidx.biometric.BiometricPrompt.CryptoObject
+import androidx.biometric.BiometricPrompt.PromptInfo
+import androidx.core.content.ContextCompat
+import androidx.fragment.app.FragmentActivity
+import io.element.android.libraries.cryptography.api.EncryptionDecryptionService
+import io.element.android.libraries.cryptography.api.SecretKeyRepository
+import kotlinx.coroutines.CompletableDeferred
+import timber.log.Timber
+import java.security.InvalidKeyException
+import javax.crypto.Cipher
+
+interface BiometricUnlock {
+
+ interface Callback {
+ fun onBiometricSetupError()
+ fun onBiometricUnlockSuccess()
+ fun onBiometricUnlockFailed(error: Exception?)
+ }
+
+ sealed interface AuthenticationResult {
+ data object Success : AuthenticationResult
+ data class Failure(val error: Exception? = null) : AuthenticationResult
+ }
+
+ val isActive: Boolean
+ fun setup()
+ suspend fun authenticate(): AuthenticationResult
+}
+
+class NoopBiometricUnlock : BiometricUnlock {
+ override val isActive: Boolean = false
+ override fun setup() = Unit
+ override suspend fun authenticate() = BiometricUnlock.AuthenticationResult.Failure()
+}
+
+class DefaultBiometricUnlock(
+ private val activity: FragmentActivity,
+ private val promptInfo: PromptInfo,
+ private val secretKeyRepository: SecretKeyRepository,
+ private val encryptionDecryptionService: EncryptionDecryptionService,
+ private val keyAlias: String,
+ private val callbacks: List
+) : BiometricUnlock {
+
+ override val isActive: Boolean = true
+
+ private lateinit var cryptoObject: CryptoObject
+
+ override fun setup() {
+ try {
+ val secretKey = ensureKey()
+ val cipher = encryptionDecryptionService.createEncryptionCipher(secretKey)
+ cryptoObject = CryptoObject(cipher)
+ } catch (e: InvalidKeyException) {
+ callbacks.forEach { it.onBiometricSetupError() }
+ Timber.e(e, "Invalid biometric key")
+ }
+ }
+
+ override suspend fun authenticate(): BiometricUnlock.AuthenticationResult {
+ if (!this::cryptoObject.isInitialized) {
+ return BiometricUnlock.AuthenticationResult.Failure()
+ }
+ val deferredAuthenticationResult = CompletableDeferred()
+ val executor = ContextCompat.getMainExecutor(activity.baseContext)
+ val callback = AuthenticationCallback(callbacks, deferredAuthenticationResult)
+ val prompt = BiometricPrompt(activity, executor, callback)
+ prompt.authenticate(promptInfo, cryptoObject)
+ return deferredAuthenticationResult.await()
+ }
+
+ @Throws(KeyPermanentlyInvalidatedException::class)
+ private fun ensureKey() = secretKeyRepository.getOrCreateKey(keyAlias, true).also {
+ encryptionDecryptionService.createEncryptionCipher(it)
+ }
+}
+
+private class AuthenticationCallback(
+ private val callbacks: List,
+ private val deferredAuthenticationResult: CompletableDeferred,
+) : BiometricPrompt.AuthenticationCallback() {
+
+ override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
+ super.onAuthenticationError(errorCode, errString)
+ val biometricUnlockError = BiometricUnlockError(errorCode, errString.toString())
+ callbacks.forEach { it.onBiometricUnlockFailed(biometricUnlockError) }
+ deferredAuthenticationResult.complete(BiometricUnlock.AuthenticationResult.Failure(biometricUnlockError))
+ }
+
+ override fun onAuthenticationFailed() {
+ super.onAuthenticationFailed()
+ callbacks.forEach { it.onBiometricUnlockFailed(null) }
+ deferredAuthenticationResult.complete(BiometricUnlock.AuthenticationResult.Failure(null))
+ }
+
+ override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
+ super.onAuthenticationSucceeded(result)
+ if (result.cryptoObject?.cipher.isValid()) {
+ callbacks.forEach { it.onBiometricUnlockSuccess() }
+ deferredAuthenticationResult.complete(BiometricUnlock.AuthenticationResult.Success)
+ } else {
+ val error = IllegalStateException("Invalid cipher")
+ callbacks.forEach { it.onBiometricUnlockFailed(error) }
+ deferredAuthenticationResult.complete(BiometricUnlock.AuthenticationResult.Failure())
+ }
+ }
+
+ private fun Cipher?.isValid(): Boolean {
+ if (this == null) return false
+ return runCatching {
+ doFinal("biometric_challenge".toByteArray())
+ }.isSuccess
+ }
+}
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricUnlockError.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricUnlockError.kt
new file mode 100644
index 0000000000..37cc3dc1a9
--- /dev/null
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricUnlockError.kt
@@ -0,0 +1,38 @@
+/*
+ * 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 io.element.android.features.lockscreen.impl.biometric
+
+import androidx.biometric.BiometricPrompt
+
+/**
+ * Wrapper for [BiometricPrompt.AuthenticationCallback] errors.
+ */
+class BiometricUnlockError(val code: Int, message: String) : Exception(message) {
+ /**
+ * This error disables Biometric authentication, either temporarily or permanently.
+ */
+ val isAuthDisabledError: Boolean get() = code in LOCKOUT_ERROR_CODES
+
+ /**
+ * This error permanently disables Biometric authentication.
+ */
+ val isAuthPermanentlyDisabledError: Boolean get() = code == BiometricPrompt.ERROR_LOCKOUT_PERMANENT
+
+ companion object {
+ private val LOCKOUT_ERROR_CODES = arrayOf(BiometricPrompt.ERROR_LOCKOUT, BiometricPrompt.ERROR_LOCKOUT_PERMANENT)
+ }
+}
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricUnlockManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricUnlockManager.kt
new file mode 100644
index 0000000000..f7fe416f23
--- /dev/null
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricUnlockManager.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2023 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 io.element.android.features.lockscreen.impl.biometric
+
+import androidx.compose.runtime.Composable
+
+interface BiometricUnlockManager {
+
+ /**
+ * If the device is secured for example with a pin, pattern or password.
+ */
+ val isDeviceSecured: Boolean
+
+ /**
+ * If the device has biometric hardware and if the user has enrolled at least one biometric.
+ */
+ val hasAvailableAuthenticator: Boolean
+
+ fun addCallback(callback: BiometricUnlock.Callback)
+ fun removeCallback(callback: BiometricUnlock.Callback)
+
+ @Composable
+ fun rememberBiometricUnlock(): BiometricUnlock
+}
diff --git a/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/SecretKeyProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/DefaultBiometricUnlockCallback.kt
similarity index 66%
rename from libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/SecretKeyProvider.kt
rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/DefaultBiometricUnlockCallback.kt
index 85f57ac07f..34ed45b464 100644
--- a/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/SecretKeyProvider.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/DefaultBiometricUnlockCallback.kt
@@ -14,14 +14,10 @@
* limitations under the License.
*/
-package io.element.android.libraries.cryptography.api
+package io.element.android.features.lockscreen.impl.biometric
-import javax.crypto.SecretKey
-
-/**
- * Simple interface to get or create a secret key for a given alias.
- * Implementation should be able to store the generated key securely.
- */
-interface SecretKeyProvider {
- fun getOrCreateKey(alias: String): SecretKey
+open class DefaultBiometricUnlockCallback : BiometricUnlock.Callback {
+ override fun onBiometricSetupError() = Unit
+ override fun onBiometricUnlockSuccess() = Unit
+ override fun onBiometricUnlockFailed(error: Exception?) = Unit
}
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/DefaultBiometricUnlockManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/DefaultBiometricUnlockManager.kt
new file mode 100644
index 0000000000..2d3dd7146b
--- /dev/null
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/DefaultBiometricUnlockManager.kt
@@ -0,0 +1,148 @@
+/*
+ * Copyright (c) 2023 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 io.element.android.features.lockscreen.impl.biometric
+
+import android.app.KeyguardManager
+import android.content.Context
+import android.content.ContextWrapper
+import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricPrompt
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.res.stringResource
+import androidx.core.content.getSystemService
+import androidx.fragment.app.FragmentActivity
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.appconfig.LockScreenConfig
+import io.element.android.features.lockscreen.impl.R
+import io.element.android.features.lockscreen.impl.storage.LockScreenStore
+import io.element.android.libraries.cryptography.api.EncryptionDecryptionService
+import io.element.android.libraries.cryptography.api.SecretKeyRepository
+import io.element.android.libraries.di.AppScope
+import io.element.android.libraries.di.ApplicationContext
+import io.element.android.libraries.di.SingleIn
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import java.util.concurrent.CopyOnWriteArrayList
+import javax.inject.Inject
+
+private const val SECRET_KEY_ALIAS = "elementx.SECRET_KEY_ALIAS_BIOMETRIC"
+
+@ContributesBinding(AppScope::class)
+@SingleIn(AppScope::class)
+class DefaultBiometricUnlockManager @Inject constructor(
+ @ApplicationContext private val context: Context,
+ private val lockScreenStore: LockScreenStore,
+ private val lockScreenConfig: LockScreenConfig,
+ private val encryptionDecryptionService: EncryptionDecryptionService,
+ private val secretKeyRepository: SecretKeyRepository,
+ private val coroutineScope: CoroutineScope,
+) : BiometricUnlockManager {
+
+ private val callbacks = CopyOnWriteArrayList()
+ private val biometricManager = BiometricManager.from(context)
+ private val keyguardManager: KeyguardManager = context.getSystemService()!!
+
+ /**
+ * Returns true if a weak biometric method (i.e.: some face or iris unlock implementations) can be used.
+ */
+ private val canUseWeakBiometricAuth: Boolean
+ get() = lockScreenConfig.isWeakBiometricsEnabled
+ && biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) == BiometricManager.BIOMETRIC_SUCCESS
+
+ /**
+ * Returns true if a strong biometric method (i.e.: fingerprint, some face or iris unlock implementations) can be used.
+ */
+ private val canUseStrongBiometricAuth: Boolean
+ get() = lockScreenConfig.isStrongBiometricsEnabled
+ && biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS
+
+ /**
+ * Returns true if any biometric method (weak or strong) can be used.
+ */
+ override val hasAvailableAuthenticator: Boolean
+ get() = canUseWeakBiometricAuth || canUseStrongBiometricAuth
+
+ override val isDeviceSecured: Boolean
+ get() = keyguardManager.isDeviceSecure
+
+ private val internalCallback = object : DefaultBiometricUnlockCallback() {
+ override fun onBiometricSetupError() {
+ coroutineScope.launch {
+ lockScreenStore.setIsBiometricUnlockAllowed(false)
+ secretKeyRepository.deleteKey(SECRET_KEY_ALIAS)
+ }
+ }
+ }
+
+ @Composable
+ override fun rememberBiometricUnlock(): BiometricUnlock {
+ val isBiometricAllowed by lockScreenStore.isBiometricUnlockAllowed().collectAsState(initial = false)
+ val lifecycleState by LocalLifecycleOwner.current.lifecycle.currentStateFlow.collectAsState()
+ val isAvailable by remember(lifecycleState) {
+ derivedStateOf {
+ isBiometricAllowed && hasAvailableAuthenticator
+ }
+ }
+ val promptTitle = stringResource(id = R.string.screen_app_lock_biometric_unlock_title_android)
+ val promptNegative = stringResource(id = R.string.screen_app_lock_use_pin_android)
+ val activity = LocalContext.current.findFragmentActivity()
+ return remember(isAvailable) {
+ if (isAvailable && activity != null) {
+ val authenticators = when {
+ canUseStrongBiometricAuth -> BiometricManager.Authenticators.BIOMETRIC_STRONG
+ canUseWeakBiometricAuth -> BiometricManager.Authenticators.BIOMETRIC_WEAK
+ else -> 0
+ }
+ val promptInfo = BiometricPrompt.PromptInfo.Builder().apply {
+ setTitle(promptTitle)
+ setNegativeButtonText(promptNegative)
+ setAllowedAuthenticators(authenticators)
+ }.build()
+ DefaultBiometricUnlock(
+ activity = activity,
+ promptInfo = promptInfo,
+ secretKeyRepository = secretKeyRepository,
+ encryptionDecryptionService = encryptionDecryptionService,
+ keyAlias = SECRET_KEY_ALIAS,
+ callbacks = callbacks + internalCallback
+ )
+ } else {
+ NoopBiometricUnlock()
+ }
+ }
+ }
+
+ override fun addCallback(callback: BiometricUnlock.Callback) {
+ callbacks.add(callback)
+ }
+
+ override fun removeCallback(callback: BiometricUnlock.Callback) {
+ callbacks.remove(callback)
+ }
+
+ private fun Context.findFragmentActivity(): FragmentActivity? = when (this) {
+ is FragmentActivity -> this
+ is ContextWrapper -> baseContext.findFragmentActivity()
+ else -> null
+ }
+}
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt
index e5e2dc6106..a256562c43 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt
@@ -17,10 +17,10 @@
package io.element.android.features.lockscreen.impl.pin
import com.squareup.anvil.annotations.ContributesBinding
-import io.element.android.features.lockscreen.impl.pin.storage.PinCodeStore
+import io.element.android.features.lockscreen.impl.storage.LockScreenStore
import io.element.android.libraries.cryptography.api.EncryptionDecryptionService
import io.element.android.libraries.cryptography.api.EncryptionResult
-import io.element.android.libraries.cryptography.api.SecretKeyProvider
+import io.element.android.libraries.cryptography.api.SecretKeyRepository
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn
import java.util.concurrent.CopyOnWriteArrayList
@@ -31,9 +31,9 @@ private const val SECRET_KEY_ALIAS = "elementx.SECRET_KEY_ALIAS_PIN_CODE"
@ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)
class DefaultPinCodeManager @Inject constructor(
- private val secretKeyProvider: SecretKeyProvider,
+ private val secretKeyRepository: SecretKeyRepository,
private val encryptionDecryptionService: EncryptionDecryptionService,
- private val pinCodeStore: PinCodeStore,
+ private val lockScreenStore: LockScreenStore,
) : PinCodeManager {
private val callbacks = CopyOnWriteArrayList()
@@ -47,37 +47,37 @@ class DefaultPinCodeManager @Inject constructor(
}
override suspend fun isPinCodeAvailable(): Boolean {
- return pinCodeStore.hasPinCode()
+ return lockScreenStore.hasPinCode()
}
override suspend fun getPinCodeSize(): Int {
- val encryptedPinCode = pinCodeStore.getEncryptedCode() ?: return 0
- val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS)
+ val encryptedPinCode = lockScreenStore.getEncryptedCode() ?: return 0
+ val secretKey = secretKeyRepository.getOrCreateKey(SECRET_KEY_ALIAS, false)
val decryptedPinCode = encryptionDecryptionService.decrypt(secretKey, EncryptionResult.fromBase64(encryptedPinCode))
return decryptedPinCode.size
}
override suspend fun createPinCode(pinCode: String) {
- val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS)
+ val secretKey = secretKeyRepository.getOrCreateKey(SECRET_KEY_ALIAS, false)
val encryptedPinCode = encryptionDecryptionService.encrypt(secretKey, pinCode.toByteArray()).toBase64()
- pinCodeStore.saveEncryptedPinCode(encryptedPinCode)
+ lockScreenStore.saveEncryptedPinCode(encryptedPinCode)
callbacks.forEach { it.onPinCodeCreated() }
}
override suspend fun verifyPinCode(pinCode: String): Boolean {
- val encryptedPinCode = pinCodeStore.getEncryptedCode() ?: return false
+ val encryptedPinCode = lockScreenStore.getEncryptedCode() ?: return false
return try {
- val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS)
+ val secretKey = secretKeyRepository.getOrCreateKey(SECRET_KEY_ALIAS, false)
val decryptedPinCode = encryptionDecryptionService.decrypt(secretKey, EncryptionResult.fromBase64(encryptedPinCode))
val pinCodeToCheck = pinCode.toByteArray()
decryptedPinCode.contentEquals(pinCodeToCheck).also { isPinCodeCorrect ->
if (isPinCodeCorrect) {
- pinCodeStore.resetCounter()
+ lockScreenStore.resetCounter()
callbacks.forEach { callback ->
callback.onPinCodeVerified()
}
} else {
- pinCodeStore.onWrongPin()
+ lockScreenStore.onWrongPin()
}
}
} catch (failure: Throwable) {
@@ -86,12 +86,12 @@ class DefaultPinCodeManager @Inject constructor(
}
override suspend fun deletePinCode() {
- pinCodeStore.deleteEncryptedPinCode()
- pinCodeStore.resetCounter()
+ lockScreenStore.deleteEncryptedPinCode()
+ lockScreenStore.resetCounter()
callbacks.forEach { it.onPinCodeRemoved() }
}
override suspend fun getRemainingPinCodeAttemptsNumber(): Int {
- return pinCodeStore.getRemainingPinCodeAttemptsNumber()
+ return lockScreenStore.getRemainingPinCodeAttemptsNumber()
}
}
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt
index 9032e8d0ef..6cb41bfefc 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt
@@ -20,5 +20,5 @@ sealed interface LockScreenSettingsEvents {
data object OnRemovePin : LockScreenSettingsEvents
data object ConfirmRemovePin : LockScreenSettingsEvents
data object CancelRemovePin : LockScreenSettingsEvents
- data object ToggleBiometric : LockScreenSettingsEvents
+ data object ToggleBiometricAllowed : LockScreenSettingsEvents
}
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt
index a07607eea3..2ccff9f7bb 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt
@@ -32,9 +32,11 @@ import com.bumble.appyx.navmodel.backstack.operation.push
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
+import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockManager
+import io.element.android.features.lockscreen.impl.biometric.DefaultBiometricUnlockCallback
import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
-import io.element.android.features.lockscreen.impl.setup.SetupPinNode
+import io.element.android.features.lockscreen.impl.setup.LockScreenSetupFlowNode
import io.element.android.features.lockscreen.impl.unlock.PinUnlockNode
import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
@@ -48,6 +50,7 @@ class LockScreenSettingsFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
private val pinCodeManager: PinCodeManager,
+ private val biometricUnlockManager: BiometricUnlockManager,
) : BackstackNode(
backstack = BackStack(
initialElement = NavTarget.Unknown,
@@ -76,15 +79,17 @@ class LockScreenSettingsFlowNode @AssistedInject constructor(
backstack.newRoot(NavTarget.Settings)
}
- override fun onPinCodeCreated() {
- backstack.newRoot(NavTarget.Settings)
- }
-
override fun onPinCodeRemoved() {
navigateUp()
}
}
+ private val biometricUnlockCallback = object : DefaultBiometricUnlockCallback() {
+ override fun onBiometricUnlockSuccess() {
+ backstack.newRoot(NavTarget.Settings)
+ }
+ }
+
init {
lifecycleScope.launch {
if (pinCodeManager.isPinCodeAvailable()) {
@@ -96,9 +101,11 @@ class LockScreenSettingsFlowNode @AssistedInject constructor(
lifecycle.subscribe(
onCreate = {
pinCodeManager.addCallback(pinCodeManagerCallback)
+ biometricUnlockManager.addCallback(biometricUnlockCallback)
},
onDestroy = {
pinCodeManager.removeCallback(pinCodeManagerCallback)
+ biometricUnlockManager.removeCallback(biometricUnlockCallback)
}
)
}
@@ -109,7 +116,12 @@ class LockScreenSettingsFlowNode @AssistedInject constructor(
createNode(buildContext)
}
NavTarget.Setup -> {
- createNode(buildContext)
+ val callback = object : LockScreenSetupFlowNode.Callback {
+ override fun onSetupDone() {
+ backstack.newRoot(NavTarget.Settings)
+ }
+ }
+ createNode(buildContext, plugins = listOf(callback))
}
NavTarget.Settings -> {
val callback = object : LockScreenSettingsNode.Callback {
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt
index 9bef944194..2c086ea92a 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt
@@ -18,13 +18,16 @@ package io.element.android.features.lockscreen.impl.settings
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import io.element.android.appconfig.LockScreenConfig
+import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockManager
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
+import io.element.android.features.lockscreen.impl.storage.LockScreenStore
import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -33,6 +36,8 @@ import javax.inject.Inject
class LockScreenSettingsPresenter @Inject constructor(
private val lockScreenConfig: LockScreenConfig,
private val pinCodeManager: PinCodeManager,
+ private val lockScreenStore: LockScreenStore,
+ private val biometricUnlockManager: BiometricUnlockManager,
private val coroutineScope: CoroutineScope,
) : Presenter {
@@ -44,14 +49,16 @@ class LockScreenSettingsPresenter @Inject constructor(
var showRemovePinOption by remember {
mutableStateOf(false)
}
- var isBiometricEnabled by remember {
+ var showToggleBiometric by remember {
mutableStateOf(false)
}
+ val isBiometricEnabled by lockScreenStore.isBiometricUnlockAllowed().collectAsState(initial = false)
var showRemovePinConfirmation by remember {
mutableStateOf(false)
}
LaunchedEffect(triggerComputation) {
showRemovePinOption = !lockScreenConfig.isPinMandatory && pinCodeManager.isPinCodeAvailable()
+ showToggleBiometric = biometricUnlockManager.isDeviceSecured
}
fun handleEvents(event: LockScreenSettingsEvents) {
@@ -67,8 +74,10 @@ class LockScreenSettingsPresenter @Inject constructor(
}
}
LockScreenSettingsEvents.OnRemovePin -> showRemovePinConfirmation = true
- LockScreenSettingsEvents.ToggleBiometric -> {
- //TODO branch biometric logic
+ LockScreenSettingsEvents.ToggleBiometricAllowed -> {
+ coroutineScope.launch {
+ lockScreenStore.setIsBiometricUnlockAllowed(!isBiometricEnabled)
+ }
}
}
}
@@ -77,6 +86,7 @@ class LockScreenSettingsPresenter @Inject constructor(
showRemovePinOption = showRemovePinOption,
isBiometricEnabled = isBiometricEnabled,
showRemovePinConfirmation = showRemovePinConfirmation,
+ showToggleBiometric = showToggleBiometric,
eventSink = ::handleEvents
)
}
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsState.kt
index a6697d8a6d..856899eb47 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsState.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsState.kt
@@ -20,5 +20,6 @@ data class LockScreenSettingsState(
val showRemovePinOption: Boolean,
val isBiometricEnabled: Boolean,
val showRemovePinConfirmation: Boolean,
+ val showToggleBiometric: Boolean,
val eventSink: (LockScreenSettingsEvents) -> Unit
)
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt
index b693b033b1..2db2d62103 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt
@@ -31,9 +31,11 @@ fun aLockScreenSettingsState(
isLockMandatory: Boolean = false,
isBiometricEnabled: Boolean = false,
showRemovePinConfirmation: Boolean = false,
+ showToggleBiometric: Boolean = true,
) = LockScreenSettingsState(
showRemovePinOption = isLockMandatory,
isBiometricEnabled = isBiometricEnabled,
showRemovePinConfirmation = showRemovePinConfirmation,
+ showToggleBiometric = showToggleBiometric,
eventSink = {}
)
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt
index 97c640ba78..15254eb728 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt
@@ -58,8 +58,16 @@ fun LockScreenSettingsView(
}
)
}
- PreferenceDivider()
- PreferenceSwitch(title = stringResource(id = R.string.screen_app_lock_settings_enable_biometric_unlock), isChecked = state.isBiometricEnabled)
+ if (state.showToggleBiometric) {
+ PreferenceDivider()
+ PreferenceSwitch(
+ title = stringResource(id = R.string.screen_app_lock_settings_enable_biometric_unlock),
+ isChecked = state.isBiometricEnabled,
+ onCheckedChange = {
+ state.eventSink(LockScreenSettingsEvents.ToggleBiometricAllowed)
+ }
+ )
+ }
}
}
if (state.showRemovePinConfirmation) {
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/LockScreenSetupFlowNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/LockScreenSetupFlowNode.kt
new file mode 100644
index 0000000000..f612f3b82e
--- /dev/null
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/LockScreenSetupFlowNode.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2023 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 io.element.android.features.lockscreen.impl.setup
+
+import android.os.Parcelable
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.bumble.appyx.core.composable.Children
+import com.bumble.appyx.core.lifecycle.subscribe
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.core.plugin.Plugin
+import com.bumble.appyx.core.plugin.plugins
+import com.bumble.appyx.navmodel.backstack.BackStack
+import com.bumble.appyx.navmodel.backstack.operation.newRoot
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import io.element.android.anvilannotations.ContributesNode
+import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback
+import io.element.android.features.lockscreen.impl.pin.PinCodeManager
+import io.element.android.features.lockscreen.impl.setup.biometric.SetupBiometricNode
+import io.element.android.features.lockscreen.impl.setup.pin.SetupPinNode
+import io.element.android.libraries.architecture.BackstackNode
+import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
+import io.element.android.libraries.architecture.createNode
+import io.element.android.libraries.di.SessionScope
+import kotlinx.parcelize.Parcelize
+
+@ContributesNode(SessionScope::class)
+class LockScreenSetupFlowNode @AssistedInject constructor(
+ @Assisted buildContext: BuildContext,
+ @Assisted plugins: List,
+ private val pinCodeManager: PinCodeManager,
+) : BackstackNode(
+ backstack = BackStack(
+ initialElement = NavTarget.Pin,
+ savedStateMap = buildContext.savedStateMap,
+ ),
+ buildContext = buildContext,
+ plugins = plugins,
+) {
+
+ interface Callback : Plugin {
+ fun onSetupDone()
+ }
+
+ private fun onSetupDone() {
+ plugins().forEach { it.onSetupDone() }
+ }
+
+ sealed interface NavTarget : Parcelable {
+ @Parcelize
+ data object Pin : NavTarget
+
+ @Parcelize
+ data object Biometric : NavTarget
+ }
+
+ private val pinCodeManagerCallback = object : DefaultPinCodeManagerCallback() {
+
+ override fun onPinCodeCreated() {
+ backstack.newRoot(NavTarget.Biometric)
+ }
+ }
+
+ init {
+ lifecycle.subscribe(
+ onCreate = {
+ pinCodeManager.addCallback(pinCodeManagerCallback)
+ },
+ onDestroy = {
+ pinCodeManager.removeCallback(pinCodeManagerCallback)
+ }
+ )
+ }
+
+ override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
+ return when (navTarget) {
+ NavTarget.Pin -> {
+ createNode(buildContext)
+ }
+ NavTarget.Biometric -> {
+ val callback = object : SetupBiometricNode.Callback {
+ override fun onBiometricSetupDone() {
+ onSetupDone()
+ }
+ }
+ createNode(buildContext, plugins = listOf(callback))
+ }
+ }
+ }
+
+ @Composable
+ override fun View(modifier: Modifier) {
+ Children(
+ navModel = backstack,
+ modifier = modifier,
+ transitionHandler = rememberDefaultTransitionHandler(),
+ )
+ }
+}
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricEvents.kt
new file mode 100644
index 0000000000..d4d9e76d96
--- /dev/null
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricEvents.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2023 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 io.element.android.features.lockscreen.impl.setup.biometric
+
+sealed interface SetupBiometricEvents {
+ data object AllowBiometric : SetupBiometricEvents
+ data object UsePin : SetupBiometricEvents
+}
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricNode.kt
new file mode 100644
index 0000000000..ae99730a0e
--- /dev/null
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricNode.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2023 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 io.element.android.features.lockscreen.impl.setup.biometric
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Modifier
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.core.plugin.Plugin
+import com.bumble.appyx.core.plugin.plugins
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import io.element.android.anvilannotations.ContributesNode
+import io.element.android.libraries.di.SessionScope
+
+@ContributesNode(SessionScope::class)
+class SetupBiometricNode @AssistedInject constructor(
+ @Assisted buildContext: BuildContext,
+ @Assisted plugins: List,
+ private val presenter: SetupBiometricPresenter,
+) : Node(buildContext, plugins = plugins) {
+
+ interface Callback : Plugin {
+ fun onBiometricSetupDone()
+ }
+
+ private fun onSetupDone() {
+ plugins().forEach { it.onBiometricSetupDone() }
+ }
+
+ @Composable
+ override fun View(modifier: Modifier) {
+ val state = presenter.present()
+ LaunchedEffect(state.isBiometricSetupDone) {
+ if (state.isBiometricSetupDone) {
+ onSetupDone()
+ }
+ }
+ SetupBiometricView(
+ state = state,
+ modifier = modifier
+ )
+ }
+}
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricPresenter.kt
new file mode 100644
index 0000000000..ff65a2c7aa
--- /dev/null
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricPresenter.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2023 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 io.element.android.features.lockscreen.impl.setup.biometric
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import io.element.android.features.lockscreen.impl.storage.LockScreenStore
+import io.element.android.libraries.architecture.Presenter
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+class SetupBiometricPresenter @Inject constructor(
+ private val lockScreenStore: LockScreenStore,
+) : Presenter {
+
+ @Composable
+ override fun present(): SetupBiometricState {
+
+ var isBiometricSetupDone by remember {
+ mutableStateOf(false)
+ }
+
+ val coroutineScope = rememberCoroutineScope()
+
+ fun handleEvents(event: SetupBiometricEvents) {
+ when (event) {
+ SetupBiometricEvents.AllowBiometric -> coroutineScope.launch {
+ lockScreenStore.setIsBiometricUnlockAllowed(true)
+ isBiometricSetupDone = true
+ }
+ SetupBiometricEvents.UsePin -> coroutineScope.launch {
+ lockScreenStore.setIsBiometricUnlockAllowed(false)
+ isBiometricSetupDone = true
+ }
+ }
+ }
+
+ return SetupBiometricState(
+ isBiometricSetupDone = isBiometricSetupDone,
+ eventSink = ::handleEvents
+ )
+ }
+}
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricState.kt
new file mode 100644
index 0000000000..2f352f5d89
--- /dev/null
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricState.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2023 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 io.element.android.features.lockscreen.impl.setup.biometric
+
+data class SetupBiometricState(
+ val isBiometricSetupDone: Boolean,
+ val eventSink: (SetupBiometricEvents) -> Unit
+)
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricStateProvider.kt
new file mode 100644
index 0000000000..baecb23c96
--- /dev/null
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricStateProvider.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2023 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 io.element.android.features.lockscreen.impl.setup.biometric
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+
+open class SetupBiometricStateProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ aSetupBiometricState(),
+ )
+}
+
+fun aSetupBiometricState(
+ isBiometricSetupDone: Boolean = false,
+) = SetupBiometricState(
+ isBiometricSetupDone = isBiometricSetupDone,
+ eventSink = {}
+)
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricView.kt
new file mode 100644
index 0000000000..e1bdabb114
--- /dev/null
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricView.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2023 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 io.element.android.features.lockscreen.impl.setup.biometric
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.Arrangement.spacedBy
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Fingerprint
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import io.element.android.features.lockscreen.impl.R
+import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
+import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.designsystem.theme.components.Button
+import io.element.android.libraries.designsystem.theme.components.TextButton
+
+@Composable
+fun SetupBiometricView(
+ state: SetupBiometricState,
+ modifier: Modifier = Modifier,
+) {
+ BackHandler(true) {
+ state.eventSink(SetupBiometricEvents.UsePin)
+ }
+ HeaderFooterPage(
+ modifier = modifier.padding(top = 80.dp),
+ header = {
+ SetupBiometricHeader()
+ },
+ footer = {
+ SetupBiometricFooter(
+ onAllowClicked = { state.eventSink(SetupBiometricEvents.AllowBiometric) },
+ onSkipClicked = { state.eventSink(SetupBiometricEvents.UsePin) }
+ )
+ },
+ )
+}
+
+@Composable
+private fun SetupBiometricHeader(modifier: Modifier = Modifier) {
+ val biometricAuth = stringResource(id = R.string.screen_app_lock_biometric_authentication)
+ IconTitleSubtitleMolecule(
+ iconImageVector = Icons.Default.Fingerprint,
+ title = stringResource(id = R.string.screen_app_lock_settings_enable_biometric_unlock),
+ subTitle = stringResource(id = R.string.screen_app_lock_setup_biometric_unlock_subtitle, biometricAuth),
+ modifier = modifier
+ )
+}
+
+@Composable
+private fun SetupBiometricFooter(
+ onAllowClicked: () -> Unit,
+ onSkipClicked: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier.fillMaxWidth(),
+ verticalArrangement = spacedBy(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ val biometricAuth = stringResource(id = R.string.screen_app_lock_biometric_authentication)
+ Button(
+ text = stringResource(id = R.string.screen_app_lock_setup_biometric_unlock_allow_title, biometricAuth),
+ onClick = onAllowClicked
+ )
+ TextButton(
+ text = stringResource(id = R.string.screen_app_lock_setup_biometric_unlock_skip),
+ onClick = onSkipClicked
+ )
+ }
+}
+
+@Composable
+@PreviewsDayNight
+internal fun SetupBiometricViewPreview(@PreviewParameter(SetupBiometricStateProvider::class) state: SetupBiometricState) {
+ ElementPreview {
+ SetupBiometricView(
+ state = state,
+ )
+ }
+}
+
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinEvents.kt
similarity index 92%
rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinEvents.kt
rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinEvents.kt
index 45c5b034b0..d0105b1a21 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinEvents.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinEvents.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package io.element.android.features.lockscreen.impl.setup
+package io.element.android.features.lockscreen.impl.setup.pin
sealed interface SetupPinEvents {
data class OnPinEntryChanged(val entryAsText: String) : SetupPinEvents
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinNode.kt
similarity index 96%
rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinNode.kt
rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinNode.kt
index a1342c816b..c159c4cc58 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinNode.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinNode.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package io.element.android.features.lockscreen.impl.setup
+package io.element.android.features.lockscreen.impl.setup.pin
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinPresenter.kt
similarity index 94%
rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt
rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinPresenter.kt
index 89e8b98c51..33bf5ccba2 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinPresenter.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package io.element.android.features.lockscreen.impl.setup
+package io.element.android.features.lockscreen.impl.setup.pin
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -25,8 +25,8 @@ import androidx.compose.runtime.setValue
import io.element.android.appconfig.LockScreenConfig
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
-import io.element.android.features.lockscreen.impl.setup.validation.PinValidator
-import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure
+import io.element.android.features.lockscreen.impl.setup.pin.validation.PinValidator
+import io.element.android.features.lockscreen.impl.setup.pin.validation.SetupPinFailure
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta
import kotlinx.coroutines.delay
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinState.kt
similarity index 87%
rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinState.kt
rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinState.kt
index 3ae4a2c85b..4c9b68178f 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinState.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinState.kt
@@ -14,10 +14,10 @@
* limitations under the License.
*/
-package io.element.android.features.lockscreen.impl.setup
+package io.element.android.features.lockscreen.impl.setup.pin
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
-import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure
+import io.element.android.features.lockscreen.impl.setup.pin.validation.SetupPinFailure
data class SetupPinState(
val choosePinEntry: PinEntry,
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinStateProvider.kt
similarity index 93%
rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt
rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinStateProvider.kt
index bb0a46d10c..582c27b5fe 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinStateProvider.kt
@@ -14,11 +14,11 @@
* limitations under the License.
*/
-package io.element.android.features.lockscreen.impl.setup
+package io.element.android.features.lockscreen.impl.setup.pin
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
-import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure
+import io.element.android.features.lockscreen.impl.setup.pin.validation.SetupPinFailure
open class SetupPinStateProvider : PreviewParameterProvider {
override val values: Sequence
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinView.kt
similarity index 97%
rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt
rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinView.kt
index 537a90d0b6..3d41d1ba1d 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinView.kt
@@ -16,7 +16,7 @@
@file:OptIn(ExperimentalMaterial3Api::class)
-package io.element.android.features.lockscreen.impl.setup
+package io.element.android.features.lockscreen.impl.setup.pin
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.consumeWindowInsets
@@ -40,7 +40,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.features.lockscreen.impl.R
import io.element.android.features.lockscreen.impl.components.PinEntryTextField
-import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure
+import io.element.android.features.lockscreen.impl.setup.pin.validation.SetupPinFailure
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/validation/PinValidator.kt
similarity index 94%
rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt
rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/validation/PinValidator.kt
index ec17411396..ca01aab61f 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/validation/PinValidator.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package io.element.android.features.lockscreen.impl.setup.validation
+package io.element.android.features.lockscreen.impl.setup.pin.validation
import io.element.android.appconfig.LockScreenConfig
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/SetupPinFailure.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/validation/SetupPinFailure.kt
similarity index 90%
rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/SetupPinFailure.kt
rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/validation/SetupPinFailure.kt
index 3bb21cb9e6..271dcc2f2c 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/SetupPinFailure.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/validation/SetupPinFailure.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package io.element.android.features.lockscreen.impl.setup.validation
+package io.element.android.features.lockscreen.impl.setup.pin.validation
sealed interface SetupPinFailure {
data object PinBlacklisted : SetupPinFailure
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/EncryptedPinCodeStorage.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/EncryptedPinCodeStorage.kt
similarity index 95%
rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/EncryptedPinCodeStorage.kt
rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/EncryptedPinCodeStorage.kt
index 2345eaf481..7f19346cec 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/EncryptedPinCodeStorage.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/EncryptedPinCodeStorage.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package io.element.android.features.lockscreen.impl.pin.storage
+package io.element.android.features.lockscreen.impl.storage
/**
* Should be implemented by any class that provides access to the encrypted PIN code.
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PinCodeStore.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/LockScreenStore.kt
similarity index 71%
rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PinCodeStore.kt
rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/LockScreenStore.kt
index 818bc6a47d..77ce61d190 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PinCodeStore.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/LockScreenStore.kt
@@ -14,9 +14,11 @@
* limitations under the License.
*/
-package io.element.android.features.lockscreen.impl.pin.storage
+package io.element.android.features.lockscreen.impl.storage
-interface PinCodeStore : EncryptedPinCodeStorage {
+import kotlinx.coroutines.flow.Flow
+
+interface LockScreenStore : EncryptedPinCodeStorage {
/**
* Returns the remaining PIN code attempts. When this reaches 0 the PIN code access won't be available for some time.
@@ -32,6 +34,14 @@ interface PinCodeStore : EncryptedPinCodeStorage {
* Resets the counter of attempts for PIN code and biometric access.
*/
suspend fun resetCounter()
+
+ /**
+ * Returns whether the biometric unlock is allowed or not.
+ */
+ fun isBiometricUnlockAllowed(): Flow
+
+ /**
+ * Sets whether the biometric unlock is allowed or not.
+ */
+ suspend fun setIsBiometricUnlockAllowed(isAllowed: Boolean)
}
-
-
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/PreferencesLockScreenStore.kt
similarity index 82%
rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt
rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/PreferencesLockScreenStore.kt
index 8631c05502..4b01b6e62a 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/PreferencesLockScreenStore.kt
@@ -14,11 +14,12 @@
* limitations under the License.
*/
-package io.element.android.features.lockscreen.impl.pin.storage
+package io.element.android.features.lockscreen.impl.storage
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
@@ -28,6 +29,7 @@ import io.element.android.appconfig.LockScreenConfig
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import javax.inject.Inject
@@ -36,13 +38,14 @@ private val Context.dataStore: DataStore by preferencesDataStore(na
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
-class PreferencesPinCodeStore @Inject constructor(
+class PreferencesLockScreenStore @Inject constructor(
@ApplicationContext private val context: Context,
private val lockScreenConfig: LockScreenConfig,
-) : PinCodeStore {
+) : LockScreenStore {
private val pinCodeKey = stringPreferencesKey("encoded_pin_code")
private val remainingAttemptsKey = intPreferencesKey("remaining_pin_code_attempts")
+ private val biometricUnlockKey = booleanPreferencesKey("biometric_unlock_enabled")
override suspend fun getRemainingPinCodeAttemptsNumber(): Int {
return context.dataStore.data.map { preferences ->
@@ -88,5 +91,17 @@ class PreferencesPinCodeStore @Inject constructor(
}.first()
}
+ override fun isBiometricUnlockAllowed(): Flow {
+ return context.dataStore.data.map { preferences ->
+ preferences[biometricUnlockKey] ?: false
+ }
+ }
+
+ override suspend fun setIsBiometricUnlockAllowed(isAllowed: Boolean) {
+ context.dataStore.edit { preferences ->
+ preferences[biometricUnlockKey] = isAllowed
+ }
+ }
+
private fun Preferences.getRemainingPinCodeAttemptsNumber() = this[remainingAttemptsKey] ?: lockScreenConfig.maxPinCodeAttemptsBeforeLogout
}
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt
index 18c45f5e4b..b2a33f1e24 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt
@@ -24,4 +24,5 @@ sealed interface PinUnlockEvents {
data object ClearSignOutPrompt : PinUnlockEvents
data object SignOut : PinUnlockEvents
data object OnUseBiometric : PinUnlockEvents
+ data object ClearBiometricError : PinUnlockEvents
}
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt
index ed8ee1933d..0e1f78d9e6 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt
@@ -24,6 +24,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
+import io.element.android.features.lockscreen.impl.biometric.BiometricUnlock
+import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockManager
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
@@ -38,6 +40,7 @@ import javax.inject.Inject
class PinUnlockPresenter @Inject constructor(
private val pinCodeManager: PinCodeManager,
+ private val biometricUnlockManager: BiometricUnlockManager,
private val matrixClient: MatrixClient,
private val coroutineScope: CoroutineScope,
) : Presenter {
@@ -60,6 +63,11 @@ class PinUnlockPresenter @Inject constructor(
val signOutAction = remember {
mutableStateOf>(Async.Uninitialized)
}
+ var biometricUnlockResult by remember {
+ mutableStateOf(null)
+ }
+
+ val biometricUnlock = biometricUnlockManager.rememberBiometricUnlock()
LaunchedEffect(Unit) {
suspend {
@@ -67,6 +75,10 @@ class PinUnlockPresenter @Inject constructor(
PinEntry.createEmpty(pinCodeSize)
}.runCatchingUpdatingState(pinEntryState)
}
+ LaunchedEffect(biometricUnlock) {
+ biometricUnlock.setup()
+ biometricUnlock.authenticate()
+ }
LaunchedEffect(pinEntry) {
if (pinEntry.isComplete()) {
@@ -97,7 +109,12 @@ class PinUnlockPresenter @Inject constructor(
}
}
PinUnlockEvents.OnUseBiometric -> {
- //TODO
+ coroutineScope.launch {
+ biometricUnlockResult = biometricUnlock.authenticate()
+ }
+ }
+ PinUnlockEvents.ClearBiometricError -> {
+ biometricUnlockResult = null
}
}
}
@@ -107,6 +124,8 @@ class PinUnlockPresenter @Inject constructor(
remainingAttempts = remainingAttempts,
showSignOutPrompt = showSignOutPrompt,
signOutAction = signOutAction.value,
+ showBiometricUnlock = biometricUnlock.isActive,
+ biometricUnlockResult = biometricUnlockResult,
eventSink = ::handleEvents
)
}
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt
index 29d246b21b..667f7a825a 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt
@@ -16,6 +16,8 @@
package io.element.android.features.lockscreen.impl.unlock
+import io.element.android.features.lockscreen.impl.biometric.BiometricUnlock
+import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockError
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.libraries.architecture.Async
@@ -25,10 +27,22 @@ data class PinUnlockState(
val remainingAttempts: Async,
val showSignOutPrompt: Boolean,
val signOutAction: Async,
+ val showBiometricUnlock: Boolean,
+ val biometricUnlockResult: BiometricUnlock.AuthenticationResult?,
val eventSink: (PinUnlockEvents) -> Unit
) {
val isSignOutPromptCancellable = when (remainingAttempts) {
is Async.Success -> remainingAttempts.data > 0
else -> true
}
+
+ val biometricUnlockErrorMessage = when {
+ biometricUnlockResult is BiometricUnlock.AuthenticationResult.Failure
+ && biometricUnlockResult.error is BiometricUnlockError
+ && biometricUnlockResult.error.isAuthDisabledError -> {
+ biometricUnlockResult.error.message
+ }
+ else -> null
+ }
+ val showBiometricUnlockError = biometricUnlockErrorMessage != null
}
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt
index 009a5bf4a4..cbf6b2dee2 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt
@@ -17,6 +17,7 @@
package io.element.android.features.lockscreen.impl.unlock
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.features.lockscreen.impl.biometric.BiometricUnlock
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.libraries.architecture.Async
@@ -27,6 +28,7 @@ open class PinUnlockStateProvider : PreviewParameterProvider {
aPinUnlockState(pinEntry = PinEntry.createEmpty(4).fillWith("12")),
aPinUnlockState(showWrongPinTitle = true),
aPinUnlockState(showSignOutPrompt = true),
+ aPinUnlockState(showBiometricUnlock = false),
aPinUnlockState(showSignOutPrompt = true, remainingAttempts = 0),
aPinUnlockState(signOutAction = Async.Loading()),
)
@@ -37,12 +39,16 @@ fun aPinUnlockState(
remainingAttempts: Int = 3,
showWrongPinTitle: Boolean = false,
showSignOutPrompt: Boolean = false,
+ showBiometricUnlock: Boolean = true,
+ biometricUnlockResult: BiometricUnlock.AuthenticationResult? = null,
signOutAction: Async = Async.Uninitialized,
) = PinUnlockState(
pinEntry = Async.Success(pinEntry),
showWrongPinTitle = showWrongPinTitle,
remainingAttempts = Async.Success(remainingAttempts),
showSignOutPrompt = showSignOutPrompt,
+ showBiometricUnlock = showBiometricUnlock,
signOutAction = signOutAction,
+ biometricUnlockResult = biometricUnlockResult,
eventSink = {}
)
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt
index 514531a718..9d921eaaf8 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt
@@ -44,6 +44,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
+import androidx.lifecycle.Lifecycle
import io.element.android.features.lockscreen.impl.R
import io.element.android.features.lockscreen.impl.pin.model.PinDigit
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
@@ -58,6 +59,7 @@ import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
+import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings
@@ -66,6 +68,12 @@ fun PinUnlockView(
state: PinUnlockState,
modifier: Modifier = Modifier,
) {
+ OnLifecycleEvent { _, event ->
+ when (event) {
+ Lifecycle.Event.ON_RESUME -> state.eventSink.invoke(PinUnlockEvents.OnUseBiometric)
+ else -> Unit
+ }
+ }
Surface(modifier) {
BoxWithConstraints {
val commonModifier = Modifier
@@ -82,6 +90,7 @@ fun PinUnlockView(
val footer = @Composable {
PinUnlockFooter(
modifier = Modifier.padding(top = 24.dp),
+ showBiometricUnlock = state.showBiometricUnlock,
onUseBiometric = {
state.eventSink(PinUnlockEvents.OnUseBiometric)
},
@@ -126,6 +135,12 @@ fun PinUnlockView(
if (state.signOutAction is Async.Loading) {
ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content))
}
+ if (state.showBiometricUnlockError) {
+ ErrorDialog(
+ content = state.biometricUnlockErrorMessage ?: "",
+ onDismiss = { state.eventSink(PinUnlockEvents.ClearBiometricError) }
+ )
+ }
}
}
@@ -284,12 +299,15 @@ private fun PinUnlockHeader(
@Composable
private fun PinUnlockFooter(
+ showBiometricUnlock: Boolean,
onUseBiometric: () -> Unit,
onForgotPin: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround) {
- TextButton(text = "Use biometric", onClick = onUseBiometric)
+ if (showBiometricUnlock) {
+ TextButton(text = stringResource(id = R.string.screen_app_lock_use_biometric_android), onClick = onUseBiometric)
+ }
TextButton(text = stringResource(id = R.string.screen_app_lock_forgot_pin), onClick = onForgotPin)
}
}
diff --git a/features/lockscreen/impl/src/main/res/values/localazy.xml b/features/lockscreen/impl/src/main/res/values/localazy.xml
index bd6d522d57..865102ea5b 100644
--- a/features/lockscreen/impl/src/main/res/values/localazy.xml
+++ b/features/lockscreen/impl/src/main/res/values/localazy.xml
@@ -10,6 +10,7 @@
"biometric authentication"
"biometric unlock"
+ "Unlock with biometric"
"Forgot PIN?"
"Change PIN code"
"Allow biometric unlock"
@@ -30,5 +31,7 @@ Choose something memorable. If you forget this PIN, you will be logged out of th
"PINs don\'t match"
"You’ll need to re-login and create a new PIN to proceed"
"You are being signed out"
+ "Use biometric"
+ "Use PIN"
"Signing out…"
diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/biometric/FakeBiometricUnlockManager.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/biometric/FakeBiometricUnlockManager.kt
new file mode 100644
index 0000000000..d26a90d305
--- /dev/null
+++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/biometric/FakeBiometricUnlockManager.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2023 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 io.element.android.features.lockscreen.impl.biometric
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+
+class FakeBiometricUnlockManager : BiometricUnlockManager {
+
+ override var isDeviceSecured: Boolean = true
+ override var hasAvailableAuthenticator: Boolean = false
+
+ override fun addCallback(callback: BiometricUnlock.Callback) {
+ // no-op
+ }
+
+ override fun removeCallback(callback: BiometricUnlock.Callback) {
+ // no-op
+ }
+
+ @Composable
+ override fun rememberBiometricUnlock(): BiometricUnlock {
+ return remember {
+ NoopBiometricUnlock()
+ }
+ }
+}
diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/LockScreenConfig.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/LockScreenConfig.kt
index d42dad101a..aa575eabd4 100644
--- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/LockScreenConfig.kt
+++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/LockScreenConfig.kt
@@ -17,19 +17,25 @@
package io.element.android.features.lockscreen.impl.fixtures
import io.element.android.appconfig.LockScreenConfig
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
internal fun aLockScreenConfig(
isPinMandatory: Boolean = false,
pinBlacklist: Set = emptySet(),
pinSize: Int = 4,
maxPinCodeAttemptsBeforeLogout: Int = 3,
- gracePeriodInMillis: Long = 5 * 60 * 1000L
+ gracePeriod: Duration = 3.seconds,
+ isStrongBiometricsEnabled: Boolean = true,
+ isWeakBiometricsEnabled: Boolean = true,
): LockScreenConfig {
return LockScreenConfig(
isPinMandatory = isPinMandatory,
pinBlacklist = pinBlacklist,
pinSize = pinSize,
maxPinCodeAttemptsBeforeLogout = maxPinCodeAttemptsBeforeLogout,
- gracePeriodInMillis = gracePeriodInMillis
+ gracePeriod = gracePeriod,
+ isStrongBiometricsEnabled = isStrongBiometricsEnabled,
+ isWeakBiometricsEnabled = isWeakBiometricsEnabled,
)
}
diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/PinCodeManager.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/PinCodeManager.kt
index bf9ebdf541..b6bd73141f 100644
--- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/PinCodeManager.kt
+++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/PinCodeManager.kt
@@ -18,16 +18,16 @@ package io.element.android.features.lockscreen.impl.fixtures
import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManager
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
-import io.element.android.features.lockscreen.impl.pin.storage.InMemoryPinCodeStore
-import io.element.android.features.lockscreen.impl.pin.storage.PinCodeStore
+import io.element.android.features.lockscreen.impl.pin.storage.InMemoryLockScreenStore
+import io.element.android.features.lockscreen.impl.storage.LockScreenStore
import io.element.android.libraries.cryptography.api.EncryptionDecryptionService
import io.element.android.libraries.cryptography.impl.AESEncryptionDecryptionService
-import io.element.android.libraries.cryptography.test.SimpleSecretKeyProvider
+import io.element.android.libraries.cryptography.test.SimpleSecretKeyRepository
internal fun aPinCodeManager(
- pinCodeStore: PinCodeStore = InMemoryPinCodeStore(),
- secretKeyProvider: SimpleSecretKeyProvider = SimpleSecretKeyProvider(),
+ lockScreenStore: LockScreenStore = InMemoryLockScreenStore(),
+ secretKeyRepository: SimpleSecretKeyRepository = SimpleSecretKeyRepository(),
encryptionDecryptionService: EncryptionDecryptionService = AESEncryptionDecryptionService(),
): PinCodeManager {
- return DefaultPinCodeManager(secretKeyProvider, encryptionDecryptionService, pinCodeStore)
+ return DefaultPinCodeManager(secretKeyRepository, encryptionDecryptionService, lockScreenStore)
}
diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerTest.kt
index 8b14d15e5e..3c65620084 100644
--- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerTest.kt
+++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerTest.kt
@@ -17,18 +17,18 @@
package io.element.android.features.lockscreen.impl.pin
import com.google.common.truth.Truth.assertThat
-import io.element.android.features.lockscreen.impl.pin.storage.InMemoryPinCodeStore
+import io.element.android.features.lockscreen.impl.pin.storage.InMemoryLockScreenStore
import io.element.android.libraries.cryptography.impl.AESEncryptionDecryptionService
-import io.element.android.libraries.cryptography.test.SimpleSecretKeyProvider
+import io.element.android.libraries.cryptography.test.SimpleSecretKeyRepository
import kotlinx.coroutines.test.runTest
import org.junit.Test
class DefaultPinCodeManagerTest {
- private val pinCodeStore = InMemoryPinCodeStore()
- private val secretKeyProvider = SimpleSecretKeyProvider()
+ private val lockScreenStore = InMemoryLockScreenStore()
+ private val secretKeyRepository = SimpleSecretKeyRepository()
private val encryptionDecryptionService = AESEncryptionDecryptionService()
- private val pinCodeManager = DefaultPinCodeManager(secretKeyProvider, encryptionDecryptionService, pinCodeStore)
+ private val pinCodeManager = DefaultPinCodeManager(secretKeyRepository, encryptionDecryptionService, lockScreenStore)
@Test
fun `given a pin code when create and delete assert no pin code left`() = runTest {
diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryPinCodeStore.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryLockScreenStore.kt
similarity index 73%
rename from features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryPinCodeStore.kt
rename to features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryLockScreenStore.kt
index 6b6597728c..5d1af46ae5 100644
--- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryPinCodeStore.kt
+++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryLockScreenStore.kt
@@ -16,12 +16,17 @@
package io.element.android.features.lockscreen.impl.pin.storage
+import io.element.android.features.lockscreen.impl.storage.LockScreenStore
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+
private const val DEFAULT_REMAINING_ATTEMPTS = 3
-class InMemoryPinCodeStore : PinCodeStore {
+class InMemoryLockScreenStore : LockScreenStore {
private var pinCode: String? = null
private var remainingAttempts: Int = DEFAULT_REMAINING_ATTEMPTS
+ private var isBiometricUnlockAllowed = MutableStateFlow(false)
override suspend fun getRemainingPinCodeAttemptsNumber(): Int {
return remainingAttempts
@@ -50,4 +55,12 @@ class InMemoryPinCodeStore : PinCodeStore {
override suspend fun hasPinCode(): Boolean {
return pinCode != null
}
+
+ override fun isBiometricUnlockAllowed(): Flow {
+ return isBiometricUnlockAllowed
+ }
+
+ override suspend fun setIsBiometricUnlockAllowed(isAllowed: Boolean) {
+ isBiometricUnlockAllowed.value = isAllowed
+ }
}
diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenterTest.kt
index 578f20e7d9..bd99c09bbe 100644
--- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenterTest.kt
+++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenterTest.kt
@@ -21,8 +21,10 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.appconfig.LockScreenConfig
+import io.element.android.features.lockscreen.impl.biometric.FakeBiometricUnlockManager
import io.element.android.features.lockscreen.impl.fixtures.aLockScreenConfig
import io.element.android.features.lockscreen.impl.fixtures.aPinCodeManager
+import io.element.android.features.lockscreen.impl.pin.storage.InMemoryLockScreenStore
import io.element.android.tests.testutils.awaitLastSequentialItem
import io.element.android.tests.testutils.consumeItemsUntilPredicate
import kotlinx.coroutines.CoroutineScope
@@ -67,13 +69,16 @@ class LockScreenSettingsPresenterTest {
coroutineScope: CoroutineScope,
lockScreenConfig: LockScreenConfig = aLockScreenConfig(),
): LockScreenSettingsPresenter {
- val pinCodeManager = aPinCodeManager().apply {
+ val lockScreenStore = InMemoryLockScreenStore()
+ val pinCodeManager = aPinCodeManager(lockScreenStore = lockScreenStore).apply {
createPinCode("1234")
}
return LockScreenSettingsPresenter(
+ lockScreenStore = lockScreenStore,
pinCodeManager = pinCodeManager,
coroutineScope = coroutineScope,
lockScreenConfig = lockScreenConfig,
+ biometricUnlockManager = FakeBiometricUnlockManager(),
)
}
}
diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricPresenterTest.kt
new file mode 100644
index 0000000000..3db9d246f9
--- /dev/null
+++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricPresenterTest.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2023 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 io.element.android.features.lockscreen.impl.setup.biometric
+
+import app.cash.molecule.RecompositionMode
+import app.cash.molecule.moleculeFlow
+import app.cash.turbine.test
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.lockscreen.impl.pin.storage.InMemoryLockScreenStore
+import io.element.android.features.lockscreen.impl.storage.LockScreenStore
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class SetupBiometricPresenterTest {
+
+ @Test
+ fun `present - allow flow`() = runTest {
+ val lockScreenStore = InMemoryLockScreenStore()
+ val presenter = createSetupBiometricPresenter(lockScreenStore)
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ awaitItem().also { state ->
+ assertThat(state.isBiometricSetupDone).isFalse()
+ state.eventSink(SetupBiometricEvents.AllowBiometric)
+ }
+ awaitItem().also { state ->
+ assertThat(state.isBiometricSetupDone).isTrue()
+ }
+ }
+ assertThat(lockScreenStore.isBiometricUnlockAllowed().first()).isTrue()
+ }
+
+ @Test
+ fun `present - skip flow`() = runTest {
+ val lockScreenStore = InMemoryLockScreenStore()
+ val presenter = createSetupBiometricPresenter(lockScreenStore)
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ awaitItem().also { state ->
+ assertThat(state.isBiometricSetupDone).isFalse()
+ state.eventSink(SetupBiometricEvents.UsePin)
+ }
+ awaitItem().also { state ->
+ assertThat(state.isBiometricSetupDone).isTrue()
+ }
+ }
+ assertThat(lockScreenStore.isBiometricUnlockAllowed().first()).isFalse()
+ }
+
+ private fun createSetupBiometricPresenter(
+ lockScreenStore: LockScreenStore = InMemoryLockScreenStore()
+ ): SetupBiometricPresenter {
+ return SetupBiometricPresenter(
+ lockScreenStore = lockScreenStore,
+ )
+ }
+}
diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinPresenterTest.kt
similarity index 96%
rename from features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt
rename to features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinPresenterTest.kt
index 3969ea7c28..7f3373bd4a 100644
--- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt
+++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinPresenterTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package io.element.android.features.lockscreen.impl.setup
+package io.element.android.features.lockscreen.impl.setup.pin
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
@@ -27,8 +27,8 @@ import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCall
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
import io.element.android.features.lockscreen.impl.pin.model.assertEmpty
import io.element.android.features.lockscreen.impl.pin.model.assertText
-import io.element.android.features.lockscreen.impl.setup.validation.PinValidator
-import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure
+import io.element.android.features.lockscreen.impl.setup.pin.validation.PinValidator
+import io.element.android.features.lockscreen.impl.setup.pin.validation.SetupPinFailure
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.tests.testutils.awaitLastSequentialItem
import io.element.android.tests.testutils.consumeItemsUntilPredicate
diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt
index 234beae337..95ded7617c 100644
--- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt
+++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt
@@ -20,6 +20,8 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
+import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockManager
+import io.element.android.features.lockscreen.impl.biometric.FakeBiometricUnlockManager
import io.element.android.features.lockscreen.impl.fixtures.aPinCodeManager
import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
@@ -48,7 +50,7 @@ class PinUnlockPresenterTest {
pinCodeVerified.complete(Unit)
}
}
- val presenter = createPinUnlockPresenter(this, callback)
+ val presenter = createPinUnlockPresenter(this, callback = callback)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -88,7 +90,7 @@ class PinUnlockPresenterTest {
pinCodeVerified.complete(Unit)
}
}
- val presenter = createPinUnlockPresenter(this, callback)
+ val presenter = createPinUnlockPresenter(this, callback = callback)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -146,6 +148,7 @@ class PinUnlockPresenterTest {
private suspend fun createPinUnlockPresenter(
scope: CoroutineScope,
+ biometricUnlockManager: BiometricUnlockManager = FakeBiometricUnlockManager(),
callback: PinCodeManager.Callback = DefaultPinCodeManagerCallback(),
): PinUnlockPresenter {
val pinCodeManager = aPinCodeManager().apply {
@@ -153,9 +156,10 @@ class PinUnlockPresenterTest {
createPinCode(completePin)
}
return PinUnlockPresenter(
- pinCodeManager,
- FakeMatrixClient(),
- scope,
+ pinCodeManager = pinCodeManager,
+ biometricUnlockManager = biometricUnlockManager,
+ matrixClient = FakeMatrixClient(),
+ coroutineScope = scope,
)
}
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index ce124fd8e8..421210a841 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -14,7 +14,7 @@ datastore = "1.0.0"
constraintlayout = "2.1.4"
constraintlayout_compose = "1.0.1"
recyclerview = "1.3.2"
-lifecycle = "2.6.2"
+lifecycle = "2.7.0-alpha03"
activity = "1.8.0"
startup = "1.1.1"
media3 = "1.1.1"
@@ -90,6 +90,7 @@ androidx_splash = "androidx.core:core-splashscreen:1.0.1"
androidx_security_crypto = "androidx.security:security-crypto:1.1.0-alpha06"
androidx_media3_exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
androidx_media3_ui = { module = "androidx.media3:media3-ui", version.ref = "media3" }
+androidx_biometric = "androidx.biometric:biometric-ktx:1.2.0-alpha05"
androidx_activity_activity = { module = "androidx.activity:activity", version.ref = "activity" }
androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity" }
diff --git a/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/SecretKeyRepository.kt b/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/SecretKeyRepository.kt
new file mode 100644
index 0000000000..71f29b9be8
--- /dev/null
+++ b/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/SecretKeyRepository.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2023 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 io.element.android.libraries.cryptography.api
+
+import javax.crypto.SecretKey
+
+/**
+ * Simple interface to get, create and delete a secret key for a given alias.
+ * Implementation should be able to store the generated key securely.
+ */
+interface SecretKeyRepository {
+ /**
+ * Get or create a secret key for a given alias.
+ * @param alias the alias to use
+ * @param requiresUserAuthentication true if the key should be protected by user authentication
+ */
+ fun getOrCreateKey(alias: String, requiresUserAuthentication: Boolean): SecretKey
+
+ /**
+ * Delete the secret key for a given alias.
+ * @param alias the alias to use
+ */
+ fun deleteKey(alias: String)
+}
diff --git a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/CryptographyModule.kt b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/CryptographyModule.kt
new file mode 100644
index 0000000000..7b55dca15b
--- /dev/null
+++ b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/CryptographyModule.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2023 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 io.element.android.libraries.cryptography.impl
+
+import com.squareup.anvil.annotations.ContributesTo
+import dagger.Module
+import dagger.Provides
+import io.element.android.libraries.di.AppScope
+import java.security.KeyStore
+
+internal const val ANDROID_KEYSTORE = "AndroidKeyStore"
+
+@ContributesTo(AppScope::class)
+@Module
+object CryptographyModule {
+
+ @Provides
+ fun providesAndroidKeyStore(): KeyStore {
+ return KeyStore.getInstance(ANDROID_KEYSTORE).apply {
+ load(null)
+ }
+ }
+}
diff --git a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyRepository.kt
similarity index 75%
rename from libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt
rename to libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyRepository.kt
index 9313e7a48b..f1bdb03b5c 100644
--- a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt
+++ b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyRepository.kt
@@ -21,28 +21,27 @@ import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.cryptography.api.AESEncryptionSpecs
-import io.element.android.libraries.cryptography.api.SecretKeyProvider
+import io.element.android.libraries.cryptography.api.SecretKeyRepository
import io.element.android.libraries.di.AppScope
+import timber.log.Timber
import java.security.KeyStore
+import java.security.KeyStoreException
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.inject.Inject
-private const val ANDROID_KEYSTORE = "AndroidKeyStore"
-
/**
- * Default implementation of [SecretKeyProvider] that uses the Android Keystore to store the keys.
+ * Default implementation of [SecretKeyRepository] that uses the Android Keystore to store the keys.
* The generated key uses AES algorithm, with a key size of 128 bits, and the GCM block mode.
*/
@ContributesBinding(AppScope::class)
-class KeyStoreSecretKeyProvider @Inject constructor() : SecretKeyProvider {
+class KeyStoreSecretKeyRepository @Inject constructor(
+ private val keyStore: KeyStore,
+) : SecretKeyRepository {
// False positive lint issue
@SuppressLint("WrongConstant")
- override fun getOrCreateKey(alias: String): SecretKey {
- val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply {
- load(null)
- }
+ override fun getOrCreateKey(alias: String, requiresUserAuthentication: Boolean): SecretKey {
val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry)
?.secretKey
return if (secretKeyEntry == null) {
@@ -54,6 +53,7 @@ class KeyStoreSecretKeyProvider @Inject constructor() : SecretKeyProvider {
.setBlockModes(AESEncryptionSpecs.BLOCK_MODE)
.setEncryptionPaddings(AESEncryptionSpecs.PADDINGS)
.setKeySize(AESEncryptionSpecs.KEY_SIZE)
+ .setUserAuthenticationRequired(requiresUserAuthentication)
.build()
generator.init(keyGenSpec)
generator.generateKey()
@@ -61,4 +61,12 @@ class KeyStoreSecretKeyProvider @Inject constructor() : SecretKeyProvider {
secretKeyEntry
}
}
+
+ override fun deleteKey(alias: String) {
+ try {
+ keyStore.deleteEntry(alias)
+ } catch (e: KeyStoreException) {
+ Timber.e(e)
+ }
+ }
}
diff --git a/libraries/cryptography/test/src/main/kotlin/io/element/android/libraries/cryptography/test/SimpleSecretKeyProvider.kt b/libraries/cryptography/test/src/main/kotlin/io/element/android/libraries/cryptography/test/SimpleSecretKeyRepository.kt
similarity index 78%
rename from libraries/cryptography/test/src/main/kotlin/io/element/android/libraries/cryptography/test/SimpleSecretKeyProvider.kt
rename to libraries/cryptography/test/src/main/kotlin/io/element/android/libraries/cryptography/test/SimpleSecretKeyRepository.kt
index d06a545d78..b6f9155d12 100644
--- a/libraries/cryptography/test/src/main/kotlin/io/element/android/libraries/cryptography/test/SimpleSecretKeyProvider.kt
+++ b/libraries/cryptography/test/src/main/kotlin/io/element/android/libraries/cryptography/test/SimpleSecretKeyRepository.kt
@@ -17,20 +17,24 @@
package io.element.android.libraries.cryptography.test
import io.element.android.libraries.cryptography.api.AESEncryptionSpecs
-import io.element.android.libraries.cryptography.api.SecretKeyProvider
+import io.element.android.libraries.cryptography.api.SecretKeyRepository
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
-class SimpleSecretKeyProvider : SecretKeyProvider {
+class SimpleSecretKeyRepository : SecretKeyRepository {
private var secretKeyForAlias = HashMap()
- override fun getOrCreateKey(alias: String): SecretKey {
+ override fun getOrCreateKey(alias: String, requiresUserAuthentication: Boolean): SecretKey {
return secretKeyForAlias.getOrPut(alias) {
generateKey()
}
}
+ override fun deleteKey(alias: String) {
+ secretKeyForAlias.remove(alias)
+ }
+
private fun generateKey(): SecretKey {
val keyGenerator = KeyGenerator.getInstance(AESEncryptionSpecs.ALGORITHM)
keyGenerator.init(AESEncryptionSpecs.KEY_SIZE)
diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml
index 33a0789240..8ff9fc54b9 100644
--- a/libraries/ui-strings/src/main/res/values/localazy.xml
+++ b/libraries/ui-strings/src/main/res/values/localazy.xml
@@ -218,6 +218,7 @@
"This is the beginning of %1$s."
"This is the beginning of this conversation."
"New"
+ "Notify the whole room"
"Share analytics data"
"Failed selecting media, please try again."
"Failed processing media to upload, please try again."
@@ -262,6 +263,7 @@ If you proceed, some of your settings may change."
"en"
"Error"
"Success"
+ "Everyone"
"Share anonymous usage data to help us identify issues."
"You can read all our terms %1$s."
"here"
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.biometric_null_SetupBiometricView-D-2_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.biometric_null_SetupBiometricView-D-2_2_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..5ecc718c8f
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.biometric_null_SetupBiometricView-D-2_2_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fb7cf95951b4cae20445929a7b0ce7d3fb4cb1ff0d6ce735e890106754485856
+size 37651
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.biometric_null_SetupBiometricView-N-2_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.biometric_null_SetupBiometricView-N-2_3_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..652efd1ba4
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.biometric_null_SetupBiometricView-N-2_3_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:38e9a9481637e9fafbc2bf4083b1024e735bd40f6b4bdcda4f50b403df475e96
+size 35595
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-D-3_3_null_0,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_0,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-D-3_3_null_0,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-D-3_3_null_1,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_1,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-D-3_3_null_1,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-D-3_3_null_2,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_2,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-D-3_3_null_2,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-D-3_3_null_3,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_3,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-D-3_3_null_3,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-D-3_3_null_4,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_4,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-D-3_3_null_4,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-N-3_4_null_0,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_0,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-N-3_4_null_0,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-N-3_4_null_1,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_1,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-N-3_4_null_1,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-N-3_4_null_2,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_2,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-N-3_4_null_2,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-N-3_4_null_3,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_3,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-N-3_4_null_3,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-N-3_4_null_4,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_4,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-N-3_4_null_4,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-D-4_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-D-5_5_null,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-D-4_4_null,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-D-5_5_null,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-N-4_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-N-5_6_null,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-N-4_5_null,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-N-5_6_null,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-4_4_null_0,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_0,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-4_4_null_0,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-4_4_null_1,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_1,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-4_4_null_1,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-4_4_null_2,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_2,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-4_4_null_2,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-4_4_null_3,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_3,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-4_4_null_3,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-4_4_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-4_4_null_4,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..e401862562
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-4_4_null_4,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1facbdcba634b37a8162876e6af02a3565eaee2ac4afe32816b1f8e9602b2a65
+size 36565
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-4_4_null_5,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_4,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-4_4_null_5,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-4_4_null_6,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_5,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-4_4_null_6,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-4_5_null_0,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_0,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-4_5_null_0,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-4_5_null_1,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_1,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-4_5_null_1,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-4_5_null_2,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_2,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-4_5_null_2,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-4_5_null_3,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_3,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-4_5_null_3,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-4_5_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-4_5_null_4,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..02565fd0ec
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-4_5_null_4,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a4b3e856f102b2d53fd5776204a8576ed515e9e4bfd9899e4138eb1515a11b12
+size 34241
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-4_5_null_5,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_4,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-4_5_null_5,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-4_5_null_6,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_5,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-4_5_null_6,NEXUS_5,1.0,en].png