forked from dsutanto/bChot-android
First Commit
This commit is contained in:
@@ -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>
|
||||
+46
@@ -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)
|
||||
}
|
||||
}
|
||||
+121
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
@@ -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,
|
||||
)
|
||||
}
|
||||
+81
@@ -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()
|
||||
}
|
||||
}
|
||||
+128
@@ -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
|
||||
}
|
||||
}
|
||||
+38
@@ -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
|
||||
}
|
||||
+30
@@ -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)
|
||||
}
|
||||
}
|
||||
+169
@@ -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
|
||||
}
|
||||
}
|
||||
+15
@@ -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
|
||||
}
|
||||
+125
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+88
@@ -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()
|
||||
}
|
||||
}
|
||||
+17
@@ -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
|
||||
}
|
||||
+78
@@ -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
|
||||
}
|
||||
+24
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+79
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
@@ -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
|
||||
}
|
||||
+127
@@ -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()
|
||||
}
|
||||
}
|
||||
+45
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+89
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+17
@@ -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
|
||||
)
|
||||
+33
@@ -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 = {}
|
||||
)
|
||||
+96
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
+105
@@ -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()
|
||||
}
|
||||
}
|
||||
+14
@@ -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
|
||||
}
|
||||
+49
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+59
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+14
@@ -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
|
||||
)
|
||||
+25
@@ -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 = {}
|
||||
)
|
||||
+88
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+14
@@ -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
|
||||
}
|
||||
+37
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+113
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+27
@@ -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
|
||||
}
|
||||
}
|
||||
+51
@@ -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 = {}
|
||||
)
|
||||
+155
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
+31
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -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
|
||||
}
|
||||
+37
@@ -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>
|
||||
}
|
||||
+38
@@ -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)
|
||||
}
|
||||
+92
@@ -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
|
||||
}
|
||||
+21
@@ -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
|
||||
}
|
||||
+48
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+52
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+181
@@ -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)
|
||||
}
|
||||
}
|
||||
+42
@@ -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
|
||||
}
|
||||
+56
@@ -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 = {}
|
||||
)
|
||||
+377
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+84
@@ -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)
|
||||
}
|
||||
}
|
||||
+18
@@ -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)
|
||||
}
|
||||
+227
@@ -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 = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
@@ -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">"l’authentification 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 l’application à 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 dall’app"</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">"I’d 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">"You’ll need to re-login and create a new PIN to proceed"</string>
|
||||
<string name="screen_app_lock_signout_alert_title">"You are being signed out"</string>
|
||||
<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>
|
||||
+26
@@ -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)
|
||||
}
|
||||
}
|
||||
+75
@@ -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)
|
||||
}
|
||||
}
|
||||
+96
@@ -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,
|
||||
)
|
||||
+17
@@ -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()
|
||||
}
|
||||
+40
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
@@ -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,
|
||||
)
|
||||
}
|
||||
+25
@@ -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)
|
||||
}
|
||||
+59
@@ -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,
|
||||
)
|
||||
+20
@@ -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()
|
||||
}
|
||||
+72
@@ -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")
|
||||
}
|
||||
}
|
||||
+62
@@ -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
|
||||
}
|
||||
}
|
||||
+164
@@ -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
Reference in New Issue
Block a user