First Commit

This commit is contained in:
2025-12-18 16:28:50 +07:00
commit 8c3e4f491f
9974 changed files with 396488 additions and 0 deletions
+58
View File
@@ -0,0 +1,58 @@
import extension.setupDependencyInjection
import extension.testCommonDependencies
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
id("io.element.android-compose-library")
id("kotlin-parcelize")
}
android {
namespace = "io.element.android.features.lockscreen.impl"
testOptions {
unitTests.isIncludeAndroidResources = true
}
}
setupDependencyInjection()
dependencies {
api(projects.features.lockscreen.api)
implementation(projects.appconfig)
implementation(projects.features.enterprise.api)
implementation(projects.libraries.core)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.cryptography.api)
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.testtags)
implementation(projects.libraries.uiUtils)
implementation(projects.features.logout.api)
implementation(projects.libraries.uiCommon)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.sessionStorage.api)
implementation(projects.services.appnavstate.api)
implementation(libs.androidx.datastore.preferences)
implementation(libs.androidx.biometric)
testCommonDependencies(libs, true)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.cryptography.test)
testImplementation(projects.libraries.cryptography.impl)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.sessionStorage.test)
testImplementation(projects.services.appnavstate.test)
testImplementation(projects.features.logout.test)
}
@@ -0,0 +1,16 @@
<!--
~ Copyright (c) 2025 Element Creations Ltd.
~ Copyright 2024 New Vector Ltd.
~
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
~ Please see LICENSE files in the repository root for full details.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name="io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity"
android:configChanges="screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode"/>
</application>
</manifest>
@@ -0,0 +1,46 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl
import android.content.Context
import android.content.Intent
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity
import io.element.android.libraries.architecture.createNode
@ContributesBinding(AppScope::class)
class DefaultLockScreenEntryPoint : LockScreenEntryPoint {
override fun createNode(
parentNode: Node,
buildContext: BuildContext,
navTarget: LockScreenEntryPoint.Target,
callback: LockScreenEntryPoint.Callback,
): Node {
return parentNode.createNode<LockScreenFlowNode>(
buildContext = buildContext,
plugins = listOf(
LockScreenFlowNode.Inputs(
when (navTarget) {
LockScreenEntryPoint.Target.Setup -> LockScreenFlowNode.NavTarget.Setup
LockScreenEntryPoint.Target.Settings -> LockScreenFlowNode.NavTarget.Settings
}
),
callback,
)
)
}
override fun pinUnlockIntent(context: Context): Intent {
return PinUnlockActivity.newIntent(context)
}
}
@@ -0,0 +1,121 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn
import io.element.android.features.lockscreen.api.LockScreenLockState
import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager
import io.element.android.features.lockscreen.impl.biometric.DefaultBiometricUnlockCallback
import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
import io.element.android.features.lockscreen.impl.storage.LockScreenStore
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.sessionstorage.api.observer.SessionListener
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
import io.element.android.services.appnavstate.api.AppForegroundStateService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlin.time.Duration
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class DefaultLockScreenService(
private val lockScreenConfig: LockScreenConfig,
private val lockScreenStore: LockScreenStore,
private val pinCodeManager: PinCodeManager,
@AppCoroutineScope
private val coroutineScope: CoroutineScope,
private val sessionObserver: SessionObserver,
private val appForegroundStateService: AppForegroundStateService,
biometricAuthenticatorManager: BiometricAuthenticatorManager,
) : LockScreenService {
private val _lockState = MutableStateFlow<LockScreenLockState>(LockScreenLockState.Unlocked)
override val lockState: StateFlow<LockScreenLockState> = _lockState
private var lockJob: Job? = null
init {
pinCodeManager.addCallback(object : DefaultPinCodeManagerCallback() {
override fun onPinCodeVerified() {
_lockState.value = LockScreenLockState.Unlocked
}
override fun onPinCodeRemoved() {
_lockState.value = LockScreenLockState.Unlocked
}
})
biometricAuthenticatorManager.addCallback(object : DefaultBiometricUnlockCallback() {
override fun onBiometricAuthenticationSuccess() {
_lockState.value = LockScreenLockState.Unlocked
coroutineScope.launch {
lockScreenStore.resetCounter()
}
}
})
coroutineScope.lockIfNeeded()
observeAppForegroundState()
observeSessionsState()
}
/**
* Makes sure to delete the pin code when the last session is deleted.
*/
private fun observeSessionsState() {
sessionObserver.addListener(object : SessionListener {
override suspend fun onSessionDeleted(userId: String, wasLastSession: Boolean) {
if (wasLastSession) {
pinCodeManager.deletePinCode()
}
}
})
}
/**
* Makes sure to lock the app if it goes in background for a certain amount of time.
*/
private fun observeAppForegroundState() {
coroutineScope.launch {
appForegroundStateService.startObservingForeground()
appForegroundStateService.isInForeground.collect { isInForeground ->
if (isInForeground) {
lockJob?.cancel()
} else {
lockJob = lockIfNeeded(gracePeriod = lockScreenConfig.gracePeriod)
}
}
}
}
override fun isPinSetup(): Flow<Boolean> {
return pinCodeManager.hasPinCode()
}
override fun isSetupRequired(): Flow<Boolean> {
return isPinSetup().map { isPinSetup ->
!isPinSetup && lockScreenConfig.isPinMandatory
}
}
private fun CoroutineScope.lockIfNeeded(gracePeriod: Duration = Duration.ZERO) = launch {
if (isPinSetup().first()) {
delay(gracePeriod)
_lockState.value = LockScreenLockState.Locked
}
}
}
@@ -0,0 +1,41 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.BindingContainer
import dev.zacsweers.metro.ContributesTo
import dev.zacsweers.metro.Provides
import kotlin.time.Duration
import io.element.android.appconfig.LockScreenConfig as AppConfigLockScreenConfig
data class LockScreenConfig(
val isPinMandatory: Boolean,
val forbiddenPinCodes: Set<String>,
val pinSize: Int,
val maxPinCodeAttemptsBeforeLogout: Int,
val gracePeriod: Duration,
val isStrongBiometricsEnabled: Boolean,
val isWeakBiometricsEnabled: Boolean,
)
@ContributesTo(AppScope::class)
@BindingContainer
object LockScreenConfigModule {
@Provides
fun providesLockScreenConfig(): LockScreenConfig = LockScreenConfig(
isPinMandatory = AppConfigLockScreenConfig.IS_PIN_MANDATORY,
forbiddenPinCodes = AppConfigLockScreenConfig.FORBIDDEN_PIN_CODES,
pinSize = AppConfigLockScreenConfig.PIN_SIZE,
maxPinCodeAttemptsBeforeLogout = AppConfigLockScreenConfig.MAX_PIN_CODE_ATTEMPTS_BEFORE_LOGOUT,
gracePeriod = AppConfigLockScreenConfig.GRACE_PERIOD,
isStrongBiometricsEnabled = AppConfigLockScreenConfig.IS_STRONG_BIOMETRICS_ENABLED,
isWeakBiometricsEnabled = AppConfigLockScreenConfig.IS_WEAK_BIOMETRICS_ENABLED,
)
}
@@ -0,0 +1,81 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl
import android.os.Parcelable
import androidx.compose.runtime.Composable
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 com.bumble.appyx.navmodel.backstack.BackStack
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.features.lockscreen.impl.settings.LockScreenSettingsFlowNode
import io.element.android.features.lockscreen.impl.setup.LockScreenSetupFlowNode
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.SessionScope
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
@AssistedInject
class LockScreenFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
) : BaseFlowNode<LockScreenFlowNode.NavTarget>(
backstack = BackStack(
initialElement = plugins.filterIsInstance<Inputs>().first().initialNavTarget,
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
plugins = plugins,
) {
data class Inputs(
val initialNavTarget: NavTarget,
) : NodeInputs
sealed interface NavTarget : Parcelable {
@Parcelize
data object Setup : NavTarget
@Parcelize
data object Settings : NavTarget
}
private class OnSetupDoneCallback(private val plugins: List<LockScreenEntryPoint.Callback>) : LockScreenSetupFlowNode.Callback {
override fun onSetupDone() {
plugins.forEach {
it.onSetupDone()
}
}
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
NavTarget.Setup -> {
val callback = OnSetupDoneCallback(plugins())
createNode<LockScreenSetupFlowNode>(buildContext, plugins = listOf(callback))
}
NavTarget.Settings -> {
createNode<LockScreenSettingsFlowNode>(buildContext)
}
}
}
@Composable
override fun View(modifier: Modifier) {
BackstackView()
}
}
@@ -0,0 +1,128 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
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.core.extensions.runCatchingExceptions
import io.element.android.libraries.cryptography.api.EncryptionDecryptionService
import io.element.android.libraries.cryptography.api.SecretKeyRepository
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import timber.log.Timber
import java.security.InvalidKeyException
import javax.crypto.Cipher
interface BiometricAuthenticator {
interface Callback {
fun onBiometricSetupError()
fun onBiometricAuthenticationSuccess()
fun onBiometricAuthenticationFailed(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 NoopBiometricAuthentication : BiometricAuthenticator {
override val isActive: Boolean = false
override fun setup() = Unit
override suspend fun authenticate() = BiometricAuthenticator.AuthenticationResult.Failure()
}
class DefaultBiometricAuthentication(
private val activity: FragmentActivity,
private val promptInfo: PromptInfo,
private val secretKeyRepository: SecretKeyRepository,
private val encryptionDecryptionService: EncryptionDecryptionService,
private val keyAlias: String,
private val callbacks: List<BiometricAuthenticator.Callback>
) : BiometricAuthenticator {
override val isActive: Boolean = true
private var cryptoObject: CryptoObject? = null
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(): BiometricAuthenticator.AuthenticationResult {
val cryptoObject = cryptoObject ?: return BiometricAuthenticator.AuthenticationResult.Failure()
val deferredAuthenticationResult = CompletableDeferred<BiometricAuthenticator.AuthenticationResult>()
val executor = ContextCompat.getMainExecutor(activity.baseContext)
val callback = AuthenticationCallback(callbacks, deferredAuthenticationResult)
val prompt = BiometricPrompt(activity, executor, callback)
prompt.authenticate(promptInfo, cryptoObject)
return try {
deferredAuthenticationResult.await()
} catch (cancellation: CancellationException) {
prompt.cancelAuthentication()
BiometricAuthenticator.AuthenticationResult.Failure(cancellation)
}
}
@Throws(KeyPermanentlyInvalidatedException::class)
private fun ensureKey() = secretKeyRepository.getOrCreateKey(keyAlias, true).also {
encryptionDecryptionService.createEncryptionCipher(it)
}
}
private class AuthenticationCallback(
private val callbacks: List<BiometricAuthenticator.Callback>,
private val deferredAuthenticationResult: CompletableDeferred<BiometricAuthenticator.AuthenticationResult>,
) : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
val biometricUnlockError = BiometricUnlockError(errorCode, errString.toString())
callbacks.forEach { it.onBiometricAuthenticationFailed(biometricUnlockError) }
deferredAuthenticationResult.complete(BiometricAuthenticator.AuthenticationResult.Failure(biometricUnlockError))
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
callbacks.forEach { it.onBiometricAuthenticationFailed(null) }
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
if (result.cryptoObject?.cipher.isValid()) {
callbacks.forEach { it.onBiometricAuthenticationSuccess() }
deferredAuthenticationResult.complete(BiometricAuthenticator.AuthenticationResult.Success)
} else {
val error = IllegalStateException("Invalid cipher")
callbacks.forEach { it.onBiometricAuthenticationFailed(error) }
deferredAuthenticationResult.complete(BiometricAuthenticator.AuthenticationResult.Failure())
}
}
private fun Cipher?.isValid(): Boolean {
if (this == null) return false
return runCatchingExceptions {
doFinal("biometric_challenge".toByteArray())
}.isSuccess
}
}
@@ -0,0 +1,38 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.biometric
import androidx.compose.runtime.Composable
interface BiometricAuthenticatorManager {
/**
* 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: BiometricAuthenticator.Callback)
fun removeCallback(callback: BiometricAuthenticator.Callback)
/**
* Remember a biometric authenticator ready for unlocking the app.
*/
@Composable
fun rememberUnlockBiometricAuthenticator(): BiometricAuthenticator
/**
* Remember a biometric authenticator ready for confirmation.
*/
@Composable
fun rememberConfirmBiometricAuthenticator(): BiometricAuthenticator
}
@@ -0,0 +1,30 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
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,169 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
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.res.stringResource
import androidx.core.content.getSystemService
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.compose.LocalLifecycleOwner
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn
import io.element.android.features.lockscreen.impl.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.annotations.AppCoroutineScope
import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.util.concurrent.CopyOnWriteArrayList
private const val SECRET_KEY_ALIAS = "elementx.SECRET_KEY_ALIAS_BIOMETRIC"
@ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)
class DefaultBiometricAuthenticatorManager(
@ApplicationContext private val context: Context,
private val lockScreenStore: LockScreenStore,
private val lockScreenConfig: LockScreenConfig,
private val encryptionDecryptionService: EncryptionDecryptionService,
private val secretKeyRepository: SecretKeyRepository,
@AppCoroutineScope
private val coroutineScope: CoroutineScope,
) : BiometricAuthenticatorManager {
private val callbacks = CopyOnWriteArrayList<BiometricAuthenticator.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 rememberUnlockBiometricAuthenticator(): BiometricAuthenticator {
val isBiometricAllowed by remember {
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)
return rememberBiometricAuthenticator(
isAvailable = isAvailable,
promptTitle = promptTitle,
promptNegative = promptNegative,
)
}
@Composable
override fun rememberConfirmBiometricAuthenticator(): BiometricAuthenticator {
val lifecycleState by LocalLifecycleOwner.current.lifecycle.currentStateFlow.collectAsState()
val isAvailable by remember(lifecycleState) {
derivedStateOf { hasAvailableAuthenticator }
}
val promptTitle = stringResource(id = R.string.screen_app_lock_confirm_biometric_authentication_android)
val promptNegative = stringResource(id = CommonStrings.action_cancel)
return rememberBiometricAuthenticator(
isAvailable = isAvailable,
promptTitle = promptTitle,
promptNegative = promptNegative,
)
}
@Composable
private fun rememberBiometricAuthenticator(
isAvailable: Boolean,
promptTitle: String,
promptNegative: String,
): BiometricAuthenticator {
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()
DefaultBiometricAuthentication(
activity = activity,
promptInfo = promptInfo,
secretKeyRepository = secretKeyRepository,
encryptionDecryptionService = encryptionDecryptionService,
keyAlias = SECRET_KEY_ALIAS,
callbacks = callbacks + internalCallback
)
} else {
NoopBiometricAuthentication()
}
}
}
override fun addCallback(callback: BiometricAuthenticator.Callback) {
callbacks.add(callback)
}
override fun removeCallback(callback: BiometricAuthenticator.Callback) {
callbacks.remove(callback)
}
private fun Context.findFragmentActivity(): FragmentActivity? = when (this) {
is FragmentActivity -> this
is ContextWrapper -> baseContext.findFragmentActivity()
else -> null
}
}
@@ -0,0 +1,15 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.biometric
open class DefaultBiometricUnlockCallback : BiometricAuthenticator.Callback {
override fun onBiometricSetupError() = Unit
override fun onBiometricAuthenticationSuccess() = Unit
override fun onBiometricAuthenticationFailed(error: Exception?) = Unit
}
@@ -0,0 +1,125 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.components
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.lockscreen.impl.pin.model.PinDigit
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.pinDigitBg
@Composable
fun PinEntryTextField(
pinEntry: PinEntry,
isSecured: Boolean,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
) {
BasicTextField(
modifier = modifier,
value = pinEntry.toText(),
onValueChange = {
onValueChange(it)
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
decorationBox = {
PinEntryRow(pinEntry = pinEntry, isSecured = isSecured)
}
)
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun PinEntryRow(
pinEntry: PinEntry,
isSecured: Boolean,
) {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
for (digit in pinEntry.digits) {
PinDigitView(digit = digit, isSecured = isSecured)
}
}
}
@Composable
private fun PinDigitView(
digit: PinDigit,
isSecured: Boolean,
) {
val shape = RoundedCornerShape(8.dp)
val appearanceModifier = when (digit) {
PinDigit.Empty -> {
Modifier.border(1.dp, ElementTheme.colors.iconPrimary, shape)
}
is PinDigit.Filled -> {
Modifier.background(ElementTheme.colors.pinDigitBg, shape)
}
}
Box(
modifier = Modifier
.size(48.dp)
.then(appearanceModifier),
contentAlignment = Alignment.Center,
) {
if (digit is PinDigit.Filled) {
val text = if (isSecured) {
""
} else {
digit.value.toString()
}
Text(
text = text,
style = ElementTheme.typography.fontHeadingMdBold
)
}
}
}
@PreviewsDayNight
@Composable
internal fun PinEntryTextFieldPreview() {
ElementPreview {
val pinEntry = PinEntry.createEmpty(4).fillWith("12")
Column {
PinEntryTextField(
pinEntry = pinEntry,
isSecured = true,
onValueChange = {},
)
Spacer(modifier = Modifier.size(16.dp))
PinEntryTextField(
pinEntry = pinEntry,
isSecured = false,
onValueChange = {},
)
}
}
}
@@ -0,0 +1,88 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.pin
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn
import io.element.android.features.lockscreen.impl.storage.LockScreenStore
import io.element.android.libraries.cryptography.api.EncryptionDecryptionService
import io.element.android.libraries.cryptography.api.EncryptionResult
import io.element.android.libraries.cryptography.api.SecretKeyRepository
import kotlinx.coroutines.flow.Flow
import java.util.concurrent.CopyOnWriteArrayList
private const val SECRET_KEY_ALIAS = "elementx.SECRET_KEY_ALIAS_PIN_CODE"
@ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)
class DefaultPinCodeManager(
private val secretKeyRepository: SecretKeyRepository,
private val encryptionDecryptionService: EncryptionDecryptionService,
private val lockScreenStore: LockScreenStore,
) : PinCodeManager {
private val callbacks = CopyOnWriteArrayList<PinCodeManager.Callback>()
override fun addCallback(callback: PinCodeManager.Callback) {
callbacks.add(callback)
}
override fun removeCallback(callback: PinCodeManager.Callback) {
callbacks.remove(callback)
}
override fun hasPinCode(): Flow<Boolean> {
return lockScreenStore.hasPinCode()
}
override suspend fun getPinCodeSize(): Int {
val encryptedPinCode = lockScreenStore.getEncryptedCode() ?: return 0
val secretKey = secretKeyRepository.getOrCreateKey(SECRET_KEY_ALIAS, false)
val decryptedPinCode = encryptionDecryptionService.decrypt(secretKey, EncryptionResult.fromBase64(encryptedPinCode))
return decryptedPinCode.size
}
override suspend fun createPinCode(pinCode: String) {
val secretKey = secretKeyRepository.getOrCreateKey(SECRET_KEY_ALIAS, false)
val encryptedPinCode = encryptionDecryptionService.encrypt(secretKey, pinCode.toByteArray()).toBase64()
lockScreenStore.saveEncryptedPinCode(encryptedPinCode)
callbacks.forEach { it.onPinCodeCreated() }
}
override suspend fun verifyPinCode(pinCode: String): Boolean {
val encryptedPinCode = lockScreenStore.getEncryptedCode() ?: return false
return try {
val secretKey = secretKeyRepository.getOrCreateKey(SECRET_KEY_ALIAS, false)
val decryptedPinCode = encryptionDecryptionService.decrypt(secretKey, EncryptionResult.fromBase64(encryptedPinCode))
val pinCodeToCheck = pinCode.toByteArray()
decryptedPinCode.contentEquals(pinCodeToCheck).also { isPinCodeCorrect ->
if (isPinCodeCorrect) {
lockScreenStore.resetCounter()
callbacks.forEach { callback ->
callback.onPinCodeVerified()
}
} else {
lockScreenStore.onWrongPin()
}
}
} catch (failure: Throwable) {
false
}
}
override suspend fun deletePinCode() {
lockScreenStore.deleteEncryptedPinCode()
lockScreenStore.resetCounter()
callbacks.forEach { it.onPinCodeRemoved() }
}
override suspend fun getRemainingPinCodeAttemptsNumber(): Int {
return lockScreenStore.getRemainingPinCodeAttemptsNumber()
}
}
@@ -0,0 +1,17 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.pin
open class DefaultPinCodeManagerCallback : PinCodeManager.Callback {
override fun onPinCodeVerified() = Unit
override fun onPinCodeCreated() = Unit
override fun onPinCodeRemoved() = Unit
}
@@ -0,0 +1,78 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.pin
import kotlinx.coroutines.flow.Flow
/**
* This interface is the main interface to manage the pin code.
* Implementation should take care of encrypting the pin code and storing it.
*/
interface PinCodeManager {
/**
* Callbacks for pin code management events.
*/
interface Callback {
/**
* Called when the pin code is verified.
*/
fun onPinCodeVerified()
/**
* Called when the pin code is created.
*/
fun onPinCodeCreated()
/**
* Called when the pin code is removed.
*/
fun onPinCodeRemoved()
}
/**
* Register a callback to be notified of pin code management events.
*/
fun addCallback(callback: Callback)
/**
* Unregister callback to be notified of pin code management events.
*/
fun removeCallback(callback: Callback)
/**
* @return true if a pin code is available.
*/
fun hasPinCode(): Flow<Boolean>
/**
* @return the size of the saved pin code.
*/
suspend fun getPinCodeSize(): Int
/**
* Creates a new encrypted pin code.
* @param pinCode the clear pin code to create
*/
suspend fun createPinCode(pinCode: String)
/**
* @return true if the pin code is correct.
*/
suspend fun verifyPinCode(pinCode: String): Boolean
/**
* Deletes the previously created pin code.
*/
suspend fun deletePinCode()
/**
* @return the number of remaining attempts before the pin code is blocked.
*/
suspend fun getRemainingPinCodeAttemptsNumber(): Int
}
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.pin.model
import androidx.compose.runtime.Immutable
@Immutable
sealed interface PinDigit {
data object Empty : PinDigit
data class Filled(val value: Char) : PinDigit
fun toText(): String {
return when (this) {
is Empty -> ""
is Filled -> value.toString()
}
}
}
@@ -0,0 +1,79 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.pin.model
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
data class PinEntry(
val digits: ImmutableList<PinDigit>,
) {
companion object {
fun createEmpty(size: Int): PinEntry {
val digits = List(size) { PinDigit.Empty }
return PinEntry(
digits = digits.toImmutableList()
)
}
}
val size = digits.size
/**
* Fill the first digits with the given text.
* Can't be more than the size of the PinEntry
* Keep the Empty digits at the end
* @return the new PinEntry
*/
fun fillWith(text: String): PinEntry {
val newDigits = MutableList<PinDigit>(size) { PinDigit.Empty }
text.forEachIndexed { index, char ->
if (index < size && char.isDigit()) {
newDigits[index] = PinDigit.Filled(char)
}
}
return copy(digits = newDigits.toImmutableList())
}
fun deleteLast(): PinEntry {
if (isEmpty()) return this
val newDigits = digits.toMutableList()
newDigits.indexOfLast { it is PinDigit.Filled }.also { lastFilled ->
newDigits[lastFilled] = PinDigit.Empty
}
return copy(digits = newDigits.toImmutableList())
}
fun addDigit(digit: Char): PinEntry {
if (isComplete()) return this
val newDigits = digits.toMutableList()
newDigits.indexOfFirst { it is PinDigit.Empty }.also { firstEmpty ->
newDigits[firstEmpty] = PinDigit.Filled(digit)
}
return copy(digits = newDigits.toImmutableList())
}
fun clear(): PinEntry {
return createEmpty(size)
}
fun isComplete(): Boolean {
return digits.all { it is PinDigit.Filled }
}
fun isEmpty(): Boolean {
return digits.all { it is PinDigit.Empty }
}
fun toText(): String {
return digits.joinToString("") {
it.toText()
}
}
}
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.settings
sealed interface LockScreenSettingsEvents {
data object OnRemovePin : LockScreenSettingsEvents
data object ConfirmRemovePin : LockScreenSettingsEvents
data object CancelRemovePin : LockScreenSettingsEvents
data object ToggleBiometricAllowed : LockScreenSettingsEvents
}
@@ -0,0 +1,127 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.settings
import android.os.Parcelable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
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.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.newRoot
import com.bumble.appyx.navmodel.backstack.operation.push
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.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.pin.SetupPinNode
import io.element.android.features.lockscreen.impl.unlock.PinUnlockNode
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.ui.common.nodes.emptyNode
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
@AssistedInject
class LockScreenSettingsFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val pinCodeManager: PinCodeManager,
) : BaseFlowNode<LockScreenSettingsFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Loading,
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
plugins = plugins,
) {
sealed interface NavTarget : Parcelable {
@Parcelize
data object Loading : NavTarget
@Parcelize
data object Unlock : NavTarget
@Parcelize
data object SetupPin : NavTarget
@Parcelize
data object Settings : NavTarget
}
private val pinCodeManagerCallback = object : DefaultPinCodeManagerCallback() {
override fun onPinCodeRemoved() {
navigateUp()
}
override fun onPinCodeCreated() {
backstack.newRoot(NavTarget.Settings)
}
}
override fun onBuilt() {
super.onBuilt()
lifecycleScope.launch {
val hasPinCode = pinCodeManager.hasPinCode().first()
if (hasPinCode) {
backstack.newRoot(NavTarget.Unlock)
} else {
backstack.newRoot(NavTarget.SetupPin)
}
}
lifecycle.subscribe(
onCreate = {
pinCodeManager.addCallback(pinCodeManagerCallback)
},
onDestroy = {
pinCodeManager.removeCallback(pinCodeManagerCallback)
}
)
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
NavTarget.Loading -> {
emptyNode(buildContext)
}
NavTarget.Unlock -> {
val callback = object : PinUnlockNode.Callback {
override fun onUnlock() {
backstack.newRoot(NavTarget.Settings)
}
}
createNode<PinUnlockNode>(buildContext, plugins = listOf(callback))
}
NavTarget.SetupPin -> {
createNode<SetupPinNode>(buildContext)
}
NavTarget.Settings -> {
val callback = object : LockScreenSettingsNode.Callback {
override fun navigateToSetupPin() {
backstack.push(NavTarget.SetupPin)
}
}
createNode<LockScreenSettingsNode>(buildContext, plugins = listOf(callback))
}
}
}
@Composable
override fun View(modifier: Modifier) {
BackstackView()
}
}
@@ -0,0 +1,45 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.settings
import androidx.compose.runtime.Composable
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 dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
@AssistedInject
class LockScreenSettingsNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: LockScreenSettingsPresenter,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun navigateToSetupPin()
}
private val callback: Callback = callback()
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
LockScreenSettingsView(
state = state,
onBackClick = this::navigateUp,
onChangePinClick = callback::navigateToSetupPin,
modifier = modifier,
)
}
}
@@ -0,0 +1,89 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.settings
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject
import io.element.android.features.lockscreen.impl.LockScreenConfig
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticator
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager
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.di.annotations.AppCoroutineScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@Inject
class LockScreenSettingsPresenter(
private val lockScreenConfig: LockScreenConfig,
private val pinCodeManager: PinCodeManager,
private val lockScreenStore: LockScreenStore,
private val biometricAuthenticatorManager: BiometricAuthenticatorManager,
@AppCoroutineScope
private val coroutineScope: CoroutineScope,
) : Presenter<LockScreenSettingsState> {
@Composable
override fun present(): LockScreenSettingsState {
val showRemovePinOption by produceState(initialValue = false) {
pinCodeManager.hasPinCode().collect { hasPinCode ->
value = !lockScreenConfig.isPinMandatory && hasPinCode
}
}
val isBiometricEnabled by remember {
lockScreenStore.isBiometricUnlockAllowed()
}.collectAsState(initial = false)
var showRemovePinConfirmation by remember {
mutableStateOf(false)
}
val biometricUnlock = biometricAuthenticatorManager.rememberConfirmBiometricAuthenticator()
fun handleEvent(event: LockScreenSettingsEvents) {
when (event) {
LockScreenSettingsEvents.CancelRemovePin -> showRemovePinConfirmation = false
LockScreenSettingsEvents.ConfirmRemovePin -> {
coroutineScope.launch {
if (showRemovePinConfirmation) {
showRemovePinConfirmation = false
pinCodeManager.deletePinCode()
}
}
}
LockScreenSettingsEvents.OnRemovePin -> showRemovePinConfirmation = true
LockScreenSettingsEvents.ToggleBiometricAllowed -> {
coroutineScope.launch {
if (!isBiometricEnabled) {
biometricUnlock.setup()
if (biometricUnlock.authenticate() == BiometricAuthenticator.AuthenticationResult.Success) {
lockScreenStore.setIsBiometricUnlockAllowed(true)
}
} else {
lockScreenStore.setIsBiometricUnlockAllowed(false)
}
}
}
}
}
return LockScreenSettingsState(
showRemovePinOption = showRemovePinOption,
isBiometricEnabled = isBiometricEnabled,
showRemovePinConfirmation = showRemovePinConfirmation,
showToggleBiometric = biometricAuthenticatorManager.isDeviceSecured,
eventSink = ::handleEvent,
)
}
}
@@ -0,0 +1,17 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.settings
data class LockScreenSettingsState(
val showRemovePinOption: Boolean,
val isBiometricEnabled: Boolean,
val showRemovePinConfirmation: Boolean,
val showToggleBiometric: Boolean,
val eventSink: (LockScreenSettingsEvents) -> Unit
)
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.settings
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
open class LockScreenSettingsStateProvider : PreviewParameterProvider<LockScreenSettingsState> {
override val values: Sequence<LockScreenSettingsState>
get() = sequenceOf(
aLockScreenSettingsState(),
aLockScreenSettingsState(isLockMandatory = true),
aLockScreenSettingsState(showRemovePinConfirmation = true),
)
}
fun aLockScreenSettingsState(
isLockMandatory: Boolean = false,
isBiometricEnabled: Boolean = false,
showRemovePinConfirmation: Boolean = false,
showToggleBiometric: Boolean = true,
) = LockScreenSettingsState(
showRemovePinOption = isLockMandatory,
isBiometricEnabled = isBiometricEnabled,
showRemovePinConfirmation = showRemovePinConfirmation,
showToggleBiometric = showToggleBiometric,
eventSink = {}
)
@@ -0,0 +1,96 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.settings
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.lockscreen.impl.R
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferenceDivider
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
import io.element.android.libraries.designsystem.theme.components.Text
@Composable
fun LockScreenSettingsView(
state: LockScreenSettingsState,
onChangePinClick: () -> Unit,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
PreferencePage(
title = stringResource(id = io.element.android.libraries.ui.strings.R.string.common_screen_lock),
onBackClick = onBackClick,
modifier = modifier
) {
PreferenceCategory(showTopDivider = false) {
ListItem(
headlineContent = {
Text(stringResource(id = R.string.screen_app_lock_settings_change_pin))
},
onClick = onChangePinClick,
)
PreferenceDivider()
if (state.showRemovePinOption) {
ListItem(
headlineContent = {
Text(stringResource(id = R.string.screen_app_lock_settings_remove_pin))
},
style = ListItemStyle.Destructive,
onClick = {
state.eventSink(LockScreenSettingsEvents.OnRemovePin)
}
)
}
if (state.showToggleBiometric) {
PreferenceDivider()
PreferenceSwitch(
title = stringResource(id = R.string.screen_app_lock_settings_enable_biometric_unlock),
isChecked = state.isBiometricEnabled,
onCheckedChange = {
state.eventSink(LockScreenSettingsEvents.ToggleBiometricAllowed)
}
)
}
}
}
if (state.showRemovePinConfirmation) {
ConfirmationDialog(
title = stringResource(id = R.string.screen_app_lock_settings_remove_pin_alert_title),
content = stringResource(id = R.string.screen_app_lock_settings_remove_pin_alert_message),
onSubmitClick = {
state.eventSink(LockScreenSettingsEvents.ConfirmRemovePin)
},
onDismiss = {
state.eventSink(LockScreenSettingsEvents.CancelRemovePin)
}
)
}
}
@PreviewsDayNight
@Composable
internal fun LockScreenSettingsViewPreview(
@PreviewParameter(LockScreenSettingsStateProvider::class) state: LockScreenSettingsState,
) {
ElementPreview {
LockScreenSettingsView(
state = state,
onChangePinClick = {},
onBackClick = {},
)
}
}
@@ -0,0 +1,105 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
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.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.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.newRoot
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager
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.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.SessionScope
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
@AssistedInject
class LockScreenSetupFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val pinCodeManager: PinCodeManager,
val biometricAuthenticatorManager: BiometricAuthenticatorManager,
) : BaseFlowNode<LockScreenSetupFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Pin,
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
plugins = plugins,
) {
interface Callback : Plugin {
fun onSetupDone()
}
private val callback: Callback = callback()
sealed interface NavTarget : Parcelable {
@Parcelize
data object Pin : NavTarget
@Parcelize
data object Biometric : NavTarget
}
private val pinCodeManagerCallback = object : DefaultPinCodeManagerCallback() {
override fun onPinCodeCreated() {
if (biometricAuthenticatorManager.hasAvailableAuthenticator) {
backstack.newRoot(NavTarget.Biometric)
} else {
callback.onSetupDone()
}
}
}
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() {
callback.onSetupDone()
}
}
createNode<SetupBiometricNode>(buildContext, plugins = listOf(callback))
}
}
}
@Composable
override fun View(modifier: Modifier) {
BackstackView()
}
}
@@ -0,0 +1,14 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.setup.biometric
sealed interface SetupBiometricEvents {
data object AllowBiometric : SetupBiometricEvents
data object UsePin : SetupBiometricEvents
}
@@ -0,0 +1,49 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
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 dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
@AssistedInject
class SetupBiometricNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: SetupBiometricPresenter,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun onBiometricSetupDone()
}
private val callback: Callback = callback()
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
LaunchedEffect(state.isBiometricSetupDone) {
if (state.isBiometricSetupDone) {
callback.onBiometricSetupDone()
}
}
SetupBiometricView(
state = state,
modifier = modifier
)
}
}
@@ -0,0 +1,59 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
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 dev.zacsweers.metro.Inject
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticator
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager
import io.element.android.features.lockscreen.impl.storage.LockScreenStore
import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.launch
@Inject
class SetupBiometricPresenter(
private val lockScreenStore: LockScreenStore,
private val biometricAuthenticatorManager: BiometricAuthenticatorManager,
) : Presenter<SetupBiometricState> {
@Composable
override fun present(): SetupBiometricState {
var isBiometricSetupDone by remember {
mutableStateOf(false)
}
val coroutineScope = rememberCoroutineScope()
val biometricUnlock = biometricAuthenticatorManager.rememberConfirmBiometricAuthenticator()
fun handleEvent(event: SetupBiometricEvents) {
when (event) {
SetupBiometricEvents.AllowBiometric -> coroutineScope.launch {
biometricUnlock.setup()
if (biometricUnlock.authenticate() == BiometricAuthenticator.AuthenticationResult.Success) {
lockScreenStore.setIsBiometricUnlockAllowed(true)
isBiometricSetupDone = true
}
}
SetupBiometricEvents.UsePin -> coroutineScope.launch {
lockScreenStore.setIsBiometricUnlockAllowed(false)
isBiometricSetupDone = true
}
}
}
return SetupBiometricState(
isBiometricSetupDone = isBiometricSetupDone,
eventSink = ::handleEvent,
)
}
}
@@ -0,0 +1,14 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.setup.biometric
data class SetupBiometricState(
val isBiometricSetupDone: Boolean,
val eventSink: (SetupBiometricEvents) -> Unit
)
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
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,88 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.setup.biometric
import androidx.activity.compose.BackHandler
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.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.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.BigIcon
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 {
state.eventSink(SetupBiometricEvents.UsePin)
}
HeaderFooterPage(
modifier = modifier.padding(top = 80.dp),
header = {
SetupBiometricHeader()
},
footer = {
SetupBiometricFooter(
onAllowClick = { state.eventSink(SetupBiometricEvents.AllowBiometric) },
onSkipClick = { state.eventSink(SetupBiometricEvents.UsePin) }
)
},
)
}
@Composable
private fun SetupBiometricHeader() {
val biometricAuth = stringResource(id = R.string.screen_app_lock_biometric_authentication)
IconTitleSubtitleMolecule(
iconStyle = BigIcon.Style.Default(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),
)
}
@Composable
private fun SetupBiometricFooter(
onAllowClick: () -> Unit,
onSkipClick: () -> Unit,
) {
ButtonColumnMolecule {
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 = onAllowClick
)
TextButton(
text = stringResource(id = R.string.screen_app_lock_setup_biometric_unlock_skip),
onClick = onSkipClick
)
}
}
@Composable
@PreviewsDayNight
internal fun SetupBiometricViewPreview(@PreviewParameter(SetupBiometricStateProvider::class) state: SetupBiometricState) {
ElementPreview {
SetupBiometricView(
state = state,
)
}
}
@@ -0,0 +1,14 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.setup.pin
sealed interface SetupPinEvents {
data class OnPinEntryChanged(val entryAsText: String, val fromConfirmationStep: Boolean) : SetupPinEvents
data object ClearFailure : SetupPinEvents
}
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.setup.pin
import androidx.compose.runtime.Composable
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 dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
@AssistedInject
class SetupPinNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: SetupPinPresenter,
) : Node(buildContext, plugins = plugins) {
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
SetupPinView(
state = state,
onBackClick = this::navigateUp,
modifier = modifier
)
}
}
@@ -0,0 +1,113 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.setup.pin
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject
import io.element.android.features.lockscreen.impl.LockScreenConfig
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.features.lockscreen.impl.setup.pin.validation.PinValidator
import io.element.android.features.lockscreen.impl.setup.pin.validation.SetupPinFailure
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta
import kotlinx.coroutines.delay
/**
* Some time for the ui to refresh before showing confirmation step.
*/
private const val DELAY_BEFORE_CONFIRMATION_STEP_IN_MILLIS = 100L
@Inject
class SetupPinPresenter(
private val lockScreenConfig: LockScreenConfig,
private val pinValidator: PinValidator,
private val buildMeta: BuildMeta,
private val pinCodeManager: PinCodeManager,
) : Presenter<SetupPinState> {
@Composable
override fun present(): SetupPinState {
var choosePinEntry by remember {
mutableStateOf(PinEntry.createEmpty(lockScreenConfig.pinSize))
}
var confirmPinEntry by remember {
mutableStateOf(PinEntry.createEmpty(lockScreenConfig.pinSize))
}
var isConfirmationStep by remember {
mutableStateOf(false)
}
var setupPinFailure by remember {
mutableStateOf<SetupPinFailure?>(null)
}
LaunchedEffect(choosePinEntry) {
if (choosePinEntry.isComplete()) {
when (val pinValidationResult = pinValidator.isPinValid(choosePinEntry)) {
is PinValidator.Result.Invalid -> {
setupPinFailure = pinValidationResult.failure
}
PinValidator.Result.Valid -> {
delay(DELAY_BEFORE_CONFIRMATION_STEP_IN_MILLIS)
isConfirmationStep = true
}
}
}
}
LaunchedEffect(confirmPinEntry) {
if (confirmPinEntry.isComplete()) {
if (confirmPinEntry == choosePinEntry) {
pinCodeManager.createPinCode(confirmPinEntry.toText())
} else {
setupPinFailure = SetupPinFailure.PinsDoNotMatch
}
}
}
fun handleEvent(event: SetupPinEvents) {
when (event) {
is SetupPinEvents.OnPinEntryChanged -> {
// Use the fromConfirmationStep flag from ui to avoid race condition.
if (event.fromConfirmationStep) {
confirmPinEntry = confirmPinEntry.fillWith(event.entryAsText)
} else {
choosePinEntry = choosePinEntry.fillWith(event.entryAsText)
}
}
SetupPinEvents.ClearFailure -> {
when (setupPinFailure) {
is SetupPinFailure.PinsDoNotMatch -> {
choosePinEntry = choosePinEntry.clear()
confirmPinEntry = confirmPinEntry.clear()
}
is SetupPinFailure.ForbiddenPin -> {
choosePinEntry = choosePinEntry.clear()
}
null -> Unit
}
isConfirmationStep = false
setupPinFailure = null
}
}
}
return SetupPinState(
choosePinEntry = choosePinEntry,
confirmPinEntry = confirmPinEntry,
isConfirmationStep = isConfirmationStep,
setupPinFailure = setupPinFailure,
appName = buildMeta.applicationName,
eventSink = ::handleEvent,
)
}
}
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.setup.pin
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.features.lockscreen.impl.setup.pin.validation.SetupPinFailure
data class SetupPinState(
val choosePinEntry: PinEntry,
val confirmPinEntry: PinEntry,
val isConfirmationStep: Boolean,
val setupPinFailure: SetupPinFailure?,
val appName: String,
val eventSink: (SetupPinEvents) -> Unit
) {
val activePinEntry = if (isConfirmationStep) {
confirmPinEntry
} else {
choosePinEntry
}
}
@@ -0,0 +1,51 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.setup.pin
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.features.lockscreen.impl.setup.pin.validation.SetupPinFailure
open class SetupPinStateProvider : PreviewParameterProvider<SetupPinState> {
override val values: Sequence<SetupPinState>
get() = sequenceOf(
aSetupPinState(),
aSetupPinState(
choosePinEntry = PinEntry.createEmpty(4).fillWith("12")
),
aSetupPinState(
choosePinEntry = PinEntry.createEmpty(4).fillWith("1789"),
isConfirmationStep = true,
),
aSetupPinState(
choosePinEntry = PinEntry.createEmpty(4).fillWith("1789"),
confirmPinEntry = PinEntry.createEmpty(4).fillWith("1788"),
isConfirmationStep = true,
creationFailure = SetupPinFailure.PinsDoNotMatch
),
aSetupPinState(
choosePinEntry = PinEntry.createEmpty(4).fillWith("1111"),
creationFailure = SetupPinFailure.ForbiddenPin
),
)
}
fun aSetupPinState(
choosePinEntry: PinEntry = PinEntry.createEmpty(4),
confirmPinEntry: PinEntry = PinEntry.createEmpty(4),
isConfirmationStep: Boolean = false,
creationFailure: SetupPinFailure? = null,
) = SetupPinState(
choosePinEntry = choosePinEntry,
confirmPinEntry = confirmPinEntry,
isConfirmationStep = isConfirmationStep,
setupPinFailure = creationFailure,
appName = "Element",
eventSink = {}
)
@@ -0,0 +1,155 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.features.lockscreen.impl.setup.pin
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.lockscreen.impl.R
import io.element.android.features.lockscreen.impl.components.PinEntryTextField
import io.element.android.features.lockscreen.impl.setup.pin.validation.SetupPinFailure
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.TopAppBar
@Composable
fun SetupPinView(
state: SetupPinState,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Scaffold(
modifier = modifier,
topBar = {
TopAppBar(
navigationIcon = {
BackButton(onClick = onBackClick)
},
title = {}
)
},
content = { padding ->
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.imePadding()
.padding(padding)
.consumeWindowInsets(padding)
.verticalScroll(state = scrollState)
.padding(vertical = 16.dp, horizontal = 20.dp),
) {
SetupPinHeader(state.isConfirmationStep, state.appName)
SetupPinContent(state)
}
}
)
}
@Composable
private fun SetupPinHeader(
isValidationStep: Boolean,
appName: String,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
IconTitleSubtitleMolecule(
title = if (isValidationStep) {
stringResource(id = R.string.screen_app_lock_setup_confirm_pin)
} else {
stringResource(id = R.string.screen_app_lock_setup_choose_pin)
},
subTitle = stringResource(id = R.string.screen_app_lock_setup_pin_context, appName),
iconStyle = BigIcon.Style.Default(CompoundIcons.LockSolid()),
)
}
}
@Composable
private fun SetupPinContent(
state: SetupPinState,
) {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
PinEntryTextField(
pinEntry = state.activePinEntry,
isSecured = true,
onValueChange = { entry ->
state.eventSink(SetupPinEvents.OnPinEntryChanged(entry, state.isConfirmationStep))
},
modifier = Modifier
.focusRequester(focusRequester)
.padding(top = 36.dp)
.fillMaxWidth()
)
if (state.setupPinFailure != null) {
ErrorDialog(
title = state.setupPinFailure.title(),
content = state.setupPinFailure.content(),
onSubmit = {
state.eventSink(SetupPinEvents.ClearFailure)
}
)
}
}
@Composable
@ReadOnlyComposable
private fun SetupPinFailure.content(): String {
return when (this) {
SetupPinFailure.ForbiddenPin -> stringResource(id = R.string.screen_app_lock_setup_pin_forbidden_dialog_content)
SetupPinFailure.PinsDoNotMatch -> stringResource(id = R.string.screen_app_lock_setup_pin_mismatch_dialog_content)
}
}
@Composable
@ReadOnlyComposable
private fun SetupPinFailure.title(): String {
return when (this) {
SetupPinFailure.ForbiddenPin -> stringResource(id = R.string.screen_app_lock_setup_pin_forbidden_dialog_title)
SetupPinFailure.PinsDoNotMatch -> stringResource(id = R.string.screen_app_lock_setup_pin_mismatch_dialog_title)
}
}
@Composable
@PreviewsDayNight
internal fun SetupPinViewPreview(@PreviewParameter(SetupPinStateProvider::class) state: SetupPinState) {
ElementPreview {
SetupPinView(
state = state,
onBackClick = {},
)
}
}
@@ -0,0 +1,31 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.setup.pin.validation
import dev.zacsweers.metro.Inject
import io.element.android.features.lockscreen.impl.LockScreenConfig
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
@Inject
class PinValidator(private val lockScreenConfig: LockScreenConfig) {
sealed interface Result {
data object Valid : Result
data class Invalid(val failure: SetupPinFailure) : Result
}
fun isPinValid(pinEntry: PinEntry): Result {
val pinAsText = pinEntry.toText()
val isForbidden = lockScreenConfig.forbiddenPinCodes.any { it == pinAsText }
return if (isForbidden) {
Result.Invalid(SetupPinFailure.ForbiddenPin)
} else {
Result.Valid
}
}
}
@@ -0,0 +1,14 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.setup.pin.validation
sealed interface SetupPinFailure {
data object ForbiddenPin : SetupPinFailure
data object PinsDoNotMatch : SetupPinFailure
}
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.storage
import kotlinx.coroutines.flow.Flow
/**
* Should be implemented by any class that provides access to the encrypted PIN code.
* All methods are suspending in case there are async IO operations involved.
*/
interface EncryptedPinCodeStorage {
/**
* Returns the encrypted PIN code.
*/
suspend fun getEncryptedCode(): String?
/**
* Saves the encrypted PIN code to some persistable storage.
*/
suspend fun saveEncryptedPinCode(pinCode: String)
/**
* Deletes the PIN code from some persistable storage.
*/
suspend fun deleteEncryptedPinCode()
/**
* Returns whether the PIN code is stored or not.
*/
fun hasPinCode(): Flow<Boolean>
}
@@ -0,0 +1,38 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.storage
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.
*/
suspend fun getRemainingPinCodeAttemptsNumber(): Int
/**
* Should decrement the number of remaining PIN code attempts.
*/
suspend fun onWrongPin()
/**
* Resets the counter of attempts for PIN code and biometric access.
*/
suspend fun resetCounter()
/**
* Returns whether the biometric unlock is allowed or not.
*/
fun isBiometricUnlockAllowed(): Flow<Boolean>
/**
* Sets whether the biometric unlock is allowed or not.
*/
suspend fun setIsBiometricUnlockAllowed(isAllowed: Boolean)
}
@@ -0,0 +1,92 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.storage
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.lockscreen.impl.LockScreenConfig
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
@ContributesBinding(AppScope::class)
class PreferencesLockScreenStore(
preferenceDataStoreFactory: PreferenceDataStoreFactory,
private val lockScreenConfig: LockScreenConfig,
) : LockScreenStore {
private val dataStore = preferenceDataStoreFactory.create("pin_code_store")
private val pinCodeKey = stringPreferencesKey("encoded_pin_code")
private val remainingAttemptsKey = intPreferencesKey("remaining_pin_code_attempts")
private val biometricUnlockKey = booleanPreferencesKey("biometric_unlock_enabled")
override suspend fun getRemainingPinCodeAttemptsNumber(): Int {
return dataStore.data.map { preferences ->
preferences.getRemainingPinCodeAttemptsNumber()
}.first()
}
override suspend fun onWrongPin() {
dataStore.edit { preferences ->
val current = preferences.getRemainingPinCodeAttemptsNumber()
val remaining = (current - 1).coerceAtLeast(0)
preferences[remainingAttemptsKey] = remaining
}
}
override suspend fun resetCounter() {
dataStore.edit { preferences ->
preferences[remainingAttemptsKey] = lockScreenConfig.maxPinCodeAttemptsBeforeLogout
}
}
override suspend fun getEncryptedCode(): String? {
return dataStore.data.map { preferences ->
preferences[pinCodeKey]
}.first()
}
override suspend fun saveEncryptedPinCode(pinCode: String) {
dataStore.edit { preferences ->
preferences[pinCodeKey] = pinCode
}
}
override suspend fun deleteEncryptedPinCode() {
dataStore.edit { preferences ->
preferences.remove(pinCodeKey)
}
}
override fun hasPinCode(): Flow<Boolean> {
return dataStore.data.map { preferences ->
preferences[pinCodeKey] != null
}
}
override fun isBiometricUnlockAllowed(): Flow<Boolean> {
return dataStore.data.map { preferences ->
preferences[biometricUnlockKey] ?: false
}
}
override suspend fun setIsBiometricUnlockAllowed(isAllowed: Boolean) {
dataStore.edit { preferences ->
preferences[biometricUnlockKey] = isAllowed
}
}
private fun Preferences.getRemainingPinCodeAttemptsNumber() = this[remainingAttemptsKey] ?: lockScreenConfig.maxPinCodeAttemptsBeforeLogout
}
@@ -0,0 +1,21 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.unlock
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
sealed interface PinUnlockEvents {
data class OnPinKeypadPressed(val pinKeypadModel: PinKeypadModel) : PinUnlockEvents
data class OnPinEntryChanged(val entryAsText: String) : PinUnlockEvents
data object OnForgetPin : PinUnlockEvents
data object ClearSignOutPrompt : PinUnlockEvents
data object SignOut : PinUnlockEvents
data object OnUseBiometric : PinUnlockEvents
data object ClearBiometricError : PinUnlockEvents
}
@@ -0,0 +1,48 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.unlock
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import dev.zacsweers.metro.Inject
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager
import io.element.android.features.lockscreen.impl.biometric.DefaultBiometricUnlockCallback
import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
@Inject
class PinUnlockHelper(
private val biometricAuthenticatorManager: BiometricAuthenticatorManager,
private val pinCodeManager: PinCodeManager
) {
@Composable
fun OnUnlockEffect(onUnlock: () -> Unit) {
val latestOnUnlock by rememberUpdatedState(onUnlock)
DisposableEffect(Unit) {
val biometricUnlockCallback = object : DefaultBiometricUnlockCallback() {
override fun onBiometricAuthenticationSuccess() {
latestOnUnlock()
}
}
val pinCodeVerifiedCallback = object : DefaultPinCodeManagerCallback() {
override fun onPinCodeVerified() {
latestOnUnlock()
}
}
biometricAuthenticatorManager.addCallback(biometricUnlockCallback)
pinCodeManager.addCallback(pinCodeVerifiedCallback)
onDispose {
biometricAuthenticatorManager.removeCallback(biometricUnlockCallback)
pinCodeManager.removeCallback(pinCodeVerifiedCallback)
}
}
}
}
@@ -0,0 +1,52 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.unlock
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 dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
@AssistedInject
class PinUnlockNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: PinUnlockPresenter,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun onUnlock()
}
private val callback: Callback = callback()
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
LaunchedEffect(state.isUnlocked) {
if (state.isUnlocked) {
callback.onUnlock()
}
}
PinUnlockView(
state = state,
// UnlockNode is only used for in-app unlock, so we can safely set isInAppUnlock to true.
// It's set to false in PinUnlockActivity.
isInAppUnlock = true,
modifier = modifier
)
}
}
@@ -0,0 +1,181 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.unlock
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticator
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
import io.element.android.features.logout.api.LogoutUseCase
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.di.annotations.AppCoroutineScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@Inject
class PinUnlockPresenter(
private val pinCodeManager: PinCodeManager,
private val biometricAuthenticatorManager: BiometricAuthenticatorManager,
private val logoutUseCase: LogoutUseCase,
@AppCoroutineScope
private val coroutineScope: CoroutineScope,
private val pinUnlockHelper: PinUnlockHelper,
) : Presenter<PinUnlockState> {
@Composable
override fun present(): PinUnlockState {
val pinEntryState = remember {
mutableStateOf<AsyncData<PinEntry>>(AsyncData.Uninitialized)
}
val pinEntry by pinEntryState
var remainingAttempts by remember {
mutableStateOf<AsyncData<Int>>(AsyncData.Uninitialized)
}
var showWrongPinTitle by rememberSaveable {
mutableStateOf(false)
}
var showSignOutPrompt by rememberSaveable {
mutableStateOf(false)
}
val signOutAction = remember {
mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
}
var biometricUnlockResult by remember {
mutableStateOf<BiometricAuthenticator.AuthenticationResult?>(null)
}
val isUnlocked = remember {
mutableStateOf(false)
}
val biometricUnlock = biometricAuthenticatorManager.rememberUnlockBiometricAuthenticator()
LaunchedEffect(Unit) {
suspend {
val pinCodeSize = pinCodeManager.getPinCodeSize()
PinEntry.createEmpty(pinCodeSize)
}.runCatchingUpdatingState(pinEntryState)
}
LaunchedEffect(biometricUnlock) {
biometricUnlock.setup()
biometricUnlock.authenticate()
}
LaunchedEffect(pinEntry) {
if (pinEntry.isComplete()) {
val isVerified = pinCodeManager.verifyPinCode(pinEntry.toText())
if (!isVerified) {
pinEntryState.value = pinEntry.clear()
showWrongPinTitle = true
}
}
val remainingAttemptsNumber = pinCodeManager.getRemainingPinCodeAttemptsNumber()
remainingAttempts = AsyncData.Success(remainingAttemptsNumber)
if (remainingAttemptsNumber == 0) {
showSignOutPrompt = true
}
}
pinUnlockHelper.OnUnlockEffect {
isUnlocked.value = true
}
fun handleEvent(event: PinUnlockEvents) {
when (event) {
is PinUnlockEvents.OnPinKeypadPressed -> {
pinEntryState.value = pinEntry.process(event.pinKeypadModel)
}
PinUnlockEvents.OnForgetPin -> showSignOutPrompt = true
PinUnlockEvents.ClearSignOutPrompt -> showSignOutPrompt = false
PinUnlockEvents.SignOut -> {
if (showSignOutPrompt) {
showSignOutPrompt = false
coroutineScope.signOut(signOutAction)
}
}
PinUnlockEvents.OnUseBiometric -> {
coroutineScope.launch {
biometricUnlockResult = biometricUnlock.authenticate()
}
}
PinUnlockEvents.ClearBiometricError -> {
biometricUnlockResult = null
}
is PinUnlockEvents.OnPinEntryChanged -> {
pinEntryState.value = pinEntry.process(event.entryAsText)
}
}
}
return PinUnlockState(
pinEntry = pinEntry,
showWrongPinTitle = showWrongPinTitle,
remainingAttempts = remainingAttempts,
showSignOutPrompt = showSignOutPrompt,
signOutAction = signOutAction.value,
showBiometricUnlock = biometricUnlock.isActive,
biometricUnlockResult = biometricUnlockResult,
isUnlocked = isUnlocked.value,
eventSink = ::handleEvent,
)
}
private fun AsyncData<PinEntry>.isComplete(): Boolean {
return dataOrNull()?.isComplete().orFalse()
}
private fun AsyncData<PinEntry>.toText(): String {
return dataOrNull()?.toText() ?: ""
}
private fun AsyncData<PinEntry>.clear(): AsyncData<PinEntry> {
return when (this) {
is AsyncData.Success -> AsyncData.Success(data.clear())
else -> this
}
}
private fun AsyncData<PinEntry>.process(pinKeypadModel: PinKeypadModel): AsyncData<PinEntry> {
return when (this) {
is AsyncData.Success -> {
val pinEntry = when (pinKeypadModel) {
PinKeypadModel.Back -> data.deleteLast()
is PinKeypadModel.Number -> data.addDigit(pinKeypadModel.number)
PinKeypadModel.Empty -> data
}
AsyncData.Success(pinEntry)
}
else -> this
}
}
private fun AsyncData<PinEntry>.process(pinEntryAsText: String): AsyncData<PinEntry> {
return when (this) {
is AsyncData.Success -> {
val pinEntry = data.fillWith(pinEntryAsText)
AsyncData.Success(pinEntry)
}
else -> this
}
}
private fun CoroutineScope.signOut(signOutAction: MutableState<AsyncAction<Unit>>) = launch {
suspend {
logoutUseCase.logoutAll(ignoreSdkError = true)
}.runCatchingUpdatingState(signOutAction)
}
}
@@ -0,0 +1,42 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.unlock
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticator
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockError
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
data class PinUnlockState(
val pinEntry: AsyncData<PinEntry>,
val showWrongPinTitle: Boolean,
val remainingAttempts: AsyncData<Int>,
val showSignOutPrompt: Boolean,
val signOutAction: AsyncAction<Unit>,
val showBiometricUnlock: Boolean,
val isUnlocked: Boolean,
val biometricUnlockResult: BiometricAuthenticator.AuthenticationResult?,
val eventSink: (PinUnlockEvents) -> Unit
) {
val isSignOutPromptCancellable = when (remainingAttempts) {
is AsyncData.Success -> remainingAttempts.data > 0
else -> true
}
val biometricUnlockErrorMessage = when {
biometricUnlockResult is BiometricAuthenticator.AuthenticationResult.Failure &&
biometricUnlockResult.error is BiometricUnlockError &&
biometricUnlockResult.error.isAuthDisabledError -> {
biometricUnlockResult.error.message
}
else -> null
}
val showBiometricUnlockError = biometricUnlockErrorMessage != null
}
@@ -0,0 +1,56 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.unlock
import androidx.biometric.BiometricPrompt
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticator
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockError
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
open class PinUnlockStateProvider : PreviewParameterProvider<PinUnlockState> {
override val values: Sequence<PinUnlockState>
get() = sequenceOf(
aPinUnlockState(),
aPinUnlockState(pinEntry = PinEntry.createEmpty(4).fillWith("12")),
aPinUnlockState(showWrongPinTitle = true),
aPinUnlockState(showSignOutPrompt = true),
aPinUnlockState(showBiometricUnlock = false),
aPinUnlockState(showSignOutPrompt = true, remainingAttempts = AsyncData.Success(0)),
aPinUnlockState(signOutAction = AsyncAction.Loading),
aPinUnlockState(
biometricUnlockResult = BiometricAuthenticator.AuthenticationResult.Failure(
BiometricUnlockError(BiometricPrompt.ERROR_LOCKOUT, "Biometric auth disabled")
)
),
)
}
fun aPinUnlockState(
pinEntry: PinEntry = PinEntry.createEmpty(4),
remainingAttempts: AsyncData<Int> = AsyncData.Success(3),
showWrongPinTitle: Boolean = false,
showSignOutPrompt: Boolean = false,
showBiometricUnlock: Boolean = true,
biometricUnlockResult: BiometricAuthenticator.AuthenticationResult? = null,
isUnlocked: Boolean = false,
signOutAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
) = PinUnlockState(
pinEntry = AsyncData.Success(pinEntry),
showWrongPinTitle = showWrongPinTitle,
remainingAttempts = remainingAttempts,
showSignOutPrompt = showSignOutPrompt,
showBiometricUnlock = showBiometricUnlock,
signOutAction = signOutAction,
biometricUnlockResult = biometricUnlockResult,
isUnlocked = isUnlocked,
eventSink = {}
)
@@ -0,0 +1,377 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.unlock
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.BoxWithConstraintsScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.lockscreen.impl.R
import io.element.android.features.lockscreen.impl.components.PinEntryTextField
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.unlock.keypad.PinKeypad
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun PinUnlockView(
state: PinUnlockState,
isInAppUnlock: Boolean,
modifier: Modifier = Modifier,
) {
OnLifecycleEvent { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> state.eventSink.invoke(PinUnlockEvents.OnUseBiometric)
else -> Unit
}
}
Surface(modifier) {
PinUnlockPage(state = state, isInAppUnlock = isInAppUnlock)
if (state.showSignOutPrompt) {
SignOutPrompt(
isCancellable = state.isSignOutPromptCancellable,
onSignOut = { state.eventSink(PinUnlockEvents.SignOut) },
onDismiss = { state.eventSink(PinUnlockEvents.ClearSignOutPrompt) },
)
}
when (state.signOutAction) {
AsyncAction.Loading -> {
ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content))
}
is AsyncAction.Success,
is AsyncAction.Confirming,
is AsyncAction.Failure,
AsyncAction.Uninitialized -> Unit
}
if (state.showBiometricUnlockError) {
ErrorDialog(
content = state.biometricUnlockErrorMessage ?: "",
onSubmit = { state.eventSink(PinUnlockEvents.ClearBiometricError) }
)
}
}
}
@Composable
private fun PinUnlockPage(
state: PinUnlockState,
isInAppUnlock: Boolean,
) {
BoxWithConstraints {
val commonModifier = Modifier
.fillMaxSize()
.systemBarsPadding()
.imePadding()
.padding(all = 20.dp)
val header = @Composable {
PinUnlockHeader(
state = state,
isInAppUnlock = isInAppUnlock,
modifier = Modifier.padding(top = 60.dp)
)
}
val footer = @Composable {
PinUnlockFooter(
modifier = Modifier.padding(top = 24.dp),
showBiometricUnlock = state.showBiometricUnlock,
onUseBiometric = {
state.eventSink(PinUnlockEvents.OnUseBiometric)
},
onForgotPin = {
state.eventSink(PinUnlockEvents.OnForgetPin)
},
)
}
val content = @Composable { constraints: BoxWithConstraintsScope ->
if (isInAppUnlock) {
val pinEntry = state.pinEntry.dataOrNull()
if (pinEntry != null) {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
PinEntryTextField(
pinEntry = pinEntry,
isSecured = true,
onValueChange = {
state.eventSink(PinUnlockEvents.OnPinEntryChanged(it))
},
modifier = Modifier
.focusRequester(focusRequester)
.fillMaxWidth()
)
}
} else {
PinKeypad(
onClick = {
state.eventSink(PinUnlockEvents.OnPinKeypadPressed(it))
},
maxWidth = constraints.maxWidth,
maxHeight = constraints.maxHeight,
horizontalAlignment = Alignment.CenterHorizontally,
)
}
}
if (maxHeight < 600.dp) {
PinUnlockCompactView(
header = header,
footer = footer,
content = content,
modifier = commonModifier,
)
} else {
PinUnlockExpandedView(
header = header,
footer = footer,
content = content,
modifier = commonModifier,
)
}
}
}
@Composable
private fun SignOutPrompt(
isCancellable: Boolean,
onSignOut: () -> Unit,
onDismiss: () -> Unit,
) {
if (isCancellable) {
ConfirmationDialog(
title = stringResource(id = R.string.screen_app_lock_signout_alert_title),
content = stringResource(id = R.string.screen_app_lock_signout_alert_message),
onSubmitClick = onSignOut,
onDismiss = onDismiss,
)
} else {
ErrorDialog(
title = stringResource(id = R.string.screen_app_lock_signout_alert_title),
content = stringResource(id = R.string.screen_app_lock_signout_alert_message),
onSubmit = onSignOut,
)
}
}
@Composable
private fun PinUnlockCompactView(
header: @Composable () -> Unit,
footer: @Composable () -> Unit,
modifier: Modifier = Modifier,
content: @Composable BoxWithConstraintsScope.() -> Unit,
) {
Row(modifier = modifier) {
Column(Modifier.weight(1f)) {
header()
Spacer(modifier = Modifier.height(24.dp))
footer()
}
BoxWithConstraints(
modifier = Modifier
.weight(1f)
.fillMaxHeight(),
contentAlignment = Alignment.Center,
) {
content()
}
}
}
@Composable
private fun PinUnlockExpandedView(
header: @Composable () -> Unit,
footer: @Composable () -> Unit,
modifier: Modifier = Modifier,
content: @Composable BoxWithConstraintsScope.() -> Unit,
) {
Column(
modifier = modifier,
) {
header()
BoxWithConstraints(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.padding(top = 40.dp),
) {
content()
}
footer()
}
}
@Composable
private fun PinDotsRow(
pinEntry: PinEntry,
) {
Row(
horizontalArrangement = spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
for (digit in pinEntry.digits) {
PinDot(isFilled = digit is PinDigit.Filled)
}
}
}
@Composable
private fun PinDot(
isFilled: Boolean,
) {
val backgroundColor = if (isFilled) {
ElementTheme.colors.iconPrimary
} else {
ElementTheme.colors.bgSubtlePrimary
}
Box(
modifier = Modifier
.size(14.dp)
.background(backgroundColor, CircleShape)
)
}
@Composable
private fun PinUnlockHeader(
state: PinUnlockState,
isInAppUnlock: Boolean,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
) {
if (isInAppUnlock) {
BigIcon(style = BigIcon.Style.Default(CompoundIcons.LockSolid()))
} else {
Icon(
modifier = Modifier
.size(32.dp),
tint = ElementTheme.colors.iconPrimary,
imageVector = CompoundIcons.LockSolid(),
contentDescription = null,
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(id = CommonStrings.common_enter_your_pin),
modifier = Modifier
.fillMaxWidth(),
textAlign = TextAlign.Center,
style = ElementTheme.typography.fontHeadingMdBold,
color = ElementTheme.colors.textPrimary,
)
Spacer(Modifier.height(8.dp))
val remainingAttempts = state.remainingAttempts.dataOrNull()
val subtitle = if (remainingAttempts != null) {
if (state.showWrongPinTitle) {
pluralStringResource(id = R.plurals.screen_app_lock_subtitle_wrong_pin, count = remainingAttempts, remainingAttempts)
} else {
pluralStringResource(id = R.plurals.screen_app_lock_subtitle, count = remainingAttempts, remainingAttempts)
}
} else {
""
}
val subtitleColor = if (state.showWrongPinTitle) {
ElementTheme.colors.textCriticalPrimary
} else {
ElementTheme.colors.textSecondary
}
Text(
text = subtitle,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
style = ElementTheme.typography.fontBodyMdRegular,
color = subtitleColor,
)
if (!isInAppUnlock && state.pinEntry is AsyncData.Success) {
Spacer(Modifier.height(24.dp))
PinDotsRow(state.pinEntry.data)
}
}
}
@Composable
private fun PinUnlockFooter(
showBiometricUnlock: Boolean,
onUseBiometric: () -> Unit,
onForgotPin: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround) {
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)
}
}
@Composable
@PreviewsDayNight
internal fun PinUnlockViewInAppPreview(@PreviewParameter(PinUnlockStateProvider::class) state: PinUnlockState) {
ElementPreview {
PinUnlockView(
state = state,
isInAppUnlock = true,
)
}
}
@Composable
@PreviewsDayNight
internal fun PinUnlockViewPreview(@PreviewParameter(PinUnlockStateProvider::class) state: PinUnlockState) {
ElementPreview {
PinUnlockView(
state = state,
isInAppUnlock = false,
)
}
}
@@ -0,0 +1,84 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.unlock.activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.OnBackPressedCallback
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.lifecycle.lifecycleScope
import dev.zacsweers.metro.Inject
import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.lockscreen.api.LockScreenLockState
import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.features.lockscreen.impl.unlock.PinUnlockPresenter
import io.element.android.features.lockscreen.impl.unlock.PinUnlockView
import io.element.android.features.lockscreen.impl.unlock.di.PinUnlockBindings
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.designsystem.theme.ElementThemeApp
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import kotlinx.coroutines.launch
class PinUnlockActivity : AppCompatActivity() {
internal companion object {
fun newIntent(context: Context): Intent {
return Intent(context, PinUnlockActivity::class.java)
}
}
@Inject lateinit var presenter: PinUnlockPresenter
@Inject lateinit var lockScreenService: LockScreenService
@Inject lateinit var appPreferencesStore: AppPreferencesStore
@Inject lateinit var enterpriseService: EnterpriseService
@Inject lateinit var buildMeta: BuildMeta
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
bindings<PinUnlockBindings>().inject(this)
setContent {
val colors by remember {
enterpriseService.semanticColorsFlow(sessionId = null)
}.collectAsState(SemanticColorsLightDark.default)
ElementThemeApp(
appPreferencesStore = appPreferencesStore,
compoundLight = colors.light,
compoundDark = colors.dark,
buildMeta = buildMeta,
) {
val state = presenter.present()
PinUnlockView(
state = state,
isInAppUnlock = false,
)
}
}
lifecycleScope.launch {
lockScreenService.lockState.collect { state ->
if (state == LockScreenLockState.Unlocked) {
finish()
}
}
}
val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
moveTaskToBack(true)
}
}
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}
}
@@ -0,0 +1,18 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.unlock.di
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesTo
import io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity
@ContributesTo(AppScope::class)
interface PinUnlockBindings {
fun inject(activity: PinUnlockActivity)
}
@@ -0,0 +1,227 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.unlock.keypad
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Backspace
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.coerceIn
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.times
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.toSp
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.libraries.ui.utils.time.digit
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
private val spaceBetweenPinKey = 16.dp
private val minSizePinKey = 16.dp
private val maxSizePinKey = 80.dp
@Composable
fun PinKeypad(
onClick: (PinKeypadModel) -> Unit,
maxWidth: Dp,
maxHeight: Dp,
modifier: Modifier = Modifier,
verticalAlignment: Alignment.Vertical = Alignment.Top,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
) {
val pinKeyMaxWidth = ((maxWidth - 2 * spaceBetweenPinKey) / 3).coerceIn(minSizePinKey, maxSizePinKey)
val pinKeyMaxHeight = ((maxHeight - 3 * spaceBetweenPinKey) / 4).coerceIn(minSizePinKey, maxSizePinKey)
val pinKeySize = if (pinKeyMaxWidth < pinKeyMaxHeight) pinKeyMaxWidth else pinKeyMaxHeight
val horizontalArrangement = spacedBy(spaceBetweenPinKey, Alignment.CenterHorizontally)
val verticalArrangement = spacedBy(spaceBetweenPinKey, Alignment.CenterVertically)
Column(
modifier = modifier.onKeyEvent { event ->
if (event.type == KeyEventType.KeyUp) {
val digitChar = event.digit
if (digitChar != null) {
onClick(PinKeypadModel.Number(digitChar))
true
} else if (event.key == Key.Backspace) {
onClick(PinKeypadModel.Back)
true
} else {
false
}
} else {
false
}
},
verticalArrangement = verticalArrangement,
horizontalAlignment = horizontalAlignment,
) {
PinKeypadRow(
pinKeySize = pinKeySize,
verticalAlignment = verticalAlignment,
horizontalArrangement = horizontalArrangement,
models = persistentListOf(PinKeypadModel.Number('1'), PinKeypadModel.Number('2'), PinKeypadModel.Number('3')),
onClick = onClick,
)
PinKeypadRow(
pinKeySize = pinKeySize,
verticalAlignment = verticalAlignment,
horizontalArrangement = horizontalArrangement,
models = persistentListOf(PinKeypadModel.Number('4'), PinKeypadModel.Number('5'), PinKeypadModel.Number('6')),
onClick = onClick,
)
PinKeypadRow(
pinKeySize = pinKeySize,
verticalAlignment = verticalAlignment,
horizontalArrangement = horizontalArrangement,
models = persistentListOf(PinKeypadModel.Number('7'), PinKeypadModel.Number('8'), PinKeypadModel.Number('9')),
onClick = onClick,
)
PinKeypadRow(
pinKeySize = pinKeySize,
verticalAlignment = verticalAlignment,
horizontalArrangement = horizontalArrangement,
models = persistentListOf(PinKeypadModel.Empty, PinKeypadModel.Number('0'), PinKeypadModel.Back),
onClick = onClick,
)
}
}
@Composable
private fun PinKeypadRow(
models: ImmutableList<PinKeypadModel>,
onClick: (PinKeypadModel) -> Unit,
pinKeySize: Dp,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
verticalAlignment: Alignment.Vertical = Alignment.Top,
) {
Row(
horizontalArrangement = horizontalArrangement,
verticalAlignment = verticalAlignment,
modifier = Modifier.fillMaxWidth(),
) {
val commonModifier = Modifier.size(pinKeySize)
for (model in models) {
when (model) {
is PinKeypadModel.Empty -> {
Spacer(modifier = commonModifier)
}
is PinKeypadModel.Back -> {
PinKeypadBackButton(
modifier = commonModifier,
onClick = { onClick(model) },
)
}
is PinKeypadModel.Number -> {
PinKeypadDigitButton(
size = pinKeySize,
modifier = commonModifier,
digit = model.number.toString(),
onClick = { onClick(model) },
)
}
}
}
}
}
@Composable
private fun PinKeypadButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable BoxScope.() -> Unit,
) {
Box(
contentAlignment = Alignment.Center,
modifier = modifier
.clip(CircleShape)
.background(color = ElementTheme.colors.bgSubtlePrimary)
.clickable(onClick = onClick),
content = content
)
}
@Composable
private fun PinKeypadDigitButton(
digit: String,
size: Dp,
onClick: (String) -> Unit,
modifier: Modifier = Modifier,
) {
PinKeypadButton(
modifier = modifier,
onClick = { onClick(digit) }
) {
val fontSize = size.toSp() / 2
val originalFont = ElementTheme.typography.fontHeadingXlBold
val ratio = fontSize.value / originalFont.fontSize.value
val lineHeight = originalFont.lineHeight * ratio
Text(
text = digit,
color = ElementTheme.colors.textPrimary,
style = originalFont.copy(fontSize = fontSize, lineHeight = lineHeight, letterSpacing = 0.sp),
)
}
}
@Composable
private fun PinKeypadBackButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
PinKeypadButton(
modifier = modifier,
onClick = onClick,
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.Backspace,
contentDescription = stringResource(CommonStrings.a11y_delete),
)
}
}
@Composable
@PreviewsDayNight
internal fun PinKeypadPreview() {
ElementPreview {
BoxWithConstraints {
PinKeypad(
maxWidth = maxWidth,
maxHeight = maxHeight,
onClick = {}
)
}
}
}
@@ -0,0 +1,18 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.unlock.keypad
import androidx.compose.runtime.Immutable
@Immutable
sealed interface PinKeypadModel {
data object Empty : PinKeypadModel
data object Back : PinKeypadModel
data class Number(val number: Char) : PinKeypadModel
}
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"біяметрычная аўтэнтыфікацыя"</string>
<string name="screen_app_lock_biometric_unlock">"біяметрычная разблакіроўка"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Разблакіроўка з дапамогай біяметрыі"</string>
<string name="screen_app_lock_forgot_pin">"Забыліся PIN-код?"</string>
<string name="screen_app_lock_settings_change_pin">"Змяніць PIN-код"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Дазволіць біяметрычную разблакіроўку"</string>
<string name="screen_app_lock_settings_remove_pin">"Выдаліць PIN-код"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Вы ўпэўнены, што хочаце выдаліць PIN-код?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Выдаліць PIN-код?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Дазволіць %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Я хацеў бы выкарыстоўваць PIN-код"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Эканомце час і выкарыстоўвайце %1$s для разблакіроўкі праграмы"</string>
<string name="screen_app_lock_setup_choose_pin">"Выберыце PIN-код"</string>
<string name="screen_app_lock_setup_confirm_pin">"Пацвярджэнне PIN-кода"</string>
<string name="screen_app_lock_setup_pin_context">"Заблакіруйце %1$s, каб павялічыць бяспеку вашых чатаў.
Абярыце што-небудзь незабыўнае. Калі вы забудзецеся гэты PIN-код, вы выйдзеце з праграмы."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Вы не можаце выбраць гэты PIN-код з меркаванняў бяспекі"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Выберыце іншы PIN-код"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Увядзіце адзін і той жа PIN двойчы"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN-коды не супадаюць"</string>
<string name="screen_app_lock_signout_alert_message">"Каб працягнуць, вам неабходна паўторна ўвайсці ў сістэму і стварыць новы PIN-код"</string>
<string name="screen_app_lock_signout_alert_title">"Вы выходзіце з сістэмы"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"У вас %1$d спроба разблакіроўкі"</item>
<item quantity="few">"У вас %1$d спробы разблакіроўкі"</item>
<item quantity="many">"У вас %1$d спроб разблакіроўкі"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"Няправільны PIN-код. У вас застаўся %1$d шанец"</item>
<item quantity="few">"Няправільны PIN-код. У вас засталася %1$d шанцы"</item>
<item quantity="many">"Няправільны PIN-код. У вас засталася %1$d шанцаў"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Выкарыстоўваць біяметрыю"</string>
<string name="screen_app_lock_use_pin_android">"Выкарыстоўваць PIN-код"</string>
<string name="screen_signout_in_progress_dialog_content">"Выхад…"</string>
</resources>
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"биометрично удостоверяване"</string>
<string name="screen_app_lock_biometric_unlock">"биометрично отключване"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Отключване с биометрия"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"Потвърдете биометричните данни"</string>
<string name="screen_app_lock_forgot_pin">"Забравихте PIN?"</string>
<string name="screen_app_lock_settings_change_pin">"Промяна на PIN кода"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Разрешаване на биометрично отключване"</string>
<string name="screen_app_lock_settings_remove_pin">"Премахване на PIN"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Сигурни ли сте, че искате да премахнете PIN?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Премахване на PIN?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Разрешаване на %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Предпочитам да използвам PIN"</string>
<string name="screen_app_lock_setup_choose_pin">"Избор на PIN"</string>
<string name="screen_app_lock_setup_confirm_pin">"Потвърждаване на PIN"</string>
<string name="screen_app_lock_setup_pin_context">"Заключете %1$s, за да добавите допълнителна сигурност към вашите чатове.
Изберете нещо запомнящо се. Ако забравите този PIN, ще бъдете излезли от приложението."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Не можете да изберете това за ваш PIN код от съображения за сигурност"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Избор на различен PIN"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Моля, въведете един и същ PIN два пъти"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PINs не съвпадат"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Имате %1$d опит да отключите"</item>
<item quantity="other">"Имате %1$d опита да отключите"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"Грешен PIN. Имате още %1$d шанс"</item>
<item quantity="other">"Грешен PIN. Имате още %1$d шанса"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Използване на биометрия"</string>
<string name="screen_app_lock_use_pin_android">"Използване на PIN"</string>
<string name="screen_signout_in_progress_dialog_content">"Излизане…"</string>
</resources>
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"Biometrické ověřování"</string>
<string name="screen_app_lock_biometric_unlock">"biometrické odemknutí"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Odemkněte pomocí biometrie"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"Potvrďte biometrické údaje"</string>
<string name="screen_app_lock_forgot_pin">"Zapomněli jste PIN?"</string>
<string name="screen_app_lock_settings_change_pin">"Změnit PIN kód"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Povolit biometrické odemykání"</string>
<string name="screen_app_lock_settings_remove_pin">"Odstranit PIN"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Opravdu chcete odstranit PIN?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Odstranit PIN?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Povolit %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Raději bych použil PIN"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Ušetřete si čas a použijte pokaždé %1$s pro odemknutí aplikace"</string>
<string name="screen_app_lock_setup_choose_pin">"Zvolte PIN"</string>
<string name="screen_app_lock_setup_confirm_pin">"Potvrďte PIN"</string>
<string name="screen_app_lock_setup_pin_context">"Zamkněte %1$s pro zvýšení bezpečnosti vašich konverzací.
Vyberte si něco zapamatovatelného. Pokud tento kód PIN zapomenete, budete z aplikace odhlášeni."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Z bezpečnostních důvodů si toto nemůžete zvolit jako svůj PIN kód"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Zvolte jiný PIN"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Zadejte stejný PIN dvakrát"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN kódy se neshodují."</string>
<string name="screen_app_lock_signout_alert_message">"Abyste mohli pokračovat, budete se muset znovu přihlásit a vytvořit nový PIN"</string>
<string name="screen_app_lock_signout_alert_title">"Jste odhlášeni"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Máte %1$d pokus pro odemknutí"</item>
<item quantity="few">"Máte %1$d pokusy pro odemknutí"</item>
<item quantity="other">"Máte %1$d pokusů pro odemknutí"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"Špatný PIN. Máte %1$d další pokus"</item>
<item quantity="few">"Špatný PIN. Máte %1$d další pokusy"</item>
<item quantity="other">"Špatný PIN. Máte %1$d dalších pokusů"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Použijte biometrické údaje"</string>
<string name="screen_app_lock_use_pin_android">"Použít PIN"</string>
<string name="screen_signout_in_progress_dialog_content">"Odhlašování…"</string>
</resources>
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"dilysu biometreg"</string>
<string name="screen_app_lock_biometric_unlock">"datgloi biometreg"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Datgloi gyda biometreg"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"Cadarnhau biometreg"</string>
<string name="screen_app_lock_forgot_pin">"Wedi anghofio\'ch PIN?"</string>
<string name="screen_app_lock_settings_change_pin">"Newid cod PIN"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Caniatáu datgloi biometreg"</string>
<string name="screen_app_lock_settings_remove_pin">"Dileu PIN"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Ydych chi\'n siŵr eich bod am ddileu\'r PIN?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Tynnu\'r PIN?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Caniatáu %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Byddai\'n well gen i ddefnyddio PIN"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Arbedwch beth amser i chi\'ch hun a defnyddiwch %1$s i ddatgloi\'r ap bob tro"</string>
<string name="screen_app_lock_setup_choose_pin">"Dewiswch PIN"</string>
<string name="screen_app_lock_setup_confirm_pin">"Cadarnhau eich PIN"</string>
<string name="screen_app_lock_setup_pin_context">"Clowch %1$s i ychwanegu diogelwch ychwanegol i\'ch sgyrsiau.
Dewiswch rywbeth cofiadwy. Os byddwch chi\'n anghofio\'r PIN hwn, byddwch chi\'n cael eich allgofnodi o\'r ap."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Does dim mod dewis hwn fel eich cod PIN am resymau diogelwch"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Dewiswch PIN gwahanol"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Rhowch yr un PIN ddwywaith"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"Nid yw\'r PINau\'n cyfateb"</string>
<string name="screen_app_lock_signout_alert_message">"Bydd angen i chi ail-fewngofnodi a chreu PIN newydd i barhau"</string>
<string name="screen_app_lock_signout_alert_title">"Rydych chi\'n cael eich allgofnodi"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="zero">"Does gennych %1$d ceisiadau i ddatgloi"</item>
<item quantity="one">"Mae gennych %1$d cais i ddatgloi"</item>
<item quantity="two">"Mae gennych %1$d gais i ddatgloi"</item>
<item quantity="few">"Mae gennych %1$d chais i ddatgloi"</item>
<item quantity="many">"Mae gennych %1$d chais i ddatgloi"</item>
<item quantity="other">"Mae gennych %1$d cais i ddatgloi"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="zero">"PIN anghywir. Does gennych %1$d cais arall"</item>
<item quantity="one">"PIN anghywir. Mae gennych %1$d cais arall"</item>
<item quantity="two">"PIN anghywir. Mae gennych %1$d gais arall"</item>
<item quantity="few">"PIN anghywir. Mae gennych %1$d chais arall"</item>
<item quantity="many">"PIN anghywir. Mae gennych %1$d chais arall"</item>
<item quantity="other">"PIN anghywir. Mae gennych %1$d cais arall"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Defnyddio biometreg"</string>
<string name="screen_app_lock_use_pin_android">"Defnyddio PIN"</string>
<string name="screen_signout_in_progress_dialog_content">"Yn allgofnodi…"</string>
</resources>
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"biometrisk godkendelse"</string>
<string name="screen_app_lock_biometric_unlock">"biometrisk oplåsning"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Lås op med biometri"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"Bekræft biometri"</string>
<string name="screen_app_lock_forgot_pin">"Glemt PIN-kode?"</string>
<string name="screen_app_lock_settings_change_pin">"Skift PIN-kode"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Tillad biometrisk oplåsning"</string>
<string name="screen_app_lock_settings_remove_pin">"Fjern PIN-koden"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Er du sikker på, at du vil fjerne PIN-koden?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Fjern PIN-koden?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Tillad %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Jeg foretrækker at bruge PIN-kode"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Spar dig selv lidt tid og brug den %1$s til at låse appen op hver gang"</string>
<string name="screen_app_lock_setup_choose_pin">"Vælg PIN-kode"</string>
<string name="screen_app_lock_setup_confirm_pin">"Bekræft PIN-kode"</string>
<string name="screen_app_lock_setup_pin_context">"Lås %1$s for at tilføje ekstra sikkerhed til dine samtaler.
Vælg noget mindeværdigt. Hvis du glemmer denne pinkode, bliver du logget ud af appen."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Du kan ikke vælge dette som din PIN-kode af sikkerhedsmæssige årsager"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Vælg en anden PIN-kode"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Indtast venligst den samme PIN-kode to gange"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN-koderne stemmer ikke overens"</string>
<string name="screen_app_lock_signout_alert_message">"Du vil være nødt til at logge ind igen og oprette en ny PIN-kode for at fortsætte."</string>
<string name="screen_app_lock_signout_alert_title">"Du bliver logget ud"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Du har %1$d forsøg på at låse op"</item>
<item quantity="other">"Du har %1$d forsøg på at låse op"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"Forkert PIN-kode. Du har %1$d chance mere"</item>
<item quantity="other">"Forkert PIN-kode. Du har %1$d flere chancer"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Brug biometri"</string>
<string name="screen_app_lock_use_pin_android">"Brug PIN-kode"</string>
<string name="screen_signout_in_progress_dialog_content">"Logger ud…"</string>
</resources>
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"biometrische Authentifizierung"</string>
<string name="screen_app_lock_biometric_unlock">"biometrisches Entsperren"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Mit Biometrie entsperren"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"Biometrische Daten bestätigen"</string>
<string name="screen_app_lock_forgot_pin">"PIN vergessen?"</string>
<string name="screen_app_lock_settings_change_pin">"PIN-Code ändern"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Biometrisches Entsperren zulassen"</string>
<string name="screen_app_lock_settings_remove_pin">"Pin entfernen"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Bist du sicher, dass du die PIN entfernen willst?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"PIN entfernen?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"%1$s zulassen"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Ich möchte diese PIN verwenden."</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Spare dir etwas Zeit und benutze %1$s, um die App zu entsperren"</string>
<string name="screen_app_lock_setup_choose_pin">"PIN wählen"</string>
<string name="screen_app_lock_setup_confirm_pin">"PIN bestätigen"</string>
<string name="screen_app_lock_setup_pin_context">"Sperre %1$s um deine Chats zusätzlich abzusichern.
Wähle eine einprägsame PIN. Wenn du sie vergisst, wirst du aus der App abgemeldet."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Aus Sicherheitsgründen kann dieser PIN-Code nicht verwendet werden."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Bitte eine andere PIN verwenden."</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Bitte gib die gleiche PIN wie zuvor ein."</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"Die PINs stimmen nicht überein"</string>
<string name="screen_app_lock_signout_alert_message">"Um fortzufahren, musst du dich erneut anmelden und eine neue PIN erstellen"</string>
<string name="screen_app_lock_signout_alert_title">"Du wirst abgemeldet"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Du hast %1$d Versuch, um zu entsperren"</item>
<item quantity="other">"Du hast %1$d Versuche, um zu entsperren"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"Falsche PIN. Du hast %1$d weiteren Versuch"</item>
<item quantity="other">"Falsche PIN. Du hast %1$d weitere Versuche"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Biometrie verwenden"</string>
<string name="screen_app_lock_use_pin_android">"PIN verwenden"</string>
<string name="screen_signout_in_progress_dialog_content">"Abmelden…"</string>
</resources>
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"βιομετρική ταυτοποίηση"</string>
<string name="screen_app_lock_biometric_unlock">"βιομετρικό ξεκλείδωμα"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Ξεκλείδωμα με βιομετρικά στοιχεία"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"Επιβεβαίωσε τον βιομετρικό έλεγχο ταυτότητας"</string>
<string name="screen_app_lock_forgot_pin">"Ξέχασες το PIN;"</string>
<string name="screen_app_lock_settings_change_pin">"Αλλαγή κωδικού PIN"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Να επιτρέπεται το βιομετρικό ξεκλείδωμα"</string>
<string name="screen_app_lock_settings_remove_pin">"Αφαίρεση PIN"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Θες σίγουρα να καταργήσεις το PIN;"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Κατάργηση PIN;"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Επέτρεψε %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Θα προτιμούσα να χρησιμοποιήσω PIN"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Εξοικονόμησε χρόνο και χρησιμοποίησε %1$s για να ξεκλειδώσεις την εφαρμογή κάθε φορά"</string>
<string name="screen_app_lock_setup_choose_pin">"Επέλεξε PIN"</string>
<string name="screen_app_lock_setup_confirm_pin">"Επιβεβαίωση PIN"</string>
<string name="screen_app_lock_setup_pin_context">"Κλειδώστε το %1$s για να προσθέσετε επιπλέον ασφάλεια στις συνομιλίες σας.
Επιλέξτε κάτι που θα θυμάστε εύκολα. Εάν ξεχάσετε αυτό το PIN, θα αποσυνδεθείτε από την εφαρμογή."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Δεν μπορείς να το επιλέξεις ως κωδικό PIN για λόγους ασφαλείας"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Επέλεξε διαφορετικό PIN"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Παρακαλώ εισήγαγε το ίδιο PIN δύο φορές"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"Τα PIN δεν ταιριάζουν"</string>
<string name="screen_app_lock_signout_alert_message">"Θα χρειαστεί να συνδεθείς ξανά και να δημιουργήσεις ένα νέο PIN για να προχωρήσεις"</string>
<string name="screen_app_lock_signout_alert_title">"Έχεις αποσυνδεθεί"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Έχετε %1$d προσπάθεια να ξεκλειδώσετε"</item>
<item quantity="other">"Έχετε %1$d προσπάθειες να ξεκλειδώσετε"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"Λάθος PIN. Έχεις %1$d ακόμη ευκαιρία"</item>
<item quantity="other">"Λάθος PIN. Έχεις %1$d ακόμη ευκαιρίες"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Χρήση βιομετρικών"</string>
<string name="screen_app_lock_use_pin_android">"Χρήση PIN"</string>
<string name="screen_signout_in_progress_dialog_content">"Αποσύνδεση…"</string>
</resources>
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"autenticación biométrica"</string>
<string name="screen_app_lock_biometric_unlock">"desbloqueo biométrico"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Desbloquear con biométrico"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"Confirmar datos biométricos"</string>
<string name="screen_app_lock_forgot_pin">"¿Olvidaste el PIN?"</string>
<string name="screen_app_lock_settings_change_pin">"Cambiar código PIN"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Permitir desbloqueo biométrico"</string>
<string name="screen_app_lock_settings_remove_pin">"Eliminar PIN"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"¿Estás seguro de que quieres eliminar el PIN?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"¿Eliminar el PIN?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Permitir %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Prefiero usar el PIN"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Ahorra algo de tiempo y usa %1$s para desbloquear la aplicación cada vez"</string>
<string name="screen_app_lock_setup_choose_pin">"Elegir PIN"</string>
<string name="screen_app_lock_setup_confirm_pin">"Confirmar PIN"</string>
<string name="screen_app_lock_setup_pin_context">"Añade un bloqueo a %1$s para añadir seguridad adicional a tus chats.
Elige algo que puedas recordar. Si olvidas este PIN, se cerrará la sesión de la aplicación."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"No puedes usar este código PIN por motivos de seguridad"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Elige un PIN diferente"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Por favor ingresa el mismo PIN dos veces"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"Los PINs no coinciden"</string>
<string name="screen_app_lock_signout_alert_message">"Tendrás que volver a iniciar sesión y crear un nuevo PIN para continuar"</string>
<string name="screen_app_lock_signout_alert_title">"Se está cerrando tu sesión"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Tienes %1$d intento de desbloqueo"</item>
<item quantity="other">"Tienes %1$d intentos de desbloqueo"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"PIN incorrecto. Tienes %1$d oportunidad más"</item>
<item quantity="other">"PIN incorrecto. Tienes %1$d oportunidades más"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Usar desbloqueo biométrico"</string>
<string name="screen_app_lock_use_pin_android">"Usar PIN"</string>
<string name="screen_signout_in_progress_dialog_content">"Cerrando sesión…"</string>
</resources>
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"biomeetrilist autentimist"</string>
<string name="screen_app_lock_biometric_unlock">"biomeetrilist lukustuse eemaldamist"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Eemalda lukustus biomeetrilise tuvastuse abil"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"Kinnita biomeetriline tuvastus"</string>
<string name="screen_app_lock_forgot_pin">"Kas unustasid PIN-koodi?"</string>
<string name="screen_app_lock_settings_change_pin">"Muuda PIN-koodi"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Kasuta lukustuse eemaldamiseks biomeetrilist tuvastust"</string>
<string name="screen_app_lock_settings_remove_pin">"Eemalda PIN-kood"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Kas sa oled kindel, et soovid eemaldada PIN-koodi?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Kas eemaldame PIN-koodi?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Kasuta %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Pigem kasutan PIN-koodi"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Säästa aega ja kasuta alati %1$s rakenduse lukustuse eemaldamiseks"</string>
<string name="screen_app_lock_setup_choose_pin">"Vali PIN-kood"</string>
<string name="screen_app_lock_setup_confirm_pin">"Korda PIN-koodi"</string>
<string name="screen_app_lock_setup_pin_context">"Lisamaks oma %1$s rakenduse vestlustele turvalisust ja privaatsust, lukusta oma nutiseade.
Vali midagi, mis hästi meelde jääb. Kui unustad selle PIN-koodi, siis turvakaalutlustel logitakse sind rakendusest välja."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Turvakaalutlustel sa ei saa sellist PIN-koodi kasutada"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Kasuta mõnda teist PIN-koodi"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Palun sisesta sama PIN-kood kaks korda"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN-koodid ei klapi omavahel"</string>
<string name="screen_app_lock_signout_alert_message">"Jätkamaks pead uuesti sisse logima ja looma uue PIN-koodi"</string>
<string name="screen_app_lock_signout_alert_title">"Sa oled logimas välja"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Sul on lukustuse eemaldamiseks jäänud %1$d katse"</item>
<item quantity="other">"Sul on lukustuse eemaldamiseks jäänud %1$d katset"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"Vale PIN-kood. Saad proovida veel %1$d korra"</item>
<item quantity="other">"Vale PIN-kood. Saad proovida veel %1$d korda"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Kasuta biomeetriat"</string>
<string name="screen_app_lock_use_pin_android">"Kasuta PIN-koodi"</string>
<string name="screen_signout_in_progress_dialog_content">"Logime välja…"</string>
</resources>
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"autentifikazio biometrikoa"</string>
<string name="screen_app_lock_biometric_unlock">"desblokeo biometrikoa"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Desblokeatu biometria bidez"</string>
<string name="screen_app_lock_forgot_pin">"PINa ahaztu duzu?"</string>
<string name="screen_app_lock_settings_change_pin">"Aldatu PIN kodea"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Baimendu desblokeo biometrikoa"</string>
<string name="screen_app_lock_settings_remove_pin">"Kendu PIN kodea"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Ziur PINa kendu nahi duzula?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"PINa kendu?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Baimendu %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Nahiago dut PINa erabili"</string>
<string name="screen_app_lock_setup_choose_pin">"Aukeratu PINa"</string>
<string name="screen_app_lock_setup_confirm_pin">"Berretsi PINa"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Segurtasun arrazoiak direla eta, ezin duzu hau aukeratu PIN kode gisa"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Aukeratu PIN ezberdin bat"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Sartu birritan PIN bera"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PINak ez datoz bat"</string>
<string name="screen_app_lock_signout_alert_message">"Saioa berriro hasi eta PIN berri bat sortu beharko duzu aurrera jarraitzeko"</string>
<string name="screen_app_lock_signout_alert_title">"Saioa amaitzen ari zara"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Saiakera %1$d duzu desblokeatzeko"</item>
<item quantity="other">"%1$d saiakera dituzu desblokeatzeko"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"PIN okerra. Aukera %1$d gehiago duzu"</item>
<item quantity="other">"Pin okerra. %1$d aukera gehiago dituzu."</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Erabili biometria"</string>
<string name="screen_app_lock_use_pin_android">"Erabili PINa"</string>
<string name="screen_signout_in_progress_dialog_content">"Saioa amaitzen…"</string>
</resources>
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"هویت‌سنجی زیستی"</string>
<string name="screen_app_lock_biometric_unlock">"قفل‌گشایی زیست‌سنجی"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"قفل‌گشایی با زیست‌سنجی"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"تأیید زیست‌سنجی"</string>
<string name="screen_app_lock_forgot_pin">"فراموشی پین؟"</string>
<string name="screen_app_lock_settings_change_pin">"تغییر کد پین"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"احازه به قفل گشایی زیست‌سنجی"</string>
<string name="screen_app_lock_settings_remove_pin">"برداشتن پین"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"مطمئنید که می‌خواهید پین را بردارید؟"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"برداشتن پین؟"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"اجازه به %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"ترجیح می‌دهم از پین استفاده کنم"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"زمانتان را ذخیره کرده و از %1$s برای قفل‌گشایی هربارهٔ کاره استفاده کنید"</string>
<string name="screen_app_lock_setup_choose_pin">"گزینش پین"</string>
<string name="screen_app_lock_setup_confirm_pin">"تأیید پین"</string>
<string name="screen_app_lock_setup_pin_context">"قفل کردن %1$s برای افزودن امنیت بیشتر به گفتگوهایتان.
چیزی به یاد ماندنی انتخاب کنید. اگر این پین را فراموش کنید، از برنامه خارج خواهید شد."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"به دلیل امنیتی نمی‌توانید این پین را برگزینید"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"گزینشی پینی متفاوت"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"لطفاً یک پین را دو بار وارد کنید"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"پین‌ها مطابق نیستند"</string>
<string name="screen_app_lock_signout_alert_message">"برای ادامه باید دوباره وارد شده و پینی جدید ایجاد کنید"</string>
<string name="screen_app_lock_signout_alert_title">"دارید خارج می‌شوید"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"شما %1$d تلاش برای باز کردن قفل دارید"</item>
<item quantity="other">"شما %1$d تلاش برای باز کردن قفل دارید"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"پین اشتباه است. شما %1$d شانس دیگر دارید"</item>
<item quantity="other">"پین اشتباه است. شما %1$d شانس دیگر دارید"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"استفاده از زیست‌سنجی"</string>
<string name="screen_app_lock_use_pin_android">"استفاده از پین"</string>
<string name="screen_signout_in_progress_dialog_content">"خارج شدن…"</string>
</resources>
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"biometrinen tunnistus"</string>
<string name="screen_app_lock_biometric_unlock">"biometrinen tunnistus"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Avaa biometrisellä"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"Vahvista biometrinen tunniste"</string>
<string name="screen_app_lock_forgot_pin">"Unohtuiko PIN-koodi?"</string>
<string name="screen_app_lock_settings_change_pin">"Vaihda PIN-koodi"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Salli biometrinen tunnistus"</string>
<string name="screen_app_lock_settings_remove_pin">"Poista PIN-koodi"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Haluatko varmasti poistaa PIN-koodin?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Poistetaanko PIN-koodi?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Salli %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Käytän mieluummin PIN-koodia"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Säästä aikaa ja ota käyttöön %1$s"</string>
<string name="screen_app_lock_setup_choose_pin">"Valitse PIN-koodi"</string>
<string name="screen_app_lock_setup_confirm_pin">"Vahvista PIN-koodi"</string>
<string name="screen_app_lock_setup_pin_context">"Lukitse %1$s -sovellus lisätäksesi turvaa keskusteluihisi.
Valitse PIN-koodi, jonka muistat. Jos unohdat sen, joudut kirjautumaan ulos."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Et voi valita tätä PIN-koodia turvallisuussyistä"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Valitse toinen PIN-koodi"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Anna sama PIN-koodi kahdesti"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN-koodit eivät täsmää"</string>
<string name="screen_app_lock_signout_alert_message">"Sinun on kirjauduttava sisään uudelleen ja luotava uusi PIN-koodi jatkaaksesi"</string>
<string name="screen_app_lock_signout_alert_title">"Sinut kirjataan ulos"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Sinulla on %1$d yritys"</item>
<item quantity="other">"Sinulla on %1$d yritystä"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"Väärä PIN-koodi. Sinulla on %1$d yritys jäljellä"</item>
<item quantity="other">"Väärä PIN-koodi. Sinulla on %1$d yritystä jäljellä"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Käytä biometristä"</string>
<string name="screen_app_lock_use_pin_android">"Käytä PIN-koodia"</string>
<string name="screen_signout_in_progress_dialog_content">"Kirjaudutaan ulos…"</string>
</resources>
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"lauthentification biométrique"</string>
<string name="screen_app_lock_biometric_unlock">"déverrouillage biométrique"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Déverrouiller avec la biométrie"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"Confirmer la biométrie"</string>
<string name="screen_app_lock_forgot_pin">"Code PIN oublié ?"</string>
<string name="screen_app_lock_settings_change_pin">"Modifier le code PIN"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Autoriser le déverrouillage biométrique"</string>
<string name="screen_app_lock_settings_remove_pin">"Supprimer le code PIN"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Êtes-vous certain de vouloir supprimer le code PIN ?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Supprimer le code PIN ?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Autoriser %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Je préfère utiliser le code PIN"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Gagnez du temps en utilisant %1$s pour déverrouiller lapplication à chaque fois."</string>
<string name="screen_app_lock_setup_choose_pin">"Choisissez un code PIN"</string>
<string name="screen_app_lock_setup_confirm_pin">"Confirmer le code PIN"</string>
<string name="screen_app_lock_setup_pin_context">"Verrouillez %1$s pour ajouter une sécurité supplémentaire à vos discussions.
Choisissez un code facile à retenir. Si vous oubliez le code PIN, vous serez déconnecté."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Vous ne pouvez pas choisir ce code PIN pour des raisons de sécurité"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Choisissez un code PIN différent"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Veuillez saisir le même code PIN deux fois"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"Les codes PIN ne correspondent pas"</string>
<string name="screen_app_lock_signout_alert_message">"Pour continuer, vous devrez vous connecter à nouveau et créer un nouveau code PIN."</string>
<string name="screen_app_lock_signout_alert_title">"Vous êtes en train de vous déconnecter"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Il reste %1$d tentative pour déverrouiller"</item>
<item quantity="other">"Il reste %1$d tentatives pour déverrouiller"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"Code PIN incorrect. Il reste %1$d tentative"</item>
<item quantity="other">"Code PIN incorrect. Il reste %1$d tentatives"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Utiliser la biométrie"</string>
<string name="screen_app_lock_use_pin_android">"Utiliser le code PIN"</string>
<string name="screen_signout_in_progress_dialog_content">"Déconnexion…"</string>
</resources>
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"biometrikus hitelesítés"</string>
<string name="screen_app_lock_biometric_unlock">"biometrikus feloldás"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Feloldás biometrikus adatokkal"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"Biometrikus megerősítés"</string>
<string name="screen_app_lock_forgot_pin">"Elfelejtette a PIN-kódot?"</string>
<string name="screen_app_lock_settings_change_pin">"PIN-kód módosítása"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Biometrikus feloldás engedélyezése"</string>
<string name="screen_app_lock_settings_remove_pin">"PIN-kód eltávolítása"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Biztos, hogy eltávolítja a PIN-kódot?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"PIN-kód eltávolítása?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"A %1$s engedélyezése"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Inkább PIN-kód használata"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Spóroljon meg némi időt, és használja a %1$st az alkalmazás feloldásához"</string>
<string name="screen_app_lock_setup_choose_pin">"PIN-kód kiválasztása"</string>
<string name="screen_app_lock_setup_confirm_pin">"PIN-kód megerősítése"</string>
<string name="screen_app_lock_setup_pin_context">"Az %1$s zárolása a csevegései nagyobb biztonsága érdekében.
Válasszon valami megjegyezhetőt. Ha elfelejti a PIN-kódot, akkor ki lesz jelentkeztetve az alkalmazásból."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Ezt biztonsági okokból nem választhatja PIN-kódként"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Válasszon egy másik PIN-kódot"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Adja meg a PIN-kódját kétszer"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"A PIN-kódok nem egyeznek"</string>
<string name="screen_app_lock_signout_alert_message">"A folytatáshoz újra be kell jelentkeznie, és létre kell hoznia egy új PIN-kódot"</string>
<string name="screen_app_lock_signout_alert_title">"Kijelentkeztetésre kerül"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"%1$d próbálkozása van a feloldáshoz"</item>
<item quantity="other">"%1$d próbálkozása van a feloldáshoz"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"Hibás PIN-kód. Még %1$d próbálkozási lehetősége maradt."</item>
<item quantity="other">"Hibás PIN-kód. Még %1$d próbálkozási lehetősége maradt."</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Biometrikus adatok használata"</string>
<string name="screen_app_lock_use_pin_android">"PIN-kód használata"</string>
<string name="screen_signout_in_progress_dialog_content">"Kijelentkezés…"</string>
</resources>
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"autentikasi biometrik"</string>
<string name="screen_app_lock_biometric_unlock">"pembukaan biometrik"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Buka kunci dengan biometrik"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"Konfirmasi biometrik"</string>
<string name="screen_app_lock_forgot_pin">"Lupa PIN?"</string>
<string name="screen_app_lock_settings_change_pin">"Ubah kode PIN"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Perbolehkan pembukaan biometrik"</string>
<string name="screen_app_lock_settings_remove_pin">"Hapus PIN"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Apakah Anda yakin ingin menghapus PIN?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Hapus PIN?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Perbolehkan %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Saya lebih suka menggunakan PIN"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Hemat waktu Anda dan gunakan %1$s untuk membuka kunci aplikasi setiap kalinya"</string>
<string name="screen_app_lock_setup_choose_pin">"Pilih PIN"</string>
<string name="screen_app_lock_setup_confirm_pin">"Konfirmasi PIN"</string>
<string name="screen_app_lock_setup_pin_context">"Kunci %1$s untuk menambahkan keamanan tambahan pada percakapan Anda.
Pilih sesuatu yang mudah untuk diingat. Jika Anda lupa PIN ini, Anda akan dikeluarkan dari aplikasi."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Anda tidak dapat memilih PIN ini demi keamanan"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Pilih PIN yang lain"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Silakan masukkan PIN yang sama dua kali"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN tidak cocok"</string>
<string name="screen_app_lock_signout_alert_message">"Anda harus masuk ulang dan membuat PIN baru untuk melanjutkan"</string>
<string name="screen_app_lock_signout_alert_title">"Anda sedang dikeluarkan"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="other">"Anda memiliki %1$d percobaan lagi untuk membuka kunci"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="other">"PIN salah. Anda memiliki %1$d percobaan lagi"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Gunakan biometrik"</string>
<string name="screen_app_lock_use_pin_android">"Gunakan PIN"</string>
<string name="screen_signout_in_progress_dialog_content">"Mengeluarkan dari akun…"</string>
</resources>
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"autenticazione biometrica"</string>
<string name="screen_app_lock_biometric_unlock">"sblocco con biometria"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Sblocca con biometria"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"Conferma la biometria"</string>
<string name="screen_app_lock_forgot_pin">"PIN dimenticato?"</string>
<string name="screen_app_lock_settings_change_pin">"Modifica il codice PIN"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Consenti lo sblocco biometrico"</string>
<string name="screen_app_lock_settings_remove_pin">"Rimuovi PIN"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Vuoi davvero rimuovere il PIN?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Rimuovere il PIN?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Consenti %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Preferisco usare il PIN"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Risparmia un po\' di tempo e usa %1$s per sbloccare l\'app ogni volta"</string>
<string name="screen_app_lock_setup_choose_pin">"Scegli il PIN"</string>
<string name="screen_app_lock_setup_confirm_pin">"Conferma il PIN"</string>
<string name="screen_app_lock_setup_pin_context">"Blocca %1$s per aggiungere una sicurezza extra alle tue conversazioni.
Scegli un PIN facile da ricordare. Se lo dimentichi, verrai disconnesso dallapp"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Non puoi scegliere questo codice PIN per motivi di sicurezza"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Scegli un PIN diverso"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Inserisci lo stesso PIN due volte"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"I PIN non corrispondono"</string>
<string name="screen_app_lock_signout_alert_message">"Dovrai effettuare nuovamente l\'accesso e creare un nuovo PIN per procedere"</string>
<string name="screen_app_lock_signout_alert_title">"Stai per essere disconnesso"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Hai %1$d tentativo di sblocco"</item>
<item quantity="other">"Hai %1$d tentativi di sblocco"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"PIN sbagliato. Hai %1$d altro tentativo"</item>
<item quantity="other">"PIN sbagliato. Hai altri %1$d tentativi"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Usa la biometria"</string>
<string name="screen_app_lock_use_pin_android">"Usa il PIN"</string>
<string name="screen_signout_in_progress_dialog_content">"Disconnessione in corso…"</string>
</resources>
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"ბიომეტრიული ავტორიზაცია"</string>
<string name="screen_app_lock_biometric_unlock">"ბიომეტრიული განბლოკვა"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"განბლოკვა ბიომეტრიით"</string>
<string name="screen_app_lock_forgot_pin">"დაგავიწყდათ PIN?"</string>
<string name="screen_app_lock_settings_change_pin">"PIN კოდის შეცვლა"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"ბიომეტრიული განბლოკვის დაშვება"</string>
<string name="screen_app_lock_settings_remove_pin">"პინ კოდის წაშლა"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"დარწმუნებული ხართ, რომ გსურთ PIN-ის წაშლა?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"გსურთ PIN-ის წაშლა?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"%1$s დაშვება"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"მირჩევნია PIN-ის გამოყენება"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"დაზოგეთ დრო და გამოიყენეთ %1$s აპლიკაციის განსაბლოკად."</string>
<string name="screen_app_lock_setup_choose_pin">"აირჩიეთ PIN"</string>
<string name="screen_app_lock_setup_confirm_pin">"დაადასტურეთ PIN"</string>
<string name="screen_app_lock_setup_pin_context">"თქვენი ჩატების დამატებითი უსაფრთხოებისათვის დაბლოკეთ %1$s.
აირჩიეთ რაიმე ისეთი, რაც დაგამახსოვრდებათ. თუ დაგავიწყდებათ ეს PIN, აპლიკაციიდან გამოხვალთ."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"თქვენ არ შეგიძლიათ აირჩიოთ ეს PIN კოდი უსაფრთხოების მიზეზების გამო"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"აირჩიეთ სხვა PIN"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"გთხოვთ შეიყვანოთ იგივე PIN ორჯერ"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN-ები არ ემთხვევა"</string>
<string name="screen_app_lock_signout_alert_message">"გასაგრძელებლად საჭიროა ხელახლა შესვლა და ახალი PIN-ის შექმნა"</string>
<string name="screen_app_lock_signout_alert_title">"თქვენ ახლა გადიხართ…"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"თქვენ გაქვთ %1$d მცდელობა განსაბლოკად"</item>
<item quantity="other">"თქვენ გაქვთ %1$d მცდელობა განსაბლოკად"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"არასწორი PIN. თქვენ %1$d შანსი დაგრჩათ"</item>
<item quantity="other">"არასწორი PIN. თქვენ %1$d შანსი დაგრჩათ"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"გამოიყენეთ ბიომეტრია"</string>
<string name="screen_app_lock_use_pin_android">"გამოიყენეთ PIN"</string>
<string name="screen_signout_in_progress_dialog_content">"გასვლა…"</string>
</resources>
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"생체 인식 인증"</string>
<string name="screen_app_lock_biometric_unlock">"생체 인식 잠금 해제"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"생체 인증으로 잠금 해제"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"생체 인식 확인"</string>
<string name="screen_app_lock_forgot_pin">"PIN을 잊으셨나요?"</string>
<string name="screen_app_lock_settings_change_pin">"PIN 코드 변경"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"생체 인식 잠금 해제 허용"</string>
<string name="screen_app_lock_settings_remove_pin">"PIN 제거"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"PIN을 제거하시겠습니까?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"PIN을 제거하시겠습니까?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"%1$s 허용"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"나는 PIN을 사용하고 싶습니다"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"시간을 절약하려면 %1$s 를 사용하여 앱을 매번 잠금 해제하세요."</string>
<string name="screen_app_lock_setup_choose_pin">"PIN을 선택하세요"</string>
<string name="screen_app_lock_setup_confirm_pin">"PIN 확인"</string>
<string name="screen_app_lock_setup_pin_context">"%1$s 를 잠그면 채팅에 추가 보안이 적용됩니다.
기억하기 쉬운 것을 선택하세요. 이 PIN을 잊어버리면 앱에서 로그아웃됩니다."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"보안상의 이유로 이 코드를 PIN 코드로 선택할 수 없습니다."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"다른 PIN을 선택하세요"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"PIN을 두 번 입력하세요."</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN이 일치하지 않습니다"</string>
<string name="screen_app_lock_signout_alert_message">"계속하려면 다시 로그인하고 새로운 PIN을 생성해야 합니다"</string>
<string name="screen_app_lock_signout_alert_title">"로그아웃 중입니다"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="other">"당신은 %1$d 회 잠금 해제 시도를 가지고 있습니다"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="other">"PIN이 잘못되었습니다. %1$d 번 남았습니다"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"생체 인증 사용"</string>
<string name="screen_app_lock_use_pin_android">"PIN 사용"</string>
<string name="screen_signout_in_progress_dialog_content">"로그아웃 중…"</string>
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_in_progress_dialog_content">"Atsijungiama…"</string>
</resources>
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"biometrisk autentisering"</string>
<string name="screen_app_lock_biometric_unlock">"biometrisk opplåsing"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Lås opp med biometri"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"Bekreft biometri"</string>
<string name="screen_app_lock_forgot_pin">"Glemt PIN-koden?"</string>
<string name="screen_app_lock_settings_change_pin">"Endre PIN-kode"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Tillat biometrisk opplåsing"</string>
<string name="screen_app_lock_settings_remove_pin">"Fjern PIN-kode"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Er du sikker på at du vil fjerne PIN-koden?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Fjerne PIN-kode?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Tillat %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Jeg vil heller bruke PIN-kode"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Spar deg selv litt tid og bruk%1$s for å låse opp appen hver gang"</string>
<string name="screen_app_lock_setup_choose_pin">"Velg PIN-kode"</string>
<string name="screen_app_lock_setup_confirm_pin">"Bekreft PIN-kode"</string>
<string name="screen_app_lock_setup_pin_context">"Lås %1$s for å legge til ekstra sikkerhet til chattene dine.
Velg noe som du husker. Hvis du glemmer denne PIN-koden, blir du logget ut av appen."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Du kan ikke velge dette som PIN-kode av sikkerhetsmessige årsaker"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Velg en annen PIN-kode"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Skriv inn samme PIN-kode to ganger"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN-kodene samsvarer ikke"</string>
<string name="screen_app_lock_signout_alert_message">"Du må logge inn på nytt og opprette en ny PIN-kode for å fortsette"</string>
<string name="screen_app_lock_signout_alert_title">"Du blir logget av"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Du har %1$d forsøk på å låse opp"</item>
<item quantity="other">"Du har %1$d forsøk på å låse opp"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"Feil PIN-kode. Du har %1$d forsøk igjen"</item>
<item quantity="other">"Feil PIN-kode. Du har %1$d forsøk igjen"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Bruk biometri"</string>
<string name="screen_app_lock_use_pin_android">"Bruk PIN-kode"</string>
<string name="screen_signout_in_progress_dialog_content">"Logger ut…"</string>
</resources>
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"biometrische authenticatie"</string>
<string name="screen_app_lock_biometric_unlock">"biometrische ontgrendeling"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Ontgrendelen met biometrie"</string>
<string name="screen_app_lock_forgot_pin">"Pincode vergeten?"</string>
<string name="screen_app_lock_settings_change_pin">"Pincode wijzigen"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Biometrische ontgrendeling toestaan"</string>
<string name="screen_app_lock_settings_remove_pin">"Pincode verwijderen"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Weet je zeker dat je de pincode wilt verwijderen?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Pincode verwijderen?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"%1$s toestaan"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Ik gebruik liever een pincode"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Bespaar jezelf tijd en gebruik %1$s om de app elke keer te ontgrendelen"</string>
<string name="screen_app_lock_setup_choose_pin">"Kies je pincode"</string>
<string name="screen_app_lock_setup_confirm_pin">"Bevestig pincode"</string>
<string name="screen_app_lock_setup_pin_context">"Vergrendel %1$s om je chats extra te beveiligen.
Kies iets dat je kunt onthouden. Als je deze pincode vergeet, word je uitgelogd bij de app."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Vanwege veiligheidsredenen kun je dit niet als je pincode kiezen"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Kies een andere pincode"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Voer dezelfde pincode twee keer in"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"Pincodes komen niet overeen"</string>
<string name="screen_app_lock_signout_alert_message">"Je moet opnieuw inloggen en een nieuwe pincode aanmaken om verder te gaan"</string>
<string name="screen_app_lock_signout_alert_title">"Je wordt uitgelogd"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Je hebt %1$d poging om te ontgrendelen"</item>
<item quantity="other">"Je hebt %1$d pogingen om te ontgrendelen"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"Verkeerde pincode. Je hebt nog %1$d kans"</item>
<item quantity="other">"Verkeerde pincode. Je hebt nog %1$d kansen"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Biometrie gebruiken"</string>
<string name="screen_app_lock_use_pin_android">"Pincode gebruiken"</string>
<string name="screen_signout_in_progress_dialog_content">"Uitloggen…"</string>
</resources>
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"uwierzytelnienie biometryczne"</string>
<string name="screen_app_lock_biometric_unlock">"odblokowanie biometryczne"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Odblokuj za pomocą biometrii"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"Potwierdź biometrię"</string>
<string name="screen_app_lock_forgot_pin">"Nie pamiętasz kodu PIN?"</string>
<string name="screen_app_lock_settings_change_pin">"Zmień kod PIN"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Zezwól na uwierzytelnienie biometryczne"</string>
<string name="screen_app_lock_settings_remove_pin">"Usuń PIN"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Czy na pewno chcesz usunąć PIN?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Usunąć PIN?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Zezwól na %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Wolę korzystać z kodu PIN"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Zaoszczędź sobie trochę czasu i korzystaj z %1$s do odblokowywania aplikacji"</string>
<string name="screen_app_lock_setup_choose_pin">"Wybierz PIN"</string>
<string name="screen_app_lock_setup_confirm_pin">"Potwierdź PIN"</string>
<string name="screen_app_lock_setup_pin_context">"Zablokuj %1$s, aby zwiększyć bezpieczeństwo swoich czatów.
Wybierz coś łatwego do zapamiętania. Jeśli zapomnisz ten PIN, zostaniesz wylogowany z aplikacji."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Nie możesz wybrać tego PIN\'u ze względów bezpieczeństwa"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Wybierz inny kod PIN"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Wprowadź ten sam kod PIN dwa razy"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN\'y nie pasują do siebie"</string>
<string name="screen_app_lock_signout_alert_message">"Aby kontynuować, zaloguj się ponownie i utwórz nowy kod PIN"</string>
<string name="screen_app_lock_signout_alert_title">"Trwa wylogowywanie"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Masz %1$d próbę, żeby odblokować"</item>
<item quantity="few">"Masz %1$d próby, żeby odblokować"</item>
<item quantity="many">"Masz %1$d prób, żeby odblokować"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"Błędny PIN. Pozostała %1$d próba"</item>
<item quantity="few">"Błędny PIN. Pozostały %1$d próby"</item>
<item quantity="many">"Błędny PIN. Pozostało %1$d prób"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Użyj biometrii"</string>
<string name="screen_app_lock_use_pin_android">"Użyj kodu PIN"</string>
<string name="screen_signout_in_progress_dialog_content">"Wylogowywanie…"</string>
</resources>
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"autenticação biométrica"</string>
<string name="screen_app_lock_biometric_unlock">"desbloqueio biométrico"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Desbloquear com biometria"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"Confirmar biometria"</string>
<string name="screen_app_lock_forgot_pin">"Esqueceu o PIN?"</string>
<string name="screen_app_lock_settings_change_pin">"Alterar código de PIN"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Permitir desbloqueio biométrico"</string>
<string name="screen_app_lock_settings_remove_pin">"Remover PIN"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Tem certeza de que quer remover o PIN?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Remover PIN?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Permitir %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Prefiro usar o PIN"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Poupe tempo e use %1$s para desbloquear o aplicativo todas as vezes"</string>
<string name="screen_app_lock_setup_choose_pin">"Escolher PIN"</string>
<string name="screen_app_lock_setup_confirm_pin">"Confirmar PIN"</string>
<string name="screen_app_lock_setup_pin_context">"Bloqueie o %1$s para adicionar uma segurança extra às suas conversas.
Escolha algo memorável. Se você esquecer este PIN, você será desconectado do app."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Você não pode escolher este PIN por razões de segurança"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Escolha um PIN diferente"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Por favor, digite o mesmo PIN duas vezes"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"Os PINs não correspondem"</string>
<string name="screen_app_lock_signout_alert_message">"Você terá que entrar novamente e criar um PIN novo para continuar"</string>
<string name="screen_app_lock_signout_alert_title">"Você está sendo desconectado"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Você tem %1$d tentativa de desbloqueio"</item>
<item quantity="other">"Você tem %1$d tentativas de desbloqueio"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"PIN incorreto. Você tem mais %1$d chance"</item>
<item quantity="other">"PIN incorreto. Você tem mais %1$d chances"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Usar biometria"</string>
<string name="screen_app_lock_use_pin_android">"Usar PIN"</string>
<string name="screen_signout_in_progress_dialog_content">"Saindo…"</string>
</resources>
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"autenticação biométrica"</string>
<string name="screen_app_lock_biometric_unlock">"desbloqueio biométrico"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Desbloquear com biometria"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"Confirmar com biometria"</string>
<string name="screen_app_lock_forgot_pin">"Esqueceste-te do PIN?"</string>
<string name="screen_app_lock_settings_change_pin">"Altera o código PIN"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Permitir o desbloqueio biométrico"</string>
<string name="screen_app_lock_settings_remove_pin">"Remover PIN"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Tens a certeza que queres remover o PIN?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Remover o PIN?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Permitir %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Prefiro usar o PIN"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Poupa tempo e utiliza %1$s para desbloquear a aplicação"</string>
<string name="screen_app_lock_setup_choose_pin">"Escolher PIN"</string>
<string name="screen_app_lock_setup_confirm_pin">"Confirmar PIN"</string>
<string name="screen_app_lock_setup_pin_context">"Bloqueia a %1$s para dar mais segurança às tuas conversas.
Escolhe algo memorável. Se te esqueceres deste PIN, a tua sessão será terminada."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Não podes escolher este código PIN por razões de segurança"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Escolhe um PIN diferente"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Insere o mesmo PIN duas vezes"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"Os PINs não coincidem"</string>
<string name="screen_app_lock_signout_alert_message">"Terás de voltar a iniciar sessão e criar um novo PIN para continuar"</string>
<string name="screen_app_lock_signout_alert_title">"Estás a terminar a sessão"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Tens %1$d tentativa de desbloqueio"</item>
<item quantity="other">"Tens %1$d tentativas de desbloqueio"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"PIN incorreto. Tens mais %1$d tentativa"</item>
<item quantity="other">"PIN incorreto. Tens mais %1$d tentativas"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Utilizar biometria"</string>
<string name="screen_app_lock_use_pin_android">"Utilizar PIN"</string>
<string name="screen_signout_in_progress_dialog_content">"A terminar sessão…"</string>
</resources>
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"autentificare biometrică"</string>
<string name="screen_app_lock_biometric_unlock">"deblocare biometrică"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Deblocați cu biometrice"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"Confirmați datele biometrice"</string>
<string name="screen_app_lock_forgot_pin">"Ați uitat codul PIN?"</string>
<string name="screen_app_lock_settings_change_pin">"Schimbați codul PIN"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Permite deblocarea biometrică"</string>
<string name="screen_app_lock_settings_remove_pin">"Ștergeți codul PIN"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Sunteți sigur că doriți să ștergeți codul PIN?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Ștergeți codul PIN?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Permiteți %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Prefer să folosesc un cod PIN"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Economisiți timp și utilizați %1$s pentru a debloca aplicația de fiecare dată."</string>
<string name="screen_app_lock_setup_choose_pin">"Alegeți codul PIN"</string>
<string name="screen_app_lock_setup_confirm_pin">"Confirmare PIN"</string>
<string name="screen_app_lock_setup_pin_context">"Blocați %1$s pentru a adăuga un plus de securitate la conversațiile dvs.
Alegeți ceva memorabil. Dacă uitați acest PIN, veți fi deconectat din aplicație."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Nu puteți alege acest cod PIN din motive de securitate"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Alegeți un alt cod PIN"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Vă rugăm să introduceți același cod PIN de două ori"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"Codurile PIN nu corespund"</string>
<string name="screen_app_lock_signout_alert_message">"Va trebui să vă reconectați și să creați un cod PIN nou pentru a continua"</string>
<string name="screen_app_lock_signout_alert_title">"Sunteți deconectat"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Aveți %1$d încercare de deblocare"</item>
<item quantity="few">"Aveți %1$d încercări de deblocare"</item>
<item quantity="other">"Aveți %1$d încercări de deblocare"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"PIN greșit. Mai aveți %1$d sansa"</item>
<item quantity="few">"PIN greșit. Mai aveți %1$d sanse"</item>
<item quantity="other">"PIN greșit. Mai aveți %1$d sanse"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Utilizați biometrice"</string>
<string name="screen_app_lock_use_pin_android">"Utilizați codul PIN"</string>
<string name="screen_signout_in_progress_dialog_content">"Deconectare în curs…"</string>
</resources>
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"биометрическая идентификация"</string>
<string name="screen_app_lock_biometric_unlock">"биометрическая разблокировка"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Разблокировать с помощью биометрии"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"Подтвердить биометрические данные"</string>
<string name="screen_app_lock_forgot_pin">"Забыли PIN-код?"</string>
<string name="screen_app_lock_settings_change_pin">"Изменить PIN-код"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Разрешить разблокировку по биометрии"</string>
<string name="screen_app_lock_settings_remove_pin">"Удалить PIN-код"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Вы действительно хотите удалить PIN-код?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Удалить PIN-код?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Разрешить %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Я бы предпочел использовать PIN-код"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Сэкономьте время и используйте %1$s для разблокировки приложения"</string>
<string name="screen_app_lock_setup_choose_pin">"Выберите PIN-код"</string>
<string name="screen_app_lock_setup_confirm_pin">"Подтвердите PIN-код"</string>
<string name="screen_app_lock_setup_pin_context">"Заблокируйте %1$s, чтобы повысить безопасность ваших чатов.
Введите что-нибудь незабываемое. Если вы забудете этот PIN-код, вы выйдете из приложения."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Из соображений безопасности вы не можешь выбрать это в качестве PIN-кода"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Выберите другой PIN-код"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Повторите PIN-код"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN-коды не совпадают"</string>
<string name="screen_app_lock_signout_alert_message">"Чтобы продолжить, вам необходимо повторно войти в систему и создать новый PIN-код"</string>
<string name="screen_app_lock_signout_alert_title">"Выполняется выход из системы"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"У вас осталась %1$d попытка на разблокировку"</item>
<item quantity="few">"У вас остались %1$d попытки на разблокировку"</item>
<item quantity="many">"У вас осталось %1$d попыток на разблокировку"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"Неверный PIN-код. У вас осталась %1$d попытка"</item>
<item quantity="few">"Неверный PIN-код. У вас остались %1$d попытки"</item>
<item quantity="many">"Неверный PIN-код. У вас осталось %1$d попыток"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Использовать биометрию"</string>
<string name="screen_app_lock_use_pin_android">"Использовать PIN-код"</string>
<string name="screen_signout_in_progress_dialog_content">"Выполняется выход…"</string>
</resources>
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"biometrické overenie"</string>
<string name="screen_app_lock_biometric_unlock">"biometrické odomknutie"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Odomknúť pomocou biometrie"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"Potvrdiť biometrické údaje"</string>
<string name="screen_app_lock_forgot_pin">"Zabudli ste PIN?"</string>
<string name="screen_app_lock_settings_change_pin">"Zmeniť PIN kód"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Povoliť biometrické odomknutie"</string>
<string name="screen_app_lock_settings_remove_pin">"Odstrániť PIN"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Ste si istí, že chcete odstrániť PIN?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Odstrániť PIN?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Povoliť %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Radšej použijem PIN"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Ušetrite si čas a použite zakaždým %1$s na odomknutie aplikácie"</string>
<string name="screen_app_lock_setup_choose_pin">"Vyberte PIN"</string>
<string name="screen_app_lock_setup_confirm_pin">"Potvrdiť PIN"</string>
<string name="screen_app_lock_setup_pin_context">"Uzamknite %1$s, aby ste zvýšili bezpečnosť svojich konverzácií.
Vyberte si niečo zapamätateľné. Ak tento kód PIN zabudnete, budete z aplikácie odhlásení."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Z bezpečnostných dôvodov si nemôžete toto zvoliť ako svoj PIN kód."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Vyberte iný PIN"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Zadajte prosím ten istý PIN dvakrát"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN kódy sa nezhodujú"</string>
<string name="screen_app_lock_signout_alert_message">"Ak chcete pokračovať, musíte sa znovu prihlásiť a vytvoriť nový PIN kód."</string>
<string name="screen_app_lock_signout_alert_title">"Prebieha odhlasovanie"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Máte %1$d pokus na odomknutie"</item>
<item quantity="few">"Máte %1$d pokusy na odomknutie"</item>
<item quantity="other">"Máte %1$d pokusov na odomknutie"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"Nesprávny PIN kód. Máte ešte %1$d pokus"</item>
<item quantity="few">"Nesprávny PIN kód. Máte ešte %1$d pokusy"</item>
<item quantity="other">"Nesprávny PIN kód. Máte ešte %1$d pokusov"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Použiť biometrické údaje"</string>
<string name="screen_app_lock_use_pin_android">"Použiť PIN"</string>
<string name="screen_signout_in_progress_dialog_content">"Prebieha odhlasovanie…"</string>
</resources>
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"biometrisk autentisering"</string>
<string name="screen_app_lock_biometric_unlock">"biometrisk upplåsning"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Lås upp med biometri"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"Bekräfta biometriskt"</string>
<string name="screen_app_lock_forgot_pin">"Glömt PIN-kod?"</string>
<string name="screen_app_lock_settings_change_pin">"Byt PIN-kod"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Tillåt biometrisk upplåsning"</string>
<string name="screen_app_lock_settings_remove_pin">"Ta bort PIN-kod"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Är du säker på att du vill ta bort PIN-koden?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Ta bort PIN-koden?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Tillåt %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Jag vill hellre använda PIN-kod"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Bespara dig själv lite tid och använd %1$s för att låsa upp appen varje gång"</string>
<string name="screen_app_lock_setup_choose_pin">"Välj PIN-kod"</string>
<string name="screen_app_lock_setup_confirm_pin">"Bekräfta PIN-kod"</string>
<string name="screen_app_lock_setup_pin_context">"Lås %1$s för att lägga till extra säkerhet i dina chattar.
Välj något minnesvärt. Om du glömmer den här PIN-koden loggas du ut från appen."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Du kan inte välja detta som din PIN-kod av säkerhetsskäl"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Välj en annan PIN-kod"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Ange samma PIN-kod två gånger"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN-koder matchar inte"</string>
<string name="screen_app_lock_signout_alert_message">"Du måste logga in igen och skapa en ny PIN-kod för att fortsätta"</string>
<string name="screen_app_lock_signout_alert_title">"Du blir utloggad"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Du har %1$d försök att låsa upp"</item>
<item quantity="other">"Du har %1$d försök att låsa upp"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"Fel PIN-kod. Du har %1$d försök kvar"</item>
<item quantity="other">"Fel PIN-kod. Du har %1$d försök kvar"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Använd biometri"</string>
<string name="screen_app_lock_use_pin_android">"Använd PIN-kod"</string>
<string name="screen_signout_in_progress_dialog_content">"Loggar ut …"</string>
</resources>
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"biyometrik kimlik doğrulama"</string>
<string name="screen_app_lock_biometric_unlock">"biyometrik kilit açma"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Biyometrik ile kilit aç"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"Biyometrik doğrulama"</string>
<string name="screen_app_lock_forgot_pin">"PIN\'i unuttum"</string>
<string name="screen_app_lock_settings_change_pin">"PIN kodunu değiştir"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Biyometrik kilit açmaya izin ver"</string>
<string name="screen_app_lock_settings_remove_pin">"PIN kodunu kaldır"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"PIN\'i kaldırmak istediğinizden emin misiniz?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"PIN\'i kaldır?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"İzin ver %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"PIN kullanmayı tercih ederim"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"%1$s kullanarak oturum açarken kendinize zaman kazandırın"</string>
<string name="screen_app_lock_setup_choose_pin">"PIN Seç"</string>
<string name="screen_app_lock_setup_confirm_pin">"PIN\'i onayla"</string>
<string name="screen_app_lock_setup_pin_context">"Sohbetlerinize ekstra güvenlik eklemek için %1$s kilitleyin.
Hatırlanabilir bir şey seçin. Bu PIN\'i unutursanız, uygulamadan çıkış yaparsınız."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Güvenlik nedeniyle bunu PIN kodunuz olarak seçemezsiniz"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Farklı bir PIN seçin"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Lütfen aynı PIN\'i iki kez girin"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN\'ler eşleşmiyor"</string>
<string name="screen_app_lock_signout_alert_message">"Devam etmek için yeniden oturum açmanız ve yeni bir PIN oluşturmanız gerekir"</string>
<string name="screen_app_lock_signout_alert_title">"Oturumunuz kapatılıyor"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Kilidi açmak için %1$d deneme hakkınız var"</item>
<item quantity="other">"Kilidi açmak için %1$d deneme hakkınız var"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"Yanlış PIN. %1$d kere daha şansınız var"</item>
<item quantity="other">"Yanlış PIN. %1$d kere daha şansınız var"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Biyometrik kullan"</string>
<string name="screen_app_lock_use_pin_android">"PIN kullan"</string>
<string name="screen_signout_in_progress_dialog_content">"Oturum kapatılıyor…"</string>
</resources>
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"біометрична автентифікація"</string>
<string name="screen_app_lock_biometric_unlock">"біометричне розблокування"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Розблокувати за допомогою біометрії"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"Підтвердити біометрію"</string>
<string name="screen_app_lock_forgot_pin">"Забули PIN-код?"</string>
<string name="screen_app_lock_settings_change_pin">"Змінити PIN-код"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Дозволити біометричне розблокування"</string>
<string name="screen_app_lock_settings_remove_pin">"Вилучити PIN-код"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Ви впевнені, що хочете видалити PIN-код?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Видалити PIN-код?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Дозволити %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Мені краще використати PIN-код"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Заощаджуйте час і використовуйте %1$s для розблокування застосунку щоразу"</string>
<string name="screen_app_lock_setup_choose_pin">"Виберіть PIN-код"</string>
<string name="screen_app_lock_setup_confirm_pin">"Підтвердити PIN-код"</string>
<string name="screen_app_lock_setup_pin_context">"Заблокуйте %1$s, щоб додати додаткову безпеку вашим чатам.
Виберіть щось, що запам\'ятовується. Але якщо ви забудете PIN-код, ви вийдете з застосунку."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Ви не можете вибрати його своїм PIN-кодом з міркувань безпеки"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Виберіть інший PIN-код"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Будь ласка, введіть один і той самий PIN-код двічі"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN-коди не збігаються"</string>
<string name="screen_app_lock_signout_alert_message">"Щоб продовжити, вам потрібно повторно ввійти та створити новий PIN-код"</string>
<string name="screen_app_lock_signout_alert_title">"Ви виходите з системи"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Ви маєте %1$d спробу"</item>
<item quantity="few">"Ви маєте %1$d спроби"</item>
<item quantity="many">"Ви маєте %1$d спроб"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"Хибний PIN-код. Ви маєте ще %1$d шанс"</item>
<item quantity="few">"Хибний PIN-код. Ви маєте ще %1$d шанси"</item>
<item quantity="many">"Хибний PIN-код. Ви маєте ще %1$d шансів"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Використати біометрію"</string>
<string name="screen_app_lock_use_pin_android">"Використати PIN-код"</string>
<string name="screen_signout_in_progress_dialog_content">"Вихід…"</string>
</resources>
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"زیست سنجی تصدیق"</string>
<string name="screen_app_lock_biometric_unlock">"زیست سنجی فتحِ قفل"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"زیست سنجی کے ساتھ فتح قفل کریں"</string>
<string name="screen_app_lock_forgot_pin">"PIN بھول گئے؟"</string>
<string name="screen_app_lock_settings_change_pin">"PIN رمز بدلیں"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"زیست سنجی فتحِ قفل کی اجازت دیں"</string>
<string name="screen_app_lock_settings_remove_pin">"PIN ہٹائیں"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"کیا آپ کو یقین ہے کہ آپ PIN ہٹانا چاہتے ہیں؟"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"PIN ہٹائیں؟"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"%1$s کی اجازت دیں"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"میں اس کے بجائے PIN استعمال کروں گا"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"اپنے آپ کو کچھ وقت بچائیں اور ہر بار اطلاقیے کو غیر مقفل کرنے کے لئے %1$s کا استعمال کریں۔"</string>
<string name="screen_app_lock_setup_choose_pin">"PIN چنیں"</string>
<string name="screen_app_lock_setup_confirm_pin">"PIN کی تصدیق کریں"</string>
<string name="screen_app_lock_setup_pin_context">"اپنی گفتگوہا میں اضافی سلامتی شامل کرنے کیلئے %1$s مقفل کریں۔
کوئی یادگار چیز چنیں۔ اگر آپ اس PIN کو بھول گئے، آپ طلاقیے سے خارج ہوجائیں گے۔"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"حفاظتی وجوہات کی بنا پر آپ اسے اپنے PIN رمز کے طور پر منتخب نہیں کر سکتے"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"ایک مختلف PIN چنیں"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"برائے مہربانی ایک ہی PIN دو بار درج کریں۔"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PINs مماثل نہیں ہیں"</string>
<string name="screen_app_lock_signout_alert_message">"آگے بڑھنے کیلئے آپکو دوبارہ داخل ہونے اور ایک نیا PIN بنانے کی ضرورت ہوگی۔"</string>
<string name="screen_app_lock_signout_alert_title">"آپکو خارج کیا جا رہا ہے"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"آپکے پاس %1$d غیر مقفل کرنے کی کوشش ہے"</item>
<item quantity="other">"آپکے پاس %1$d غیر مقفل کرنے کی کوششیں ہیں"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"غلط PIN۔ آپ کے پاس %1$d مزید موقع ہے"</item>
<item quantity="other">"غلط PIN۔ آپ کے پاس %1$d مزید موقعے ہیں"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"زیست سنجی استعمال کریں"</string>
<string name="screen_app_lock_use_pin_android">"PIN استعمال کریں"</string>
<string name="screen_signout_in_progress_dialog_content">"خارج ہورہاہے…"</string>
</resources>
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"biometrik autentifikatsiya"</string>
<string name="screen_app_lock_biometric_unlock">"biometrik qulf ochish"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Biometrik bilan qulfni oching"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"Biometrikni tasdiqlang"</string>
<string name="screen_app_lock_forgot_pin">"PIN kodni unutdingizmi?"</string>
<string name="screen_app_lock_settings_change_pin">"PIN kodni o\'zgartirish"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Biometrik qulfni ochishga ruxsat bering"</string>
<string name="screen_app_lock_settings_remove_pin">"PIN-kodni olib tashlang"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Haqiqatan ham PIN kodni olib tashlamoqchimisiz?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"PIN kod olib tashlansinmi?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Ruxsat berish %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Men PIN kod ishlatishni maʼqul koʻraman"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Oʻzingizga vaqt tejang va har safar ilovani ochish uchun %1$s dan foydalaning"</string>
<string name="screen_app_lock_setup_choose_pin">"PIN kodni tanlang"</string>
<string name="screen_app_lock_setup_confirm_pin">"PIN kodni tasdiqlang"</string>
<string name="screen_app_lock_setup_pin_context">"Qulflash %1$s suhbatlaringizga qoʻshimcha xavfsizlik qoʻshish uchun.
Esda qoladigan biror narsani tanlang. Agar ushbu PIN kodni unutib qolsangiz, dasturdan chiqib ketasiz."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Xavfsizlik sabablari bilan buni PIN kodingiz sifatida tanlay olmaysiz"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Boshqa PIN kod tanlang"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Iltimos, bir xil PIN kodni ikkita marta kiriting"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN kodlar bir-biriga mos kelmadi"</string>
<string name="screen_app_lock_signout_alert_message">"Davom etish uchun qayta kirishingiz va yangi PIN yaratishingiz kerak boʻladi."</string>
<string name="screen_app_lock_signout_alert_title">"Siz tizimdan chiqmoqdasiz"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Sizda %1$d ta ochishga urinish mavjud"</item>
<item quantity="other">"Sizda %1$d ta ochishga urinish mavjud"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"Notoʻgʻri PIN. Sizda yana %1$d ta imkoniyat bor"</item>
<item quantity="other">"Notoʻgʻri PIN. Sizda yana %1$d ta imkoniyat bor"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Biometrikdan foydalaning"</string>
<string name="screen_app_lock_use_pin_android">"PIN koddan foydalaning"</string>
<string name="screen_signout_in_progress_dialog_content">"Chiqish…"</string>
</resources>
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"生物辨識認證"</string>
<string name="screen_app_lock_biometric_unlock">"生物辨識解鎖"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"使用生物辨識解鎖"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"確認生物辨識"</string>
<string name="screen_app_lock_forgot_pin">"忘記 PIN 碼?"</string>
<string name="screen_app_lock_settings_change_pin">"變更 PIN 碼"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"允許生物辨識解鎖"</string>
<string name="screen_app_lock_settings_remove_pin">"移除 PIN 碼"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"您確定要移除 PIN 碼嗎?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"移除 PIN 碼"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"允許 %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"我想使用 PIN 碼"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"為自己節省一些時間,使用 %1$s 來解鎖應用程式"</string>
<string name="screen_app_lock_setup_choose_pin">"選擇 PIN 碼"</string>
<string name="screen_app_lock_setup_confirm_pin">"確認 PIN 碼"</string>
<string name="screen_app_lock_setup_pin_context">"將 %1$s 上鎖,為你的聊天室添加一層防護。
請選擇好記憶的數字。如果忘記 PIN 碼,您會被登出。"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"基於安全性的考量,您選的 PIN 碼無法使用"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"選擇不一樣的 PIN 碼"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"請輸入相同的 PIN 碼兩次"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN 碼不一樣"</string>
<string name="screen_app_lock_signout_alert_message">"您需要重新登入並建立新的 PIN 碼才能繼續"</string>
<string name="screen_app_lock_signout_alert_title">"您即將登出"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="other">"您有 %1$d 次解鎖的機會"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="other">"PIN 碼錯誤。您還有 %1$d 次機會"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"使用生物辨識"</string>
<string name="screen_app_lock_use_pin_android">"使用 PIN 碼"</string>
<string name="screen_signout_in_progress_dialog_content">"正在登出…"</string>
</resources>
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"生物识别认证"</string>
<string name="screen_app_lock_biometric_unlock">"生物识别解锁"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"使用生物识别解锁"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"确认生物特征"</string>
<string name="screen_app_lock_forgot_pin">"忘记 PIN 码?"</string>
<string name="screen_app_lock_settings_change_pin">"更改 PIN 码"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"允许生物识别解锁"</string>
<string name="screen_app_lock_settings_remove_pin">"移除 PIN 码"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"您确定要删除 PIN 码吗?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"移除 PIN 码?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"允许 %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"我宁愿使用 PIN 码"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"节省时间,用 %1$s 来解锁应用程序"</string>
<string name="screen_app_lock_setup_choose_pin">"选择 PIN 码"</string>
<string name="screen_app_lock_setup_confirm_pin">"确认 PIN 码"</string>
<string name="screen_app_lock_setup_pin_context">"锁定 %1$s 以为聊天增加安全性。
选择好记的 PIN 码。如果忘掉了这个 PIN 码,就不得不登出应用。"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"出于安全原因,您不能选择这个 PIN 码"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"选择不同的 PIN 码"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"请输入两次相同的 PIN 码"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN 码不匹配"</string>
<string name="screen_app_lock_signout_alert_message">"您需要重新登录并创建新的 PIN 才能继续"</string>
<string name="screen_app_lock_signout_alert_title">"您正在登出"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="other">"还剩 %1$d 次解锁机会"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="other">"PIN 码错误。还剩 %1$d 次机会"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"使用生物识别"</string>
<string name="screen_app_lock_use_pin_android">"使用 PIN 码"</string>
<string name="screen_signout_in_progress_dialog_content">"正在登出…"</string>
</resources>
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<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_title_android">"Unlock with biometric"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"Confirm biometric"</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_enable_biometric_unlock">"Allow biometric unlock"</string>
<string name="screen_app_lock_settings_remove_pin">"Remove PIN"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Are you sure you want to remove PIN?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Remove PIN?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Allow %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Id rather use PIN"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Save yourself some time and use %1$s to unlock the app each time"</string>
<string name="screen_app_lock_setup_choose_pin">"Choose PIN"</string>
<string name="screen_app_lock_setup_confirm_pin">"Confirm PIN"</string>
<string name="screen_app_lock_setup_pin_context">"Lock %1$s to add extra security to your chats.
Choose something memorable. If you forget this PIN, you will be logged out of the app."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"You cannot choose this as your PIN code for security reasons"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Choose a different PIN"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Please enter the same PIN twice"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PINs don\'t match"</string>
<string name="screen_app_lock_signout_alert_message">"Youll 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>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"You have %1$d attempt to unlock"</item>
<item quantity="other">"You have %1$d attempts to unlock"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"Wrong PIN. You have %1$d more chance"</item>
<item quantity="other">"Wrong PIN. You have %1$d more chances"</item>
</plurals>
<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>
</resources>
@@ -0,0 +1,26 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat
import io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class DefaultLockScreenEntryPointIntentTest {
@Test
fun `test pin unlock intent`() {
val entryPoint = DefaultLockScreenEntryPoint()
val result = entryPoint.pinUnlockIntent(InstrumentationRegistry.getInstrumentation().context)
assertThat(result.component?.className).isEqualTo(PinUnlockActivity::class.qualifiedName)
}
}
@@ -0,0 +1,75 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
import com.google.common.truth.Truth.assertThat
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.node.TestParentNode
import org.junit.Rule
import org.junit.Test
class DefaultLockScreenEntryPointTest {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
@Test
fun `test node builder Setup`() {
val entryPoint = DefaultLockScreenEntryPoint()
val parentNode = TestParentNode.create { buildContext, plugins ->
LockScreenFlowNode(
buildContext = buildContext,
plugins = plugins,
)
}
val callback = object : LockScreenEntryPoint.Callback {
override fun onSetupDone() = lambdaError()
}
val navTarget = LockScreenEntryPoint.Target.Setup
val result = entryPoint.createNode(
parentNode = parentNode,
buildContext = BuildContext.root(null),
navTarget = navTarget,
callback = callback,
)
assertThat(result).isInstanceOf(LockScreenFlowNode::class.java)
assertThat(result.plugins).contains(LockScreenFlowNode.Inputs(LockScreenFlowNode.NavTarget.Setup))
assertThat(result.plugins).contains(callback)
}
@Test
fun `test node builder Settings`() {
val entryPoint = DefaultLockScreenEntryPoint()
val parentNode = TestParentNode.create { buildContext, plugins ->
LockScreenFlowNode(
buildContext = buildContext,
plugins = plugins,
)
}
val callback = object : LockScreenEntryPoint.Callback {
override fun onSetupDone() = lambdaError()
}
val navTarget = LockScreenEntryPoint.Target.Settings
val result = entryPoint.createNode(
parentNode = parentNode,
buildContext = BuildContext.root(null),
navTarget = navTarget,
callback = callback,
)
assertThat(result).isInstanceOf(LockScreenFlowNode::class.java)
assertThat(result.plugins).contains(LockScreenFlowNode.Inputs(LockScreenFlowNode.NavTarget.Settings))
assertThat(result.plugins).contains(callback)
}
}
@@ -0,0 +1,96 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager
import io.element.android.features.lockscreen.impl.biometric.FakeBiometricAuthenticatorManager
import io.element.android.features.lockscreen.impl.fixtures.aLockScreenConfig
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
import io.element.android.features.lockscreen.impl.pin.createDefaultPinCodeManager
import io.element.android.features.lockscreen.impl.pin.storage.InMemoryLockScreenStore
import io.element.android.features.lockscreen.impl.storage.LockScreenStore
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
import io.element.android.libraries.sessionstorage.test.observer.FakeSessionObserver
import io.element.android.services.appnavstate.api.AppForegroundStateService
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
class DefaultLockScreenServiceTest {
@Test
fun `when the pin is not mandatory and no pin is configured isSetupRequired emits false`() = runTest {
val sut = createDefaultLockScreenService(
lockScreenConfig = aLockScreenConfig(isPinMandatory = false)
)
sut.isSetupRequired().test {
assertThat(awaitItem()).isFalse()
}
}
@Test
fun `when the pin is mandatory, isSetupRequired emits true`() = runTest {
val lockScreenStore = InMemoryLockScreenStore()
val sut = createDefaultLockScreenService(
lockScreenConfig = aLockScreenConfig(isPinMandatory = true),
lockScreenStore = lockScreenStore,
)
sut.isSetupRequired().test {
assertThat(awaitItem()).isTrue()
// When the user configures the pin code, the setup is not required anymore
lockScreenStore.saveEncryptedPinCode("encryptedCode")
assertThat(awaitItem()).isFalse()
// Users deletes the pin code
lockScreenStore.deleteEncryptedPinCode()
assertThat(awaitItem()).isTrue()
}
}
@Test
fun `when the last session is deleted, the pin code is removed`() = runTest {
val sessionObserver = FakeSessionObserver()
val lockScreenStore = InMemoryLockScreenStore()
val sut = createDefaultLockScreenService(
lockScreenConfig = aLockScreenConfig(isPinMandatory = true),
lockScreenStore = lockScreenStore,
sessionObserver = sessionObserver,
)
sut.isPinSetup().test {
assertThat(awaitItem()).isFalse()
// When the user configure the pin code, the setup is not required anymore
lockScreenStore.saveEncryptedPinCode("encryptedCode")
assertThat(awaitItem()).isTrue()
sessionObserver.onSessionDeleted("userId", wasLastSession = false)
expectNoEvents()
sessionObserver.onSessionDeleted("userId", wasLastSession = true)
assertThat(awaitItem()).isFalse()
}
}
}
private fun TestScope.createDefaultLockScreenService(
lockScreenConfig: LockScreenConfig = aLockScreenConfig(),
lockScreenStore: LockScreenStore = InMemoryLockScreenStore(),
pinCodeManager: PinCodeManager = createDefaultPinCodeManager(
lockScreenStore = lockScreenStore,
),
sessionObserver: SessionObserver = FakeSessionObserver(),
appForegroundStateService: AppForegroundStateService = FakeAppForegroundStateService(),
biometricAuthenticatorManager: BiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(),
) = DefaultLockScreenService(
lockScreenConfig = lockScreenConfig,
lockScreenStore = lockScreenStore,
pinCodeManager = pinCodeManager,
coroutineScope = backgroundScope,
sessionObserver = sessionObserver,
appForegroundStateService = appForegroundStateService,
biometricAuthenticatorManager = biometricAuthenticatorManager,
)
@@ -0,0 +1,17 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.biometric
class FakeBiometricAuthenticator(
override val isActive: Boolean = false,
private val authenticateLambda: suspend () -> BiometricAuthenticator.AuthenticationResult = { BiometricAuthenticator.AuthenticationResult.Success },
) : BiometricAuthenticator {
override fun setup() = Unit
override suspend fun authenticate() = authenticateLambda()
}
@@ -0,0 +1,40 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.biometric
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
class FakeBiometricAuthenticatorManager(
override var isDeviceSecured: Boolean = true,
override var hasAvailableAuthenticator: Boolean = false,
private val createBiometricAuthenticator: () -> BiometricAuthenticator = { FakeBiometricAuthenticator() },
) : BiometricAuthenticatorManager {
override fun addCallback(callback: BiometricAuthenticator.Callback) {
// no-op
}
override fun removeCallback(callback: BiometricAuthenticator.Callback) {
// no-op
}
@Composable
override fun rememberUnlockBiometricAuthenticator(): BiometricAuthenticator {
return remember {
createBiometricAuthenticator()
}
}
@Composable
override fun rememberConfirmBiometricAuthenticator(): BiometricAuthenticator {
return remember {
createBiometricAuthenticator()
}
}
}
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.fixtures
import io.element.android.features.lockscreen.impl.LockScreenConfig
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
internal fun aLockScreenConfig(
isPinMandatory: Boolean = false,
forbiddenPinCodes: Set<String> = emptySet(),
pinSize: Int = 4,
maxPinCodeAttemptsBeforeLogout: Int = 3,
gracePeriod: Duration = 3.seconds,
isStrongBiometricsEnabled: Boolean = true,
isWeakBiometricsEnabled: Boolean = true,
): LockScreenConfig {
return LockScreenConfig(
isPinMandatory = isPinMandatory,
forbiddenPinCodes = forbiddenPinCodes,
pinSize = pinSize,
maxPinCodeAttemptsBeforeLogout = maxPinCodeAttemptsBeforeLogout,
gracePeriod = gracePeriod,
isStrongBiometricsEnabled = isStrongBiometricsEnabled,
isWeakBiometricsEnabled = isWeakBiometricsEnabled,
)
}
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.fixtures
import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManager
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
import io.element.android.features.lockscreen.impl.pin.storage.InMemoryLockScreenStore
import io.element.android.features.lockscreen.impl.storage.LockScreenStore
import io.element.android.libraries.cryptography.api.EncryptionDecryptionService
import io.element.android.libraries.cryptography.impl.AESEncryptionDecryptionService
import io.element.android.libraries.cryptography.test.SimpleSecretKeyRepository
internal fun aPinCodeManager(
lockScreenStore: LockScreenStore = InMemoryLockScreenStore(),
secretKeyRepository: SimpleSecretKeyRepository = SimpleSecretKeyRepository(),
encryptionDecryptionService: EncryptionDecryptionService = AESEncryptionDecryptionService(),
): PinCodeManager {
return DefaultPinCodeManager(secretKeyRepository, encryptionDecryptionService, lockScreenStore)
}
@@ -0,0 +1,59 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.pin
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 io.element.android.libraries.cryptography.api.EncryptionDecryptionService
import io.element.android.libraries.cryptography.api.SecretKeyRepository
import io.element.android.libraries.cryptography.impl.AESEncryptionDecryptionService
import io.element.android.libraries.cryptography.test.SimpleSecretKeyRepository
import kotlinx.coroutines.test.runTest
import org.junit.Test
class DefaultPinCodeManagerTest {
@Test
fun `given a pin code when create and delete assert no pin code left`() = runTest {
val pinCodeManager = createDefaultPinCodeManager()
pinCodeManager.hasPinCode().test {
assertThat(awaitItem()).isFalse()
pinCodeManager.createPinCode("1234")
assertThat(awaitItem()).isTrue()
pinCodeManager.deletePinCode()
assertThat(awaitItem()).isFalse()
}
}
@Test
fun `given a pin code when create and verify with the same pin succeed`() = runTest {
val pinCodeManager = createDefaultPinCodeManager()
val pinCode = "1234"
pinCodeManager.createPinCode(pinCode)
assertThat(pinCodeManager.verifyPinCode(pinCode)).isTrue()
}
@Test
fun `given a pin code when create and verify with a different pin fails`() = runTest {
val pinCodeManager = createDefaultPinCodeManager()
pinCodeManager.createPinCode("1234")
assertThat(pinCodeManager.verifyPinCode("1235")).isFalse()
}
}
fun createDefaultPinCodeManager(
lockScreenStore: LockScreenStore = InMemoryLockScreenStore(),
secretKeyRepository: SecretKeyRepository = SimpleSecretKeyRepository(),
encryptionDecryptionService: EncryptionDecryptionService = AESEncryptionDecryptionService(),
) = DefaultPinCodeManager(
lockScreenStore = lockScreenStore,
secretKeyRepository = secretKeyRepository,
encryptionDecryptionService = encryptionDecryptionService,
)
@@ -0,0 +1,20 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.pin.model
import com.google.common.truth.Truth.assertThat
fun PinEntry.assertText(text: String) {
assertThat(toText()).isEqualTo(text)
}
fun PinEntry.assertEmpty() {
val isEmpty = digits.all { it is PinDigit.Empty }
assertThat(isEmpty).isTrue()
}
@@ -0,0 +1,72 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.pin.model
import com.google.common.truth.Truth.assertThat
import org.junit.Test
class PinEntryTest {
@Test
fun `when using fillWith with empty string ensure pin is empty`() {
val pinEntry = PinEntry.createEmpty(4)
val newPinEntry = pinEntry.fillWith("")
assertThat(newPinEntry.isEmpty()).isTrue()
}
@Test
fun `when using fillWith with bigger string than size ensure pin is complete`() {
val pinEntry = PinEntry.createEmpty(4)
val newPinEntry = pinEntry.fillWith("12345")
assertThat(newPinEntry.isComplete()).isTrue()
newPinEntry.assertText("1234")
}
@Test
fun `when using fillWith with non digit string ensure pin is filtering`() {
val pinEntry = PinEntry.createEmpty(4)
val newPinEntry = pinEntry.fillWith("12aa")
newPinEntry.assertText("12")
}
@Test
fun `when using clear ensure pin is empty`() {
val pinEntry = PinEntry.createEmpty(4)
val newPinEntry = pinEntry.clear()
assertThat(newPinEntry.isEmpty()).isTrue()
assertThat(newPinEntry.isComplete()).isFalse()
newPinEntry.assertText("")
}
@Test
fun `when using deleteLast ensure pin correct`() {
val pinEntry = PinEntry.createEmpty(4)
val newPinEntry = pinEntry.fillWith("1234").deleteLast()
newPinEntry.assertText("123")
}
@Test
fun `when using deleteLast with empty pin ensure pin is empty`() {
val pinEntry = PinEntry.createEmpty(4)
val newPinEntry = pinEntry.deleteLast()
assertThat(newPinEntry.isEmpty()).isTrue()
}
@Test
fun `when using addDigit with complete pin ensure pin is complete`() {
val pinEntry = PinEntry.createEmpty(4)
val newPinEntry = pinEntry
.addDigit('1')
.addDigit('2')
.addDigit('3')
.addDigit('4')
.addDigit('5')
assertThat(newPinEntry.isComplete()).isTrue()
newPinEntry.assertText("1234")
}
}
@@ -0,0 +1,62 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.pin.storage
import io.element.android.features.lockscreen.impl.storage.LockScreenStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
private const val DEFAULT_REMAINING_ATTEMPTS = 3
class InMemoryLockScreenStore : LockScreenStore {
private val hasPinCode = MutableStateFlow(false)
private var pinCode: String? = null
set(value) {
field = value
hasPinCode.value = value != null
}
private var remainingAttempts: Int = DEFAULT_REMAINING_ATTEMPTS
private var isBiometricUnlockAllowed = MutableStateFlow(false)
override suspend fun getRemainingPinCodeAttemptsNumber(): Int {
return remainingAttempts
}
override suspend fun onWrongPin() {
remainingAttempts--
}
override suspend fun resetCounter() {
remainingAttempts = DEFAULT_REMAINING_ATTEMPTS
}
override suspend fun getEncryptedCode(): String? {
return pinCode
}
override suspend fun saveEncryptedPinCode(pinCode: String) {
this.pinCode = pinCode
}
override suspend fun deleteEncryptedPinCode() {
pinCode = null
}
override fun hasPinCode(): Flow<Boolean> {
return hasPinCode
}
override fun isBiometricUnlockAllowed(): Flow<Boolean> {
return isBiometricUnlockAllowed
}
override suspend fun setIsBiometricUnlockAllowed(isAllowed: Boolean) {
isBiometricUnlockAllowed.value = isAllowed
}
}
@@ -0,0 +1,164 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.settings
import com.google.common.truth.Truth.assertThat
import io.element.android.features.lockscreen.impl.LockScreenConfig
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticator
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager
import io.element.android.features.lockscreen.impl.biometric.FakeBiometricAuthenticator
import io.element.android.features.lockscreen.impl.biometric.FakeBiometricAuthenticatorManager
import io.element.android.features.lockscreen.impl.fixtures.aLockScreenConfig
import io.element.android.features.lockscreen.impl.fixtures.aPinCodeManager
import io.element.android.features.lockscreen.impl.pin.storage.InMemoryLockScreenStore
import io.element.android.features.lockscreen.impl.storage.LockScreenStore
import io.element.android.tests.testutils.awaitLastSequentialItem
import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.element.android.tests.testutils.test
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
class LockScreenSettingsPresenterTest {
@Test
fun `present - remove pin option is hidden when mandatory`() = runTest {
val presenter = createLockScreenSettingsPresenter(lockScreenConfig = aLockScreenConfig(isPinMandatory = true))
presenter.test {
awaitItem().also { state ->
assertThat(state.showRemovePinOption).isFalse()
}
}
}
@Test
fun `present - remove pin flow`() = runTest {
val presenter = createLockScreenSettingsPresenter()
presenter.test {
consumeItemsUntilPredicate { state ->
state.showRemovePinOption
}.last().also { state ->
state.eventSink(LockScreenSettingsEvents.OnRemovePin)
}
awaitLastSequentialItem().also { state ->
assertThat(state.showRemovePinConfirmation).isTrue()
state.eventSink(LockScreenSettingsEvents.CancelRemovePin)
}
awaitLastSequentialItem().also { state ->
assertThat(state.showRemovePinConfirmation).isFalse()
state.eventSink(LockScreenSettingsEvents.OnRemovePin)
}
awaitLastSequentialItem().also { state ->
assertThat(state.showRemovePinConfirmation).isTrue()
state.eventSink(LockScreenSettingsEvents.ConfirmRemovePin)
}
consumeItemsUntilPredicate {
it.showRemovePinOption.not()
}.last().also { state ->
assertThat(state.showRemovePinConfirmation).isFalse()
assertThat(state.showRemovePinOption).isFalse()
}
}
}
@Test
fun `present - show toggle biometric if device is secured`() = runTest {
val fakeBiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(
isDeviceSecured = true,
)
val presenter = createLockScreenSettingsPresenter(
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
)
presenter.test {
skipItems(1)
assertThat(awaitItem().showToggleBiometric).isTrue()
}
}
@Test
fun `present - enable biometric unlock success`() = runTest {
val fakeBiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(
createBiometricAuthenticator = {
FakeBiometricAuthenticator(authenticateLambda = { BiometricAuthenticator.AuthenticationResult.Success })
}
)
val presenter = createLockScreenSettingsPresenter(
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
)
presenter.test {
skipItems(1)
awaitItem().also { state ->
state.eventSink(LockScreenSettingsEvents.ToggleBiometricAllowed)
}
awaitItem().also { state ->
assertThat(state.isBiometricEnabled).isTrue()
}
}
}
@Test
fun `present - enable biometric unlock failure`() = runTest {
val fakeBiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(
createBiometricAuthenticator = {
FakeBiometricAuthenticator(authenticateLambda = { BiometricAuthenticator.AuthenticationResult.Failure() })
}
)
val presenter = createLockScreenSettingsPresenter(
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
)
presenter.test {
skipItems(1)
awaitItem().also { state ->
state.eventSink(LockScreenSettingsEvents.ToggleBiometricAllowed)
}
}
}
@Test
fun `present - disable biometric unlock`() = runTest {
val fakeBiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(
createBiometricAuthenticator = {
FakeBiometricAuthenticator(authenticateLambda = { BiometricAuthenticator.AuthenticationResult.Failure() })
}
)
val lockScreenStore = InMemoryLockScreenStore()
val presenter = createLockScreenSettingsPresenter(
lockScreenStore = lockScreenStore,
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
)
lockScreenStore.setIsBiometricUnlockAllowed(true)
presenter.test {
skipItems(1)
awaitItem().also { state ->
assertThat(state.isBiometricEnabled).isTrue()
state.eventSink(LockScreenSettingsEvents.ToggleBiometricAllowed)
}
awaitItem().also { state ->
assertThat(state.isBiometricEnabled).isFalse()
}
}
}
private suspend fun TestScope.createLockScreenSettingsPresenter(
lockScreenConfig: LockScreenConfig = aLockScreenConfig(),
biometricAuthenticatorManager: BiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(),
lockScreenStore: LockScreenStore = InMemoryLockScreenStore(),
): LockScreenSettingsPresenter {
val pinCodeManager = aPinCodeManager(lockScreenStore = lockScreenStore).apply {
createPinCode("1234")
}
return LockScreenSettingsPresenter(
lockScreenStore = lockScreenStore,
pinCodeManager = pinCodeManager,
coroutineScope = this,
lockScreenConfig = lockScreenConfig,
biometricAuthenticatorManager = biometricAuthenticatorManager,
)
}
}

Some files were not shown because too many files have changed in this diff Show More