Feature/fga/biometric unlock (#1702)
* Biometric unlock : refactor a bit existing classes * Biometric unlock : first implementation * Biometric: add ui for biometric setup * Biometric unlock : use localazy strings * Biometric unlock setup : branch skip/allow events * Biometric : fix tests * Biometrics: add small test * Biometric : clean up * Update screenshots * Biometric unlock : address some PR review * Biometric : improve a bit edge cases * Fix lint issues --------- Co-authored-by: ganfra <francoisg@element.io> Co-authored-by: ElementBot <benoitm+elementbot@element.io> Co-authored-by: Jorge Martín <jorgem@element.io>
This commit is contained in:
parent
a008f342de
commit
8d903362c8
86 changed files with 1270 additions and 107 deletions
|
|
@ -30,7 +30,7 @@ import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import com.bumble.appyx.core.integration.NodeHost
|
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 com.bumble.appyx.core.plugin.NodeReadyObserver
|
||||||
import io.element.android.libraries.architecture.bindings
|
import io.element.android.libraries.architecture.bindings
|
||||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
|
|
@ -42,7 +42,7 @@ import timber.log.Timber
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("MainActivity")
|
private val loggerTag = LoggerTag("MainActivity")
|
||||||
|
|
||||||
class MainActivity : NodeComponentActivity() {
|
class MainActivity : NodeActivity() {
|
||||||
|
|
||||||
private lateinit var mainNode: MainNode
|
private lateinit var mainNode: MainNode
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,5 +22,5 @@
|
||||||
<item name="postSplashScreenTheme">@style/Theme.ElementX</item>
|
<item name="postSplashScreenTheme">@style/Theme.ElementX</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Theme.ElementX" parent="Theme.Material3.Dark" />
|
<style name="Theme.ElementX" parent="Theme.Material3.Dark.NoActionBar" />
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -21,5 +21,5 @@
|
||||||
<item name="windowSplashScreenAnimatedIcon">@drawable/transparent</item>
|
<item name="windowSplashScreenAnimatedIcon">@drawable/transparent</item>
|
||||||
<item name="postSplashScreenTheme">@style/Theme.ElementX</item>
|
<item name="postSplashScreenTheme">@style/Theme.ElementX</item>
|
||||||
</style>
|
</style>
|
||||||
<style name="Theme.ElementX" parent="Theme.Material3.Light" />
|
<style name="Theme.ElementX" parent="Theme.Material3.Light.NoActionBar" />
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ import com.squareup.anvil.annotations.ContributesTo
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration for the lock screen feature.
|
* Configuration for the lock screen feature.
|
||||||
|
|
@ -48,7 +50,16 @@ data class LockScreenConfig(
|
||||||
/**
|
/**
|
||||||
* Time period before locking the app once backgrounded.
|
* 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)
|
@ContributesTo(AppScope::class)
|
||||||
|
|
@ -61,6 +72,8 @@ object LockScreenConfigModule {
|
||||||
pinBlacklist = setOf("0000", "1234"),
|
pinBlacklist = setOf("0000", "1234"),
|
||||||
pinSize = 4,
|
pinSize = 4,
|
||||||
maxPinCodeAttemptsBeforeLogout = 3,
|
maxPinCodeAttemptsBeforeLogout = 3,
|
||||||
gracePeriodInMillis = 90_000L
|
gracePeriod = 90.seconds,
|
||||||
|
isStrongBiometricsEnabled = true,
|
||||||
|
isWeakBiometricsEnabled = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ dependencies {
|
||||||
implementation(projects.libraries.sessionStorage.api)
|
implementation(projects.libraries.sessionStorage.api)
|
||||||
implementation(projects.services.appnavstate.api)
|
implementation(projects.services.appnavstate.api)
|
||||||
implementation(libs.androidx.datastore.preferences)
|
implementation(libs.androidx.datastore.preferences)
|
||||||
|
implementation(libs.androidx.biometric)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
testImplementation(libs.coroutines.test)
|
testImplementation(libs.coroutines.test)
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,11 @@ import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.appconfig.LockScreenConfig
|
import io.element.android.appconfig.LockScreenConfig
|
||||||
import io.element.android.features.lockscreen.api.LockScreenLockState
|
import io.element.android.features.lockscreen.api.LockScreenLockState
|
||||||
import io.element.android.features.lockscreen.api.LockScreenService
|
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.DefaultPinCodeManagerCallback
|
||||||
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
|
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.AppScope
|
||||||
import io.element.android.libraries.di.SingleIn
|
import io.element.android.libraries.di.SingleIn
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
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.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.time.Duration
|
||||||
|
|
||||||
@SingleIn(AppScope::class)
|
@SingleIn(AppScope::class)
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultLockScreenService @Inject constructor(
|
class DefaultLockScreenService @Inject constructor(
|
||||||
private val lockScreenConfig: LockScreenConfig,
|
private val lockScreenConfig: LockScreenConfig,
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
|
private val lockScreenStore: LockScreenStore,
|
||||||
private val pinCodeManager: PinCodeManager,
|
private val pinCodeManager: PinCodeManager,
|
||||||
private val coroutineScope: CoroutineScope,
|
private val coroutineScope: CoroutineScope,
|
||||||
private val sessionObserver: SessionObserver,
|
private val sessionObserver: SessionObserver,
|
||||||
private val appForegroundStateService: AppForegroundStateService,
|
private val appForegroundStateService: AppForegroundStateService,
|
||||||
|
private val biometricUnlockManager: BiometricUnlockManager,
|
||||||
) : LockScreenService {
|
) : LockScreenService {
|
||||||
|
|
||||||
private val _lockScreenState = MutableStateFlow<LockScreenLockState>(LockScreenLockState.Unlocked)
|
private val _lockScreenState = MutableStateFlow<LockScreenLockState>(LockScreenLockState.Unlocked)
|
||||||
|
|
@ -63,6 +69,14 @@ class DefaultLockScreenService @Inject constructor(
|
||||||
_lockScreenState.value = LockScreenLockState.Unlocked
|
_lockScreenState.value = LockScreenLockState.Unlocked
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
biometricUnlockManager.addCallback(object : DefaultBiometricUnlockCallback() {
|
||||||
|
override fun onBiometricUnlockSuccess() {
|
||||||
|
_lockScreenState.value = LockScreenLockState.Unlocked
|
||||||
|
coroutineScope.launch {
|
||||||
|
lockScreenStore.resetCounter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
coroutineScope.lockIfNeeded()
|
coroutineScope.lockIfNeeded()
|
||||||
observeAppForegroundState()
|
observeAppForegroundState()
|
||||||
observeSessionsState()
|
observeSessionsState()
|
||||||
|
|
@ -93,7 +107,7 @@ class DefaultLockScreenService @Inject constructor(
|
||||||
if (isInForeground) {
|
if (isInForeground) {
|
||||||
lockJob?.cancel()
|
lockJob?.cancel()
|
||||||
} else {
|
} else {
|
||||||
lockJob = lockIfNeeded(delayInMillis = lockScreenConfig.gracePeriodInMillis)
|
lockJob = lockIfNeeded(gracePeriod = lockScreenConfig.gracePeriod)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -105,9 +119,9 @@ class DefaultLockScreenService @Inject constructor(
|
||||||
&& !pinCodeManager.isPinCodeAvailable()
|
&& !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()) {
|
if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock) && pinCodeManager.isPinCodeAvailable()) {
|
||||||
delay(delayInMillis)
|
delay(gracePeriod)
|
||||||
_lockScreenState.value = LockScreenLockState.Locked
|
_lockScreenState.value = LockScreenLockState.Locked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.DefaultPinCodeManagerCallback
|
||||||
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
|
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.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.features.lockscreen.impl.unlock.PinUnlockNode
|
||||||
import io.element.android.libraries.architecture.BackstackNode
|
import io.element.android.libraries.architecture.BackstackNode
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
|
|
@ -97,7 +97,7 @@ class LockScreenFlowNode @AssistedInject constructor(
|
||||||
createNode<PinUnlockNode>(buildContext)
|
createNode<PinUnlockNode>(buildContext)
|
||||||
}
|
}
|
||||||
NavTarget.Setup -> {
|
NavTarget.Setup -> {
|
||||||
createNode<SetupPinNode>(buildContext)
|
createNode<LockScreenSetupFlowNode>(buildContext)
|
||||||
}
|
}
|
||||||
NavTarget.Settings -> {
|
NavTarget.Settings -> {
|
||||||
createNode<LockScreenSettingsFlowNode>(buildContext)
|
createNode<LockScreenSettingsFlowNode>(buildContext)
|
||||||
|
|
|
||||||
|
|
@ -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.Callback>
|
||||||
|
) : 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<BiometricUnlock.AuthenticationResult>()
|
||||||
|
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<BiometricUnlock.Callback>,
|
||||||
|
private val deferredAuthenticationResult: CompletableDeferred<BiometricUnlock.AuthenticationResult>,
|
||||||
|
) : 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -14,14 +14,10 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.cryptography.api
|
package io.element.android.features.lockscreen.impl.biometric
|
||||||
|
|
||||||
import javax.crypto.SecretKey
|
open class DefaultBiometricUnlockCallback : BiometricUnlock.Callback {
|
||||||
|
override fun onBiometricSetupError() = Unit
|
||||||
/**
|
override fun onBiometricUnlockSuccess() = Unit
|
||||||
* Simple interface to get or create a secret key for a given alias.
|
override fun onBiometricUnlockFailed(error: Exception?) = Unit
|
||||||
* Implementation should be able to store the generated key securely.
|
|
||||||
*/
|
|
||||||
interface SecretKeyProvider {
|
|
||||||
fun getOrCreateKey(alias: String): SecretKey
|
|
||||||
}
|
}
|
||||||
|
|
@ -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<BiometricUnlock.Callback>()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,10 +17,10 @@
|
||||||
package io.element.android.features.lockscreen.impl.pin
|
package io.element.android.features.lockscreen.impl.pin
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
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.EncryptionDecryptionService
|
||||||
import io.element.android.libraries.cryptography.api.EncryptionResult
|
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.AppScope
|
||||||
import io.element.android.libraries.di.SingleIn
|
import io.element.android.libraries.di.SingleIn
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
|
@ -31,9 +31,9 @@ private const val SECRET_KEY_ALIAS = "elementx.SECRET_KEY_ALIAS_PIN_CODE"
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
@SingleIn(AppScope::class)
|
@SingleIn(AppScope::class)
|
||||||
class DefaultPinCodeManager @Inject constructor(
|
class DefaultPinCodeManager @Inject constructor(
|
||||||
private val secretKeyProvider: SecretKeyProvider,
|
private val secretKeyRepository: SecretKeyRepository,
|
||||||
private val encryptionDecryptionService: EncryptionDecryptionService,
|
private val encryptionDecryptionService: EncryptionDecryptionService,
|
||||||
private val pinCodeStore: PinCodeStore,
|
private val lockScreenStore: LockScreenStore,
|
||||||
) : PinCodeManager {
|
) : PinCodeManager {
|
||||||
|
|
||||||
private val callbacks = CopyOnWriteArrayList<PinCodeManager.Callback>()
|
private val callbacks = CopyOnWriteArrayList<PinCodeManager.Callback>()
|
||||||
|
|
@ -47,37 +47,37 @@ class DefaultPinCodeManager @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun isPinCodeAvailable(): Boolean {
|
override suspend fun isPinCodeAvailable(): Boolean {
|
||||||
return pinCodeStore.hasPinCode()
|
return lockScreenStore.hasPinCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getPinCodeSize(): Int {
|
override suspend fun getPinCodeSize(): Int {
|
||||||
val encryptedPinCode = pinCodeStore.getEncryptedCode() ?: return 0
|
val encryptedPinCode = lockScreenStore.getEncryptedCode() ?: return 0
|
||||||
val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS)
|
val secretKey = secretKeyRepository.getOrCreateKey(SECRET_KEY_ALIAS, false)
|
||||||
val decryptedPinCode = encryptionDecryptionService.decrypt(secretKey, EncryptionResult.fromBase64(encryptedPinCode))
|
val decryptedPinCode = encryptionDecryptionService.decrypt(secretKey, EncryptionResult.fromBase64(encryptedPinCode))
|
||||||
return decryptedPinCode.size
|
return decryptedPinCode.size
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun createPinCode(pinCode: String) {
|
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()
|
val encryptedPinCode = encryptionDecryptionService.encrypt(secretKey, pinCode.toByteArray()).toBase64()
|
||||||
pinCodeStore.saveEncryptedPinCode(encryptedPinCode)
|
lockScreenStore.saveEncryptedPinCode(encryptedPinCode)
|
||||||
callbacks.forEach { it.onPinCodeCreated() }
|
callbacks.forEach { it.onPinCodeCreated() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun verifyPinCode(pinCode: String): Boolean {
|
override suspend fun verifyPinCode(pinCode: String): Boolean {
|
||||||
val encryptedPinCode = pinCodeStore.getEncryptedCode() ?: return false
|
val encryptedPinCode = lockScreenStore.getEncryptedCode() ?: return false
|
||||||
return try {
|
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 decryptedPinCode = encryptionDecryptionService.decrypt(secretKey, EncryptionResult.fromBase64(encryptedPinCode))
|
||||||
val pinCodeToCheck = pinCode.toByteArray()
|
val pinCodeToCheck = pinCode.toByteArray()
|
||||||
decryptedPinCode.contentEquals(pinCodeToCheck).also { isPinCodeCorrect ->
|
decryptedPinCode.contentEquals(pinCodeToCheck).also { isPinCodeCorrect ->
|
||||||
if (isPinCodeCorrect) {
|
if (isPinCodeCorrect) {
|
||||||
pinCodeStore.resetCounter()
|
lockScreenStore.resetCounter()
|
||||||
callbacks.forEach { callback ->
|
callbacks.forEach { callback ->
|
||||||
callback.onPinCodeVerified()
|
callback.onPinCodeVerified()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
pinCodeStore.onWrongPin()
|
lockScreenStore.onWrongPin()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
|
|
@ -86,12 +86,12 @@ class DefaultPinCodeManager @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun deletePinCode() {
|
override suspend fun deletePinCode() {
|
||||||
pinCodeStore.deleteEncryptedPinCode()
|
lockScreenStore.deleteEncryptedPinCode()
|
||||||
pinCodeStore.resetCounter()
|
lockScreenStore.resetCounter()
|
||||||
callbacks.forEach { it.onPinCodeRemoved() }
|
callbacks.forEach { it.onPinCodeRemoved() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getRemainingPinCodeAttemptsNumber(): Int {
|
override suspend fun getRemainingPinCodeAttemptsNumber(): Int {
|
||||||
return pinCodeStore.getRemainingPinCodeAttemptsNumber()
|
return lockScreenStore.getRemainingPinCodeAttemptsNumber()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,5 +20,5 @@ sealed interface LockScreenSettingsEvents {
|
||||||
data object OnRemovePin : LockScreenSettingsEvents
|
data object OnRemovePin : LockScreenSettingsEvents
|
||||||
data object ConfirmRemovePin : LockScreenSettingsEvents
|
data object ConfirmRemovePin : LockScreenSettingsEvents
|
||||||
data object CancelRemovePin : LockScreenSettingsEvents
|
data object CancelRemovePin : LockScreenSettingsEvents
|
||||||
data object ToggleBiometric : LockScreenSettingsEvents
|
data object ToggleBiometricAllowed : LockScreenSettingsEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,11 @@ import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
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.DefaultPinCodeManagerCallback
|
||||||
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
|
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.features.lockscreen.impl.unlock.PinUnlockNode
|
||||||
import io.element.android.libraries.architecture.BackstackNode
|
import io.element.android.libraries.architecture.BackstackNode
|
||||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||||
|
|
@ -48,6 +50,7 @@ class LockScreenSettingsFlowNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
private val pinCodeManager: PinCodeManager,
|
private val pinCodeManager: PinCodeManager,
|
||||||
|
private val biometricUnlockManager: BiometricUnlockManager,
|
||||||
) : BackstackNode<LockScreenSettingsFlowNode.NavTarget>(
|
) : BackstackNode<LockScreenSettingsFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.Unknown,
|
initialElement = NavTarget.Unknown,
|
||||||
|
|
@ -76,15 +79,17 @@ class LockScreenSettingsFlowNode @AssistedInject constructor(
|
||||||
backstack.newRoot(NavTarget.Settings)
|
backstack.newRoot(NavTarget.Settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPinCodeCreated() {
|
|
||||||
backstack.newRoot(NavTarget.Settings)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPinCodeRemoved() {
|
override fun onPinCodeRemoved() {
|
||||||
navigateUp()
|
navigateUp()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val biometricUnlockCallback = object : DefaultBiometricUnlockCallback() {
|
||||||
|
override fun onBiometricUnlockSuccess() {
|
||||||
|
backstack.newRoot(NavTarget.Settings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
if (pinCodeManager.isPinCodeAvailable()) {
|
if (pinCodeManager.isPinCodeAvailable()) {
|
||||||
|
|
@ -96,9 +101,11 @@ class LockScreenSettingsFlowNode @AssistedInject constructor(
|
||||||
lifecycle.subscribe(
|
lifecycle.subscribe(
|
||||||
onCreate = {
|
onCreate = {
|
||||||
pinCodeManager.addCallback(pinCodeManagerCallback)
|
pinCodeManager.addCallback(pinCodeManagerCallback)
|
||||||
|
biometricUnlockManager.addCallback(biometricUnlockCallback)
|
||||||
},
|
},
|
||||||
onDestroy = {
|
onDestroy = {
|
||||||
pinCodeManager.removeCallback(pinCodeManagerCallback)
|
pinCodeManager.removeCallback(pinCodeManagerCallback)
|
||||||
|
biometricUnlockManager.removeCallback(biometricUnlockCallback)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -109,7 +116,12 @@ class LockScreenSettingsFlowNode @AssistedInject constructor(
|
||||||
createNode<PinUnlockNode>(buildContext)
|
createNode<PinUnlockNode>(buildContext)
|
||||||
}
|
}
|
||||||
NavTarget.Setup -> {
|
NavTarget.Setup -> {
|
||||||
createNode<SetupPinNode>(buildContext)
|
val callback = object : LockScreenSetupFlowNode.Callback {
|
||||||
|
override fun onSetupDone() {
|
||||||
|
backstack.newRoot(NavTarget.Settings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createNode<LockScreenSetupFlowNode>(buildContext, plugins = listOf(callback))
|
||||||
}
|
}
|
||||||
NavTarget.Settings -> {
|
NavTarget.Settings -> {
|
||||||
val callback = object : LockScreenSettingsNode.Callback {
|
val callback = object : LockScreenSettingsNode.Callback {
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,16 @@ package io.element.android.features.lockscreen.impl.settings
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import io.element.android.appconfig.LockScreenConfig
|
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.pin.PinCodeManager
|
||||||
|
import io.element.android.features.lockscreen.impl.storage.LockScreenStore
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -33,6 +36,8 @@ import javax.inject.Inject
|
||||||
class LockScreenSettingsPresenter @Inject constructor(
|
class LockScreenSettingsPresenter @Inject constructor(
|
||||||
private val lockScreenConfig: LockScreenConfig,
|
private val lockScreenConfig: LockScreenConfig,
|
||||||
private val pinCodeManager: PinCodeManager,
|
private val pinCodeManager: PinCodeManager,
|
||||||
|
private val lockScreenStore: LockScreenStore,
|
||||||
|
private val biometricUnlockManager: BiometricUnlockManager,
|
||||||
private val coroutineScope: CoroutineScope,
|
private val coroutineScope: CoroutineScope,
|
||||||
) : Presenter<LockScreenSettingsState> {
|
) : Presenter<LockScreenSettingsState> {
|
||||||
|
|
||||||
|
|
@ -44,14 +49,16 @@ class LockScreenSettingsPresenter @Inject constructor(
|
||||||
var showRemovePinOption by remember {
|
var showRemovePinOption by remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
var isBiometricEnabled by remember {
|
var showToggleBiometric by remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
val isBiometricEnabled by lockScreenStore.isBiometricUnlockAllowed().collectAsState(initial = false)
|
||||||
var showRemovePinConfirmation by remember {
|
var showRemovePinConfirmation by remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
LaunchedEffect(triggerComputation) {
|
LaunchedEffect(triggerComputation) {
|
||||||
showRemovePinOption = !lockScreenConfig.isPinMandatory && pinCodeManager.isPinCodeAvailable()
|
showRemovePinOption = !lockScreenConfig.isPinMandatory && pinCodeManager.isPinCodeAvailable()
|
||||||
|
showToggleBiometric = biometricUnlockManager.isDeviceSecured
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleEvents(event: LockScreenSettingsEvents) {
|
fun handleEvents(event: LockScreenSettingsEvents) {
|
||||||
|
|
@ -67,8 +74,10 @@ class LockScreenSettingsPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LockScreenSettingsEvents.OnRemovePin -> showRemovePinConfirmation = true
|
LockScreenSettingsEvents.OnRemovePin -> showRemovePinConfirmation = true
|
||||||
LockScreenSettingsEvents.ToggleBiometric -> {
|
LockScreenSettingsEvents.ToggleBiometricAllowed -> {
|
||||||
//TODO branch biometric logic
|
coroutineScope.launch {
|
||||||
|
lockScreenStore.setIsBiometricUnlockAllowed(!isBiometricEnabled)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -77,6 +86,7 @@ class LockScreenSettingsPresenter @Inject constructor(
|
||||||
showRemovePinOption = showRemovePinOption,
|
showRemovePinOption = showRemovePinOption,
|
||||||
isBiometricEnabled = isBiometricEnabled,
|
isBiometricEnabled = isBiometricEnabled,
|
||||||
showRemovePinConfirmation = showRemovePinConfirmation,
|
showRemovePinConfirmation = showRemovePinConfirmation,
|
||||||
|
showToggleBiometric = showToggleBiometric,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,5 +20,6 @@ data class LockScreenSettingsState(
|
||||||
val showRemovePinOption: Boolean,
|
val showRemovePinOption: Boolean,
|
||||||
val isBiometricEnabled: Boolean,
|
val isBiometricEnabled: Boolean,
|
||||||
val showRemovePinConfirmation: Boolean,
|
val showRemovePinConfirmation: Boolean,
|
||||||
|
val showToggleBiometric: Boolean,
|
||||||
val eventSink: (LockScreenSettingsEvents) -> Unit
|
val eventSink: (LockScreenSettingsEvents) -> Unit
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,11 @@ fun aLockScreenSettingsState(
|
||||||
isLockMandatory: Boolean = false,
|
isLockMandatory: Boolean = false,
|
||||||
isBiometricEnabled: Boolean = false,
|
isBiometricEnabled: Boolean = false,
|
||||||
showRemovePinConfirmation: Boolean = false,
|
showRemovePinConfirmation: Boolean = false,
|
||||||
|
showToggleBiometric: Boolean = true,
|
||||||
) = LockScreenSettingsState(
|
) = LockScreenSettingsState(
|
||||||
showRemovePinOption = isLockMandatory,
|
showRemovePinOption = isLockMandatory,
|
||||||
isBiometricEnabled = isBiometricEnabled,
|
isBiometricEnabled = isBiometricEnabled,
|
||||||
showRemovePinConfirmation = showRemovePinConfirmation,
|
showRemovePinConfirmation = showRemovePinConfirmation,
|
||||||
|
showToggleBiometric = showToggleBiometric,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -58,8 +58,16 @@ fun LockScreenSettingsView(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
PreferenceDivider()
|
if (state.showToggleBiometric) {
|
||||||
PreferenceSwitch(title = stringResource(id = R.string.screen_app_lock_settings_enable_biometric_unlock), isChecked = state.isBiometricEnabled)
|
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) {
|
if (state.showRemovePinConfirmation) {
|
||||||
|
|
|
||||||
|
|
@ -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<Plugin>,
|
||||||
|
private val pinCodeManager: PinCodeManager,
|
||||||
|
) : BackstackNode<LockScreenSetupFlowNode.NavTarget>(
|
||||||
|
backstack = BackStack(
|
||||||
|
initialElement = NavTarget.Pin,
|
||||||
|
savedStateMap = buildContext.savedStateMap,
|
||||||
|
),
|
||||||
|
buildContext = buildContext,
|
||||||
|
plugins = plugins,
|
||||||
|
) {
|
||||||
|
|
||||||
|
interface Callback : Plugin {
|
||||||
|
fun onSetupDone()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onSetupDone() {
|
||||||
|
plugins<Callback>().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<SetupPinNode>(buildContext)
|
||||||
|
}
|
||||||
|
NavTarget.Biometric -> {
|
||||||
|
val callback = object : SetupBiometricNode.Callback {
|
||||||
|
override fun onBiometricSetupDone() {
|
||||||
|
onSetupDone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createNode<SetupBiometricNode>(buildContext, plugins = listOf(callback))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
Children(
|
||||||
|
navModel = backstack,
|
||||||
|
modifier = modifier,
|
||||||
|
transitionHandler = rememberDefaultTransitionHandler(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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<Plugin>,
|
||||||
|
private val presenter: SetupBiometricPresenter,
|
||||||
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
|
||||||
|
interface Callback : Plugin {
|
||||||
|
fun onBiometricSetupDone()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onSetupDone() {
|
||||||
|
plugins<Callback>().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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<SetupBiometricState> {
|
||||||
|
|
||||||
|
@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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
@ -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<SetupBiometricState> {
|
||||||
|
override val values: Sequence<SetupBiometricState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aSetupBiometricState(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aSetupBiometricState(
|
||||||
|
isBiometricSetupDone: Boolean = false,
|
||||||
|
) = SetupBiometricState(
|
||||||
|
isBiometricSetupDone = isBiometricSetupDone,
|
||||||
|
eventSink = {}
|
||||||
|
)
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.lockscreen.impl.setup
|
package io.element.android.features.lockscreen.impl.setup.pin
|
||||||
|
|
||||||
sealed interface SetupPinEvents {
|
sealed interface SetupPinEvents {
|
||||||
data class OnPinEntryChanged(val entryAsText: String) : SetupPinEvents
|
data class OnPinEntryChanged(val entryAsText: String) : SetupPinEvents
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
|
@ -25,8 +25,8 @@ import androidx.compose.runtime.setValue
|
||||||
import io.element.android.appconfig.LockScreenConfig
|
import io.element.android.appconfig.LockScreenConfig
|
||||||
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
|
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.pin.model.PinEntry
|
||||||
import io.element.android.features.lockscreen.impl.setup.validation.PinValidator
|
import io.element.android.features.lockscreen.impl.setup.pin.validation.PinValidator
|
||||||
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.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
@ -14,10 +14,10 @@
|
||||||
* limitations under the License.
|
* 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.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(
|
data class SetupPinState(
|
||||||
val choosePinEntry: PinEntry,
|
val choosePinEntry: PinEntry,
|
||||||
|
|
@ -14,11 +14,11 @@
|
||||||
* limitations under the License.
|
* 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 androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
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<SetupPinState> {
|
open class SetupPinStateProvider : PreviewParameterProvider<SetupPinState> {
|
||||||
override val values: Sequence<SetupPinState>
|
override val values: Sequence<SetupPinState>
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
@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.Column
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
|
@ -40,7 +40,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.features.lockscreen.impl.R
|
import io.element.android.features.lockscreen.impl.R
|
||||||
import io.element.android.features.lockscreen.impl.components.PinEntryTextField
|
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.atomic.molecules.IconTitleSubtitleMolecule
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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.appconfig.LockScreenConfig
|
||||||
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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 {
|
sealed interface SetupPinFailure {
|
||||||
data object PinBlacklisted : SetupPinFailure
|
data object PinBlacklisted : SetupPinFailure
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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.
|
* Should be implemented by any class that provides access to the encrypted PIN code.
|
||||||
|
|
@ -14,9 +14,11 @@
|
||||||
* limitations under the License.
|
* 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.
|
* 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.
|
* Resets the counter of attempts for PIN code and biometric access.
|
||||||
*/
|
*/
|
||||||
suspend fun resetCounter()
|
suspend fun resetCounter()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the biometric unlock is allowed or not.
|
||||||
|
*/
|
||||||
|
fun isBiometricUnlockAllowed(): Flow<Boolean>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether the biometric unlock is allowed or not.
|
||||||
|
*/
|
||||||
|
suspend fun setIsBiometricUnlockAllowed(isAllowed: Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -14,11 +14,12 @@
|
||||||
* limitations under the License.
|
* 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 android.content.Context
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
import androidx.datastore.preferences.core.intPreferencesKey
|
import androidx.datastore.preferences.core.intPreferencesKey
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
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.AppScope
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
import io.element.android.libraries.di.SingleIn
|
import io.element.android.libraries.di.SingleIn
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -36,13 +38,14 @@ private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(na
|
||||||
|
|
||||||
@SingleIn(AppScope::class)
|
@SingleIn(AppScope::class)
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class PreferencesPinCodeStore @Inject constructor(
|
class PreferencesLockScreenStore @Inject constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val lockScreenConfig: LockScreenConfig,
|
private val lockScreenConfig: LockScreenConfig,
|
||||||
) : PinCodeStore {
|
) : LockScreenStore {
|
||||||
|
|
||||||
private val pinCodeKey = stringPreferencesKey("encoded_pin_code")
|
private val pinCodeKey = stringPreferencesKey("encoded_pin_code")
|
||||||
private val remainingAttemptsKey = intPreferencesKey("remaining_pin_code_attempts")
|
private val remainingAttemptsKey = intPreferencesKey("remaining_pin_code_attempts")
|
||||||
|
private val biometricUnlockKey = booleanPreferencesKey("biometric_unlock_enabled")
|
||||||
|
|
||||||
override suspend fun getRemainingPinCodeAttemptsNumber(): Int {
|
override suspend fun getRemainingPinCodeAttemptsNumber(): Int {
|
||||||
return context.dataStore.data.map { preferences ->
|
return context.dataStore.data.map { preferences ->
|
||||||
|
|
@ -88,5 +91,17 @@ class PreferencesPinCodeStore @Inject constructor(
|
||||||
}.first()
|
}.first()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun isBiometricUnlockAllowed(): Flow<Boolean> {
|
||||||
|
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
|
private fun Preferences.getRemainingPinCodeAttemptsNumber() = this[remainingAttemptsKey] ?: lockScreenConfig.maxPinCodeAttemptsBeforeLogout
|
||||||
}
|
}
|
||||||
|
|
@ -24,4 +24,5 @@ sealed interface PinUnlockEvents {
|
||||||
data object ClearSignOutPrompt : PinUnlockEvents
|
data object ClearSignOutPrompt : PinUnlockEvents
|
||||||
data object SignOut : PinUnlockEvents
|
data object SignOut : PinUnlockEvents
|
||||||
data object OnUseBiometric : PinUnlockEvents
|
data object OnUseBiometric : PinUnlockEvents
|
||||||
|
data object ClearBiometricError : PinUnlockEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
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.PinCodeManager
|
||||||
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
||||||
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
|
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
|
||||||
|
|
@ -38,6 +40,7 @@ import javax.inject.Inject
|
||||||
|
|
||||||
class PinUnlockPresenter @Inject constructor(
|
class PinUnlockPresenter @Inject constructor(
|
||||||
private val pinCodeManager: PinCodeManager,
|
private val pinCodeManager: PinCodeManager,
|
||||||
|
private val biometricUnlockManager: BiometricUnlockManager,
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
private val coroutineScope: CoroutineScope,
|
private val coroutineScope: CoroutineScope,
|
||||||
) : Presenter<PinUnlockState> {
|
) : Presenter<PinUnlockState> {
|
||||||
|
|
@ -60,6 +63,11 @@ class PinUnlockPresenter @Inject constructor(
|
||||||
val signOutAction = remember {
|
val signOutAction = remember {
|
||||||
mutableStateOf<Async<String?>>(Async.Uninitialized)
|
mutableStateOf<Async<String?>>(Async.Uninitialized)
|
||||||
}
|
}
|
||||||
|
var biometricUnlockResult by remember {
|
||||||
|
mutableStateOf<BiometricUnlock.AuthenticationResult?>(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
val biometricUnlock = biometricUnlockManager.rememberBiometricUnlock()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
suspend {
|
suspend {
|
||||||
|
|
@ -67,6 +75,10 @@ class PinUnlockPresenter @Inject constructor(
|
||||||
PinEntry.createEmpty(pinCodeSize)
|
PinEntry.createEmpty(pinCodeSize)
|
||||||
}.runCatchingUpdatingState(pinEntryState)
|
}.runCatchingUpdatingState(pinEntryState)
|
||||||
}
|
}
|
||||||
|
LaunchedEffect(biometricUnlock) {
|
||||||
|
biometricUnlock.setup()
|
||||||
|
biometricUnlock.authenticate()
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(pinEntry) {
|
LaunchedEffect(pinEntry) {
|
||||||
if (pinEntry.isComplete()) {
|
if (pinEntry.isComplete()) {
|
||||||
|
|
@ -97,7 +109,12 @@ class PinUnlockPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PinUnlockEvents.OnUseBiometric -> {
|
PinUnlockEvents.OnUseBiometric -> {
|
||||||
//TODO
|
coroutineScope.launch {
|
||||||
|
biometricUnlockResult = biometricUnlock.authenticate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PinUnlockEvents.ClearBiometricError -> {
|
||||||
|
biometricUnlockResult = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -107,6 +124,8 @@ class PinUnlockPresenter @Inject constructor(
|
||||||
remainingAttempts = remainingAttempts,
|
remainingAttempts = remainingAttempts,
|
||||||
showSignOutPrompt = showSignOutPrompt,
|
showSignOutPrompt = showSignOutPrompt,
|
||||||
signOutAction = signOutAction.value,
|
signOutAction = signOutAction.value,
|
||||||
|
showBiometricUnlock = biometricUnlock.isActive,
|
||||||
|
biometricUnlockResult = biometricUnlockResult,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
package io.element.android.features.lockscreen.impl.unlock
|
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.features.lockscreen.impl.pin.model.PinEntry
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
|
|
||||||
|
|
@ -25,10 +27,22 @@ data class PinUnlockState(
|
||||||
val remainingAttempts: Async<Int>,
|
val remainingAttempts: Async<Int>,
|
||||||
val showSignOutPrompt: Boolean,
|
val showSignOutPrompt: Boolean,
|
||||||
val signOutAction: Async<String?>,
|
val signOutAction: Async<String?>,
|
||||||
|
val showBiometricUnlock: Boolean,
|
||||||
|
val biometricUnlockResult: BiometricUnlock.AuthenticationResult?,
|
||||||
val eventSink: (PinUnlockEvents) -> Unit
|
val eventSink: (PinUnlockEvents) -> Unit
|
||||||
) {
|
) {
|
||||||
val isSignOutPromptCancellable = when (remainingAttempts) {
|
val isSignOutPromptCancellable = when (remainingAttempts) {
|
||||||
is Async.Success -> remainingAttempts.data > 0
|
is Async.Success -> remainingAttempts.data > 0
|
||||||
else -> true
|
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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.features.lockscreen.impl.unlock
|
package io.element.android.features.lockscreen.impl.unlock
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
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.features.lockscreen.impl.pin.model.PinEntry
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
|
|
||||||
|
|
@ -27,6 +28,7 @@ open class PinUnlockStateProvider : PreviewParameterProvider<PinUnlockState> {
|
||||||
aPinUnlockState(pinEntry = PinEntry.createEmpty(4).fillWith("12")),
|
aPinUnlockState(pinEntry = PinEntry.createEmpty(4).fillWith("12")),
|
||||||
aPinUnlockState(showWrongPinTitle = true),
|
aPinUnlockState(showWrongPinTitle = true),
|
||||||
aPinUnlockState(showSignOutPrompt = true),
|
aPinUnlockState(showSignOutPrompt = true),
|
||||||
|
aPinUnlockState(showBiometricUnlock = false),
|
||||||
aPinUnlockState(showSignOutPrompt = true, remainingAttempts = 0),
|
aPinUnlockState(showSignOutPrompt = true, remainingAttempts = 0),
|
||||||
aPinUnlockState(signOutAction = Async.Loading()),
|
aPinUnlockState(signOutAction = Async.Loading()),
|
||||||
)
|
)
|
||||||
|
|
@ -37,12 +39,16 @@ fun aPinUnlockState(
|
||||||
remainingAttempts: Int = 3,
|
remainingAttempts: Int = 3,
|
||||||
showWrongPinTitle: Boolean = false,
|
showWrongPinTitle: Boolean = false,
|
||||||
showSignOutPrompt: Boolean = false,
|
showSignOutPrompt: Boolean = false,
|
||||||
|
showBiometricUnlock: Boolean = true,
|
||||||
|
biometricUnlockResult: BiometricUnlock.AuthenticationResult? = null,
|
||||||
signOutAction: Async<String?> = Async.Uninitialized,
|
signOutAction: Async<String?> = Async.Uninitialized,
|
||||||
) = PinUnlockState(
|
) = PinUnlockState(
|
||||||
pinEntry = Async.Success(pinEntry),
|
pinEntry = Async.Success(pinEntry),
|
||||||
showWrongPinTitle = showWrongPinTitle,
|
showWrongPinTitle = showWrongPinTitle,
|
||||||
remainingAttempts = Async.Success(remainingAttempts),
|
remainingAttempts = Async.Success(remainingAttempts),
|
||||||
showSignOutPrompt = showSignOutPrompt,
|
showSignOutPrompt = showSignOutPrompt,
|
||||||
|
showBiometricUnlock = showBiometricUnlock,
|
||||||
signOutAction = signOutAction,
|
signOutAction = signOutAction,
|
||||||
|
biometricUnlockResult = biometricUnlockResult,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
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.R
|
||||||
import io.element.android.features.lockscreen.impl.pin.model.PinDigit
|
import io.element.android.features.lockscreen.impl.pin.model.PinDigit
|
||||||
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
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.Surface
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
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.theme.ElementTheme
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
|
@ -66,6 +68,12 @@ fun PinUnlockView(
|
||||||
state: PinUnlockState,
|
state: PinUnlockState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
OnLifecycleEvent { _, event ->
|
||||||
|
when (event) {
|
||||||
|
Lifecycle.Event.ON_RESUME -> state.eventSink.invoke(PinUnlockEvents.OnUseBiometric)
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
Surface(modifier) {
|
Surface(modifier) {
|
||||||
BoxWithConstraints {
|
BoxWithConstraints {
|
||||||
val commonModifier = Modifier
|
val commonModifier = Modifier
|
||||||
|
|
@ -82,6 +90,7 @@ fun PinUnlockView(
|
||||||
val footer = @Composable {
|
val footer = @Composable {
|
||||||
PinUnlockFooter(
|
PinUnlockFooter(
|
||||||
modifier = Modifier.padding(top = 24.dp),
|
modifier = Modifier.padding(top = 24.dp),
|
||||||
|
showBiometricUnlock = state.showBiometricUnlock,
|
||||||
onUseBiometric = {
|
onUseBiometric = {
|
||||||
state.eventSink(PinUnlockEvents.OnUseBiometric)
|
state.eventSink(PinUnlockEvents.OnUseBiometric)
|
||||||
},
|
},
|
||||||
|
|
@ -126,6 +135,12 @@ fun PinUnlockView(
|
||||||
if (state.signOutAction is Async.Loading) {
|
if (state.signOutAction is Async.Loading) {
|
||||||
ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content))
|
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
|
@Composable
|
||||||
private fun PinUnlockFooter(
|
private fun PinUnlockFooter(
|
||||||
|
showBiometricUnlock: Boolean,
|
||||||
onUseBiometric: () -> Unit,
|
onUseBiometric: () -> Unit,
|
||||||
onForgotPin: () -> Unit,
|
onForgotPin: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Row(modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround) {
|
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)
|
TextButton(text = stringResource(id = R.string.screen_app_lock_forgot_pin), onClick = onForgotPin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="screen_app_lock_biometric_authentication">"biometric authentication"</string>
|
<string name="screen_app_lock_biometric_authentication">"biometric authentication"</string>
|
||||||
<string name="screen_app_lock_biometric_unlock">"biometric unlock"</string>
|
<string name="screen_app_lock_biometric_unlock">"biometric unlock"</string>
|
||||||
|
<string name="screen_app_lock_biometric_unlock_title_android">"Unlock with biometric"</string>
|
||||||
<string name="screen_app_lock_forgot_pin">"Forgot PIN?"</string>
|
<string name="screen_app_lock_forgot_pin">"Forgot PIN?"</string>
|
||||||
<string name="screen_app_lock_settings_change_pin">"Change PIN code"</string>
|
<string name="screen_app_lock_settings_change_pin">"Change PIN code"</string>
|
||||||
<string name="screen_app_lock_settings_enable_biometric_unlock">"Allow biometric unlock"</string>
|
<string name="screen_app_lock_settings_enable_biometric_unlock">"Allow biometric unlock"</string>
|
||||||
|
|
@ -30,5 +31,7 @@ Choose something memorable. If you forget this PIN, you will be logged out of th
|
||||||
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PINs don\'t match"</string>
|
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PINs don\'t match"</string>
|
||||||
<string name="screen_app_lock_signout_alert_message">"You’ll need to re-login and create a new PIN to proceed"</string>
|
<string name="screen_app_lock_signout_alert_message">"You’ll need to re-login and create a new PIN to proceed"</string>
|
||||||
<string name="screen_app_lock_signout_alert_title">"You are being signed out"</string>
|
<string name="screen_app_lock_signout_alert_title">"You are being signed out"</string>
|
||||||
|
<string name="screen_app_lock_use_biometric_android">"Use biometric"</string>
|
||||||
|
<string name="screen_app_lock_use_pin_android">"Use PIN"</string>
|
||||||
<string name="screen_signout_in_progress_dialog_content">"Signing out…"</string>
|
<string name="screen_signout_in_progress_dialog_content">"Signing out…"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,19 +17,25 @@
|
||||||
package io.element.android.features.lockscreen.impl.fixtures
|
package io.element.android.features.lockscreen.impl.fixtures
|
||||||
|
|
||||||
import io.element.android.appconfig.LockScreenConfig
|
import io.element.android.appconfig.LockScreenConfig
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
internal fun aLockScreenConfig(
|
internal fun aLockScreenConfig(
|
||||||
isPinMandatory: Boolean = false,
|
isPinMandatory: Boolean = false,
|
||||||
pinBlacklist: Set<String> = emptySet(),
|
pinBlacklist: Set<String> = emptySet(),
|
||||||
pinSize: Int = 4,
|
pinSize: Int = 4,
|
||||||
maxPinCodeAttemptsBeforeLogout: Int = 3,
|
maxPinCodeAttemptsBeforeLogout: Int = 3,
|
||||||
gracePeriodInMillis: Long = 5 * 60 * 1000L
|
gracePeriod: Duration = 3.seconds,
|
||||||
|
isStrongBiometricsEnabled: Boolean = true,
|
||||||
|
isWeakBiometricsEnabled: Boolean = true,
|
||||||
): LockScreenConfig {
|
): LockScreenConfig {
|
||||||
return LockScreenConfig(
|
return LockScreenConfig(
|
||||||
isPinMandatory = isPinMandatory,
|
isPinMandatory = isPinMandatory,
|
||||||
pinBlacklist = pinBlacklist,
|
pinBlacklist = pinBlacklist,
|
||||||
pinSize = pinSize,
|
pinSize = pinSize,
|
||||||
maxPinCodeAttemptsBeforeLogout = maxPinCodeAttemptsBeforeLogout,
|
maxPinCodeAttemptsBeforeLogout = maxPinCodeAttemptsBeforeLogout,
|
||||||
gracePeriodInMillis = gracePeriodInMillis
|
gracePeriod = gracePeriod,
|
||||||
|
isStrongBiometricsEnabled = isStrongBiometricsEnabled,
|
||||||
|
isWeakBiometricsEnabled = isWeakBiometricsEnabled,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.DefaultPinCodeManager
|
||||||
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
|
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.InMemoryLockScreenStore
|
||||||
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.EncryptionDecryptionService
|
||||||
import io.element.android.libraries.cryptography.impl.AESEncryptionDecryptionService
|
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(
|
internal fun aPinCodeManager(
|
||||||
pinCodeStore: PinCodeStore = InMemoryPinCodeStore(),
|
lockScreenStore: LockScreenStore = InMemoryLockScreenStore(),
|
||||||
secretKeyProvider: SimpleSecretKeyProvider = SimpleSecretKeyProvider(),
|
secretKeyRepository: SimpleSecretKeyRepository = SimpleSecretKeyRepository(),
|
||||||
encryptionDecryptionService: EncryptionDecryptionService = AESEncryptionDecryptionService(),
|
encryptionDecryptionService: EncryptionDecryptionService = AESEncryptionDecryptionService(),
|
||||||
): PinCodeManager {
|
): PinCodeManager {
|
||||||
return DefaultPinCodeManager(secretKeyProvider, encryptionDecryptionService, pinCodeStore)
|
return DefaultPinCodeManager(secretKeyRepository, encryptionDecryptionService, lockScreenStore)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,18 +17,18 @@
|
||||||
package io.element.android.features.lockscreen.impl.pin
|
package io.element.android.features.lockscreen.impl.pin
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
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.impl.AESEncryptionDecryptionService
|
||||||
import io.element.android.libraries.cryptography.test.SimpleSecretKeyProvider
|
import io.element.android.libraries.cryptography.test.SimpleSecretKeyRepository
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class DefaultPinCodeManagerTest {
|
class DefaultPinCodeManagerTest {
|
||||||
|
|
||||||
private val pinCodeStore = InMemoryPinCodeStore()
|
private val lockScreenStore = InMemoryLockScreenStore()
|
||||||
private val secretKeyProvider = SimpleSecretKeyProvider()
|
private val secretKeyRepository = SimpleSecretKeyRepository()
|
||||||
private val encryptionDecryptionService = AESEncryptionDecryptionService()
|
private val encryptionDecryptionService = AESEncryptionDecryptionService()
|
||||||
private val pinCodeManager = DefaultPinCodeManager(secretKeyProvider, encryptionDecryptionService, pinCodeStore)
|
private val pinCodeManager = DefaultPinCodeManager(secretKeyRepository, encryptionDecryptionService, lockScreenStore)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given a pin code when create and delete assert no pin code left`() = runTest {
|
fun `given a pin code when create and delete assert no pin code left`() = runTest {
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,17 @@
|
||||||
|
|
||||||
package io.element.android.features.lockscreen.impl.pin.storage
|
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
|
private const val DEFAULT_REMAINING_ATTEMPTS = 3
|
||||||
|
|
||||||
class InMemoryPinCodeStore : PinCodeStore {
|
class InMemoryLockScreenStore : LockScreenStore {
|
||||||
|
|
||||||
private var pinCode: String? = null
|
private var pinCode: String? = null
|
||||||
private var remainingAttempts: Int = DEFAULT_REMAINING_ATTEMPTS
|
private var remainingAttempts: Int = DEFAULT_REMAINING_ATTEMPTS
|
||||||
|
private var isBiometricUnlockAllowed = MutableStateFlow(false)
|
||||||
|
|
||||||
override suspend fun getRemainingPinCodeAttemptsNumber(): Int {
|
override suspend fun getRemainingPinCodeAttemptsNumber(): Int {
|
||||||
return remainingAttempts
|
return remainingAttempts
|
||||||
|
|
@ -50,4 +55,12 @@ class InMemoryPinCodeStore : PinCodeStore {
|
||||||
override suspend fun hasPinCode(): Boolean {
|
override suspend fun hasPinCode(): Boolean {
|
||||||
return pinCode != null
|
return pinCode != null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun isBiometricUnlockAllowed(): Flow<Boolean> {
|
||||||
|
return isBiometricUnlockAllowed
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun setIsBiometricUnlockAllowed(isAllowed: Boolean) {
|
||||||
|
isBiometricUnlockAllowed.value = isAllowed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -21,8 +21,10 @@ import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.appconfig.LockScreenConfig
|
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.aLockScreenConfig
|
||||||
import io.element.android.features.lockscreen.impl.fixtures.aPinCodeManager
|
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.awaitLastSequentialItem
|
||||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -67,13 +69,16 @@ class LockScreenSettingsPresenterTest {
|
||||||
coroutineScope: CoroutineScope,
|
coroutineScope: CoroutineScope,
|
||||||
lockScreenConfig: LockScreenConfig = aLockScreenConfig(),
|
lockScreenConfig: LockScreenConfig = aLockScreenConfig(),
|
||||||
): LockScreenSettingsPresenter {
|
): LockScreenSettingsPresenter {
|
||||||
val pinCodeManager = aPinCodeManager().apply {
|
val lockScreenStore = InMemoryLockScreenStore()
|
||||||
|
val pinCodeManager = aPinCodeManager(lockScreenStore = lockScreenStore).apply {
|
||||||
createPinCode("1234")
|
createPinCode("1234")
|
||||||
}
|
}
|
||||||
return LockScreenSettingsPresenter(
|
return LockScreenSettingsPresenter(
|
||||||
|
lockScreenStore = lockScreenStore,
|
||||||
pinCodeManager = pinCodeManager,
|
pinCodeManager = pinCodeManager,
|
||||||
coroutineScope = coroutineScope,
|
coroutineScope = coroutineScope,
|
||||||
lockScreenConfig = lockScreenConfig,
|
lockScreenConfig = lockScreenConfig,
|
||||||
|
biometricUnlockManager = FakeBiometricUnlockManager(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
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.PinCodeManager
|
||||||
import io.element.android.features.lockscreen.impl.pin.model.assertEmpty
|
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.pin.model.assertText
|
||||||
import io.element.android.features.lockscreen.impl.setup.validation.PinValidator
|
import io.element.android.features.lockscreen.impl.setup.pin.validation.PinValidator
|
||||||
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.matrix.test.core.aBuildMeta
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||||
|
|
@ -20,6 +20,8 @@ import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
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.fixtures.aPinCodeManager
|
||||||
import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback
|
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.pin.PinCodeManager
|
||||||
|
|
@ -48,7 +50,7 @@ class PinUnlockPresenterTest {
|
||||||
pinCodeVerified.complete(Unit)
|
pinCodeVerified.complete(Unit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val presenter = createPinUnlockPresenter(this, callback)
|
val presenter = createPinUnlockPresenter(this, callback = callback)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -88,7 +90,7 @@ class PinUnlockPresenterTest {
|
||||||
pinCodeVerified.complete(Unit)
|
pinCodeVerified.complete(Unit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val presenter = createPinUnlockPresenter(this, callback)
|
val presenter = createPinUnlockPresenter(this, callback = callback)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -146,6 +148,7 @@ class PinUnlockPresenterTest {
|
||||||
|
|
||||||
private suspend fun createPinUnlockPresenter(
|
private suspend fun createPinUnlockPresenter(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
|
biometricUnlockManager: BiometricUnlockManager = FakeBiometricUnlockManager(),
|
||||||
callback: PinCodeManager.Callback = DefaultPinCodeManagerCallback(),
|
callback: PinCodeManager.Callback = DefaultPinCodeManagerCallback(),
|
||||||
): PinUnlockPresenter {
|
): PinUnlockPresenter {
|
||||||
val pinCodeManager = aPinCodeManager().apply {
|
val pinCodeManager = aPinCodeManager().apply {
|
||||||
|
|
@ -153,9 +156,10 @@ class PinUnlockPresenterTest {
|
||||||
createPinCode(completePin)
|
createPinCode(completePin)
|
||||||
}
|
}
|
||||||
return PinUnlockPresenter(
|
return PinUnlockPresenter(
|
||||||
pinCodeManager,
|
pinCodeManager = pinCodeManager,
|
||||||
FakeMatrixClient(),
|
biometricUnlockManager = biometricUnlockManager,
|
||||||
scope,
|
matrixClient = FakeMatrixClient(),
|
||||||
|
coroutineScope = scope,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ datastore = "1.0.0"
|
||||||
constraintlayout = "2.1.4"
|
constraintlayout = "2.1.4"
|
||||||
constraintlayout_compose = "1.0.1"
|
constraintlayout_compose = "1.0.1"
|
||||||
recyclerview = "1.3.2"
|
recyclerview = "1.3.2"
|
||||||
lifecycle = "2.6.2"
|
lifecycle = "2.7.0-alpha03"
|
||||||
activity = "1.8.0"
|
activity = "1.8.0"
|
||||||
startup = "1.1.1"
|
startup = "1.1.1"
|
||||||
media3 = "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_security_crypto = "androidx.security:security-crypto:1.1.0-alpha06"
|
||||||
androidx_media3_exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
|
androidx_media3_exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
|
||||||
androidx_media3_ui = { module = "androidx.media3:media3-ui", 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_activity = { module = "androidx.activity:activity", version.ref = "activity" }
|
||||||
androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity" }
|
androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity" }
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,28 +21,27 @@ import android.security.keystore.KeyGenParameterSpec
|
||||||
import android.security.keystore.KeyProperties
|
import android.security.keystore.KeyProperties
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.libraries.cryptography.api.AESEncryptionSpecs
|
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 io.element.android.libraries.di.AppScope
|
||||||
|
import timber.log.Timber
|
||||||
import java.security.KeyStore
|
import java.security.KeyStore
|
||||||
|
import java.security.KeyStoreException
|
||||||
import javax.crypto.KeyGenerator
|
import javax.crypto.KeyGenerator
|
||||||
import javax.crypto.SecretKey
|
import javax.crypto.SecretKey
|
||||||
import javax.inject.Inject
|
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.
|
* The generated key uses AES algorithm, with a key size of 128 bits, and the GCM block mode.
|
||||||
*/
|
*/
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class KeyStoreSecretKeyProvider @Inject constructor() : SecretKeyProvider {
|
class KeyStoreSecretKeyRepository @Inject constructor(
|
||||||
|
private val keyStore: KeyStore,
|
||||||
|
) : SecretKeyRepository {
|
||||||
|
|
||||||
// False positive lint issue
|
// False positive lint issue
|
||||||
@SuppressLint("WrongConstant")
|
@SuppressLint("WrongConstant")
|
||||||
override fun getOrCreateKey(alias: String): SecretKey {
|
override fun getOrCreateKey(alias: String, requiresUserAuthentication: Boolean): SecretKey {
|
||||||
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply {
|
|
||||||
load(null)
|
|
||||||
}
|
|
||||||
val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry)
|
val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry)
|
||||||
?.secretKey
|
?.secretKey
|
||||||
return if (secretKeyEntry == null) {
|
return if (secretKeyEntry == null) {
|
||||||
|
|
@ -54,6 +53,7 @@ class KeyStoreSecretKeyProvider @Inject constructor() : SecretKeyProvider {
|
||||||
.setBlockModes(AESEncryptionSpecs.BLOCK_MODE)
|
.setBlockModes(AESEncryptionSpecs.BLOCK_MODE)
|
||||||
.setEncryptionPaddings(AESEncryptionSpecs.PADDINGS)
|
.setEncryptionPaddings(AESEncryptionSpecs.PADDINGS)
|
||||||
.setKeySize(AESEncryptionSpecs.KEY_SIZE)
|
.setKeySize(AESEncryptionSpecs.KEY_SIZE)
|
||||||
|
.setUserAuthenticationRequired(requiresUserAuthentication)
|
||||||
.build()
|
.build()
|
||||||
generator.init(keyGenSpec)
|
generator.init(keyGenSpec)
|
||||||
generator.generateKey()
|
generator.generateKey()
|
||||||
|
|
@ -61,4 +61,12 @@ class KeyStoreSecretKeyProvider @Inject constructor() : SecretKeyProvider {
|
||||||
secretKeyEntry
|
secretKeyEntry
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun deleteKey(alias: String) {
|
||||||
|
try {
|
||||||
|
keyStore.deleteEntry(alias)
|
||||||
|
} catch (e: KeyStoreException) {
|
||||||
|
Timber.e(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -17,20 +17,24 @@
|
||||||
package io.element.android.libraries.cryptography.test
|
package io.element.android.libraries.cryptography.test
|
||||||
|
|
||||||
import io.element.android.libraries.cryptography.api.AESEncryptionSpecs
|
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.KeyGenerator
|
||||||
import javax.crypto.SecretKey
|
import javax.crypto.SecretKey
|
||||||
|
|
||||||
class SimpleSecretKeyProvider : SecretKeyProvider {
|
class SimpleSecretKeyRepository : SecretKeyRepository {
|
||||||
|
|
||||||
private var secretKeyForAlias = HashMap<String, SecretKey>()
|
private var secretKeyForAlias = HashMap<String, SecretKey>()
|
||||||
|
|
||||||
override fun getOrCreateKey(alias: String): SecretKey {
|
override fun getOrCreateKey(alias: String, requiresUserAuthentication: Boolean): SecretKey {
|
||||||
return secretKeyForAlias.getOrPut(alias) {
|
return secretKeyForAlias.getOrPut(alias) {
|
||||||
generateKey()
|
generateKey()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun deleteKey(alias: String) {
|
||||||
|
secretKeyForAlias.remove(alias)
|
||||||
|
}
|
||||||
|
|
||||||
private fun generateKey(): SecretKey {
|
private fun generateKey(): SecretKey {
|
||||||
val keyGenerator = KeyGenerator.getInstance(AESEncryptionSpecs.ALGORITHM)
|
val keyGenerator = KeyGenerator.getInstance(AESEncryptionSpecs.ALGORITHM)
|
||||||
keyGenerator.init(AESEncryptionSpecs.KEY_SIZE)
|
keyGenerator.init(AESEncryptionSpecs.KEY_SIZE)
|
||||||
|
|
@ -218,6 +218,7 @@
|
||||||
<string name="room_timeline_beginning_of_room">"This is the beginning of %1$s."</string>
|
<string name="room_timeline_beginning_of_room">"This is the beginning of %1$s."</string>
|
||||||
<string name="room_timeline_beginning_of_room_no_name">"This is the beginning of this conversation."</string>
|
<string name="room_timeline_beginning_of_room_no_name">"This is the beginning of this conversation."</string>
|
||||||
<string name="room_timeline_read_marker_title">"New"</string>
|
<string name="room_timeline_read_marker_title">"New"</string>
|
||||||
|
<string name="screen_room_mentions_at_room_subtitle">"Notify the whole room"</string>
|
||||||
<string name="screen_analytics_settings_share_data">"Share analytics data"</string>
|
<string name="screen_analytics_settings_share_data">"Share analytics data"</string>
|
||||||
<string name="screen_media_picker_error_failed_selection">"Failed selecting media, please try again."</string>
|
<string name="screen_media_picker_error_failed_selection">"Failed selecting media, please try again."</string>
|
||||||
<string name="screen_media_upload_preview_error_failed_processing">"Failed processing media to upload, please try again."</string>
|
<string name="screen_media_upload_preview_error_failed_processing">"Failed processing media to upload, please try again."</string>
|
||||||
|
|
@ -262,6 +263,7 @@ If you proceed, some of your settings may change."</string>
|
||||||
<string name="test_untranslated_default_language_identifier">"en"</string>
|
<string name="test_untranslated_default_language_identifier">"en"</string>
|
||||||
<string name="dialog_title_error">"Error"</string>
|
<string name="dialog_title_error">"Error"</string>
|
||||||
<string name="dialog_title_success">"Success"</string>
|
<string name="dialog_title_success">"Success"</string>
|
||||||
|
<string name="screen_room_mentions_at_room_title">"Everyone"</string>
|
||||||
<string name="screen_analytics_settings_help_us_improve">"Share anonymous usage data to help us identify issues."</string>
|
<string name="screen_analytics_settings_help_us_improve">"Share anonymous usage data to help us identify issues."</string>
|
||||||
<string name="screen_analytics_settings_read_terms">"You can read all our terms %1$s."</string>
|
<string name="screen_analytics_settings_read_terms">"You can read all our terms %1$s."</string>
|
||||||
<string name="screen_analytics_settings_read_terms_content_link">"here"</string>
|
<string name="screen_analytics_settings_read_terms_content_link">"here"</string>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:fb7cf95951b4cae20445929a7b0ce7d3fb4cb1ff0d6ce735e890106754485856
|
||||||
|
size 37651
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:38e9a9481637e9fafbc2bf4083b1024e735bd40f6b4bdcda4f50b403df475e96
|
||||||
|
size 35595
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:1facbdcba634b37a8162876e6af02a3565eaee2ac4afe32816b1f8e9602b2a65
|
||||||
|
size 36565
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a4b3e856f102b2d53fd5776204a8576ed515e9e4bfd9899e4138eb1515a11b12
|
||||||
|
size 34241
|
||||||
Loading…
Add table
Add a link
Reference in a new issue