diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt index 0c147eba16..c35ae1d418 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt @@ -1,5 +1,6 @@ package chat.simplex.app +import SectionItemView import android.app.Application import android.content.Intent import android.net.Uri @@ -12,8 +13,7 @@ import androidx.activity.viewModels import androidx.compose.animation.core.* import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface +import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Lock import androidx.compose.runtime.* @@ -21,12 +21,13 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.fragment.app.FragmentActivity import androidx.lifecycle.* -import chat.simplex.app.model.ChatModel -import chat.simplex.app.model.NtfManager +import chat.simplex.app.MainActivity.Companion.enteredBackground +import chat.simplex.app.model.* import chat.simplex.app.model.NtfManager.Companion.getUserIdFromIntent import chat.simplex.app.ui.theme.SimpleButton import chat.simplex.app.ui.theme.SimpleXTheme @@ -37,8 +38,11 @@ import chat.simplex.app.views.chat.ChatView import chat.simplex.app.views.chatlist.* import chat.simplex.app.views.database.DatabaseErrorView import chat.simplex.app.views.helpers.* +import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword +import chat.simplex.app.views.localauth.SetAppPasscodeView import chat.simplex.app.views.newchat.* import chat.simplex.app.views.onboarding.* +import chat.simplex.app.views.usersettings.LAMode import kotlinx.coroutines.* import kotlinx.coroutines.flow.distinctUntilChanged @@ -93,7 +97,7 @@ class MainActivity: FragmentActivity() { laFailed, ::runAuthenticate, ::setPerformLA, - showLANotice = { m.controller.showLANotice(this) } + showLANotice = { showLANotice(m.controller.appPrefs.laNoticeShown, this) } ) } } @@ -111,7 +115,8 @@ class MainActivity: FragmentActivity() { override fun onResume() { super.onResume() val enteredBackgroundVal = enteredBackground.value - if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= 30_000) { + val delay = vm.chatModel.controller.appPrefs.laLockDelay.get() + if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= delay * 1000) { runAuthenticate() } } @@ -165,16 +170,27 @@ class MainActivity: FragmentActivity() { delay(50) withContext(Dispatchers.Main) { authenticate( - generalGetString(R.string.auth_unlock), - generalGetString(R.string.auth_log_in_using_credential), + if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM) + generalGetString(R.string.auth_unlock) + else + generalGetString(R.string.la_enter_app_passcode), + if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM) + generalGetString(R.string.auth_log_in_using_credential) + else + generalGetString(R.string.auth_unlock), this@MainActivity, completed = { laResult -> when (laResult) { LAResult.Success -> userAuthorized.value = true - is LAResult.Error, LAResult.Failed -> + is LAResult.Failed -> { /* Can be called multiple times on every failure */ } + is LAResult.Error -> { laFailed.value = true - LAResult.Unavailable -> { + if (m.controller.appPrefs.laMode.get() == LAMode.PASSCODE) { + laFailedAlert() + } + } + is LAResult.Unavailable -> { userAuthorized.value = true m.performLA.value = false m.controller.appPrefs.performLA.set(false) @@ -188,21 +204,116 @@ class MainActivity: FragmentActivity() { } } - private fun setPerformLA(on: Boolean) { - vm.chatModel.controller.appPrefs.laNoticeShown.set(true) - if (on) { - enableLA() - } else { - disableLA() + private fun showLANotice(laNoticeShown: SharedPreference, activity: FragmentActivity) { + Log.d(TAG, "showLANotice") + if (!laNoticeShown.get()) { + laNoticeShown.set(true) + AlertManager.shared.showAlertDialog( + title = generalGetString(R.string.la_notice_title_simplex_lock), + text = generalGetString(R.string.la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled), + confirmText = generalGetString(R.string.la_notice_turn_on), + onConfirm = { + withBGApi { // to remove this call, change ordering of onConfirm call in AlertManager + showChooseLAMode(laNoticeShown, activity) + } + } + ) } } - private fun enableLA() { + private fun showChooseLAMode(laNoticeShown: SharedPreference, activity: FragmentActivity) { + Log.d(TAG, "showLANotice") + laNoticeShown.set(true) + AlertManager.shared.showAlertDialogStacked( + title = generalGetString(R.string.la_lock_mode), + text = null, + confirmText = generalGetString(R.string.la_lock_mode_passcode), + dismissText = generalGetString(R.string.la_lock_mode_system), + onConfirm = { + AlertManager.shared.hideAlert() + setPasscode() + }, + onDismiss = { + AlertManager.shared.hideAlert() + initialEnableLA(activity) + } + ) + } + + private fun initialEnableLA(activity: FragmentActivity) { val m = vm.chatModel + val appPrefs = m.controller.appPrefs + m.controller.appPrefs.laMode.set(LAMode.SYSTEM) authenticate( generalGetString(R.string.auth_enable_simplex_lock), generalGetString(R.string.auth_confirm_credential), - this@MainActivity, + activity, + completed = { laResult -> + when (laResult) { + LAResult.Success -> { + m.performLA.value = true + appPrefs.performLA.set(true) + laTurnedOnAlert() + } + is LAResult.Failed -> { /* Can be called multiple times on every failure */ } + is LAResult.Error -> { + m.performLA.value = false + appPrefs.performLA.set(false) + laFailedAlert() + } + is LAResult.Unavailable -> { + m.performLA.value = false + appPrefs.performLA.set(false) + m.showAdvertiseLAUnavailableAlert.value = true + } + } + } + ) + } + + private fun setPasscode() { + val chatModel = vm.chatModel + val appPrefs = chatModel.controller.appPrefs + ModalManager.shared.showCustomModal { close -> + Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { + SetAppPasscodeView( + submit = { + chatModel.performLA.value = true + appPrefs.performLA.set(true) + appPrefs.laMode.set(LAMode.PASSCODE) + laTurnedOnAlert() + }, + cancel = { + chatModel.performLA.value = false + appPrefs.performLA.set(false) + laPasscodeNotSetAlert() + }, + close) + } + } + } + + private fun setPerformLA(on: Boolean, activity: FragmentActivity) { + vm.chatModel.controller.appPrefs.laNoticeShown.set(true) + if (on) { + enableLA(activity) + } else { + disableLA(activity) + } + } + + private fun enableLA(activity: FragmentActivity) { + val m = vm.chatModel + authenticate( + if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM) + generalGetString(R.string.auth_enable_simplex_lock) + else + generalGetString(R.string.new_passcode), + if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM) + generalGetString(R.string.auth_confirm_credential) + else + "", + activity, completed = { laResult -> val prefPerformLA = m.controller.appPrefs.performLA when (laResult) { @@ -211,11 +322,13 @@ class MainActivity: FragmentActivity() { prefPerformLA.set(true) laTurnedOnAlert() } - is LAResult.Error, LAResult.Failed -> { + is LAResult.Failed -> { /* Can be called multiple times on every failure */ } + is LAResult.Error -> { m.performLA.value = false prefPerformLA.set(false) + laFailedAlert() } - LAResult.Unavailable -> { + is LAResult.Unavailable -> { m.performLA.value = false prefPerformLA.set(false) laUnavailableInstructionAlert() @@ -225,24 +338,33 @@ class MainActivity: FragmentActivity() { ) } - private fun disableLA() { + private fun disableLA(activity: FragmentActivity) { val m = vm.chatModel authenticate( - generalGetString(R.string.auth_disable_simplex_lock), - generalGetString(R.string.auth_confirm_credential), - this@MainActivity, + if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM) + generalGetString(R.string.auth_disable_simplex_lock) + else + generalGetString(R.string.la_enter_app_passcode), + if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM) + generalGetString(R.string.auth_confirm_credential) + else + generalGetString(R.string.auth_disable_simplex_lock), + activity, completed = { laResult -> val prefPerformLA = m.controller.appPrefs.performLA when (laResult) { LAResult.Success -> { m.performLA.value = false prefPerformLA.set(false) + ksAppPassword.remove() } - is LAResult.Error, LAResult.Failed -> { + is LAResult.Failed -> { /* Can be called multiple times on every failure */ } + is LAResult.Error -> { m.performLA.value = true prefPerformLA.set(true) + laFailedAlert() } - LAResult.Unavailable -> { + is LAResult.Unavailable -> { m.performLA.value = false prefPerformLA.set(false) laUnavailableTurningOffAlert() @@ -264,7 +386,7 @@ fun MainPage( userAuthorized: MutableState, laFailed: MutableState, runAuthenticate: () -> Unit, - setPerformLA: (Boolean) -> Unit, + setPerformLA: (Boolean, FragmentActivity) -> Unit, showLANotice: () -> Unit ) { var showChatDatabaseError by rememberSaveable { @@ -392,6 +514,14 @@ fun MainPage( if (invitation != null) IncomingCallAlertView(invitation, chatModel) AlertManager.shared.showInView() } + + DisposableEffectOnRotate { + // When using lock delay = 0 and screen rotates, the app will be locked which is not useful. + // Let's prolong the unlocked period to 3 sec for screen rotation to take place + if (chatModel.controller.appPrefs.laLockDelay.get() == 0) { + enteredBackground.value = elapsedRealtime() + 3000 + } + } } fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt index 4f5eb464b5..63e31cb226 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt @@ -326,4 +326,4 @@ class SimplexService: Service() { private fun getPreferences(context: Context): SharedPreferences = context.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE) } -} \ No newline at end of file +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index b5fd441123..a817c402e5 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -20,7 +20,6 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.fragment.app.FragmentActivity import chat.simplex.app.* import chat.simplex.app.R import chat.simplex.app.ui.theme.* @@ -84,6 +83,8 @@ class AppPreferences(val context: Context) { set = fun(action: CallOnLockScreen) { _callOnLockScreen.set(action.name) } ) val performLA = mkBoolPreference(SHARED_PREFS_PERFORM_LA, false) + val laMode = mkEnumPreference(SHARED_PREFS_LA_MODE, LAMode.SYSTEM) { LAMode.values().firstOrNull { it.name == this } } + val laLockDelay = mkIntPreference(SHARED_PREFS_LA_LOCK_DELAY, 30) val laNoticeShown = mkBoolPreference(SHARED_PREFS_LA_NOTICE_SHOWN, false) val webrtcIceServers = mkStrPreference(SHARED_PREFS_WEBRTC_ICE_SERVERS, null) val privacyProtectScreen = mkBoolPreference(SHARED_PREFS_PRIVACY_PROTECT_SCREEN, true) @@ -142,6 +143,8 @@ class AppPreferences(val context: Context) { val initialRandomDBPassphrase = mkBoolPreference(SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE, false) val encryptedDBPassphrase = mkStrPreference(SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE, null) val initializationVectorDBPassphrase = mkStrPreference(SHARED_PREFS_INITIALIZATION_VECTOR_DB_PASSPHRASE, null) + val encryptedAppPassphrase = mkStrPreference(SHARED_PREFS_ENCRYPTED_APP_PASSPHRASE, null) + val initializationVectorAppPassphrase = mkStrPreference(SHARED_PREFS_INITIALIZATION_VECTOR_APP_PASSPHRASE, null) val encryptionStartedAt = mkDatePreference(SHARED_PREFS_ENCRYPTION_STARTED_AT, null, true) val confirmDBUpgrades = mkBoolPreference(SHARED_PREFS_CONFIRM_DB_UPGRADES, false) @@ -184,6 +187,12 @@ class AppPreferences(val context: Context) { set = fun(value) = sharedPreferences.edit().putString(prefName, value).apply() ) + private fun mkEnumPreference(prefName: String, default: T, construct: String.() -> T?): SharedPreference = + SharedPreference( + get = fun() = sharedPreferences.getString(prefName, default.toString())?.construct() ?: default, + set = fun(value) = sharedPreferences.edit().putString(prefName, value.toString()).apply() + ) + /** * Provide `[commit] = true` to save preferences right now, not after some unknown period of time. * So in case of a crash this value will be saved 100% @@ -210,6 +219,8 @@ class AppPreferences(val context: Context) { private const val SHARED_PREFS_WEBRTC_POLICY_RELAY = "WebrtcPolicyRelay" private const val SHARED_PREFS_WEBRTC_CALLS_ON_LOCK_SCREEN = "CallsOnLockScreen" private const val SHARED_PREFS_PERFORM_LA = "PerformLA" + private const val SHARED_PREFS_LA_MODE = "LocalAuthenticationMode" + private const val SHARED_PREFS_LA_LOCK_DELAY = "LocalAuthenticationLockDelay" private const val SHARED_PREFS_LA_NOTICE_SHOWN = "LANoticeShown" private const val SHARED_PREFS_WEBRTC_ICE_SERVERS = "WebrtcICEServers" private const val SHARED_PREFS_PRIVACY_PROTECT_SCREEN = "PrivacyProtectScreen" @@ -246,6 +257,8 @@ class AppPreferences(val context: Context) { private const val SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE = "InitialRandomDBPassphrase" private const val SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE = "EncryptedDBPassphrase" private const val SHARED_PREFS_INITIALIZATION_VECTOR_DB_PASSPHRASE = "InitializationVectorDBPassphrase" + private const val SHARED_PREFS_ENCRYPTED_APP_PASSPHRASE = "EncryptedAppPassphrase" + private const val SHARED_PREFS_INITIALIZATION_VECTOR_APP_PASSPHRASE = "InitializationVectorAppPassphrase" private const val SHARED_PREFS_ENCRYPTION_STARTED_AT = "EncryptionStartedAt" private const val SHARED_PREFS_CONFIRM_DB_UPGRADES = "ConfirmDBUpgrades" private const val SHARED_PREFS_CURRENT_THEME = "CurrentTheme" @@ -1707,43 +1720,6 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a ) } - fun showLANotice(activity: FragmentActivity) { - Log.d(TAG, "showLANotice") - if (!appPrefs.laNoticeShown.get()) { - appPrefs.laNoticeShown.set(true) - AlertManager.shared.showAlertDialog( - title = generalGetString(R.string.la_notice_title_simplex_lock), - text = generalGetString(R.string.la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled), - confirmText = generalGetString(R.string.la_notice_turn_on), - onConfirm = { - authenticate( - generalGetString(R.string.auth_enable_simplex_lock), - generalGetString(R.string.auth_confirm_credential), - activity, - completed = { laResult -> - when (laResult) { - LAResult.Success -> { - chatModel.performLA.value = true - appPrefs.performLA.set(true) - laTurnedOnAlert() - } - is LAResult.Error, LAResult.Failed -> { - chatModel.performLA.value = false - appPrefs.performLA.set(false) - } - LAResult.Unavailable -> { - chatModel.performLA.value = false - appPrefs.performLA.set(false) - chatModel.showAdvertiseLAUnavailableAlert.value = true - } - } - } - ) - } - ) - } - } - fun isIgnoringBatteryOptimizations(context: Context): Boolean { val powerManager = context.getSystemService(Application.POWER_SERVICE) as PowerManager return powerManager.isIgnoringBatteryOptimizations(context.packageName) @@ -3669,4 +3645,4 @@ sealed class XFTPErrorType { @Serializable @SerialName("HAS_FILE") object HAS_FILE: XFTPErrorType() @Serializable @SerialName("FILE_IO") object FILE_IO: XFTPErrorType() @Serializable @SerialName("INTERNAL") object INTERNAL: XFTPErrorType() -} \ No newline at end of file +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Shape.kt b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Shape.kt index d0a00450f6..79ab4eead4 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Shape.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Shape.kt @@ -8,4 +8,4 @@ val Shapes = Shapes( small = RoundedCornerShape(4.dp), medium = RoundedCornerShape(4.dp), large = RoundedCornerShape(0.dp) -) \ No newline at end of file +) diff --git a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt index 7590ecdcfb..8b05e28925 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt @@ -74,4 +74,4 @@ fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) { shapes = Shapes, content = content ) -} \ No newline at end of file +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt index b60df05f9e..fbd765213e 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt @@ -148,4 +148,4 @@ fun ProfileNameField(name: MutableState, focusRequester: FocusRequester? singleLine = true, cursorBrush = SolidColor(HighOrLowlight) ) -} \ No newline at end of file +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/call/CallManager.kt b/apps/android/app/src/main/java/chat/simplex/app/views/call/CallManager.kt index 2a4b397840..a88f07d3c5 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/call/CallManager.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/call/CallManager.kt @@ -106,4 +106,4 @@ class CallManager(val chatModel: ChatModel) { chatModel.controller.ntfManager.cancelCallNotification() } } -} \ No newline at end of file +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/call/SoundPlayer.kt b/apps/android/app/src/main/java/chat/simplex/app/views/call/SoundPlayer.kt index 009ffdf6e1..456f50d0bd 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/call/SoundPlayer.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/call/SoundPlayer.kt @@ -48,4 +48,4 @@ class SoundPlayer { companion object { val shared = SoundPlayer() } -} \ No newline at end of file +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/VerifyCodeView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/VerifyCodeView.kt index fd88ccdc34..58c13e8bb5 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/VerifyCodeView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/VerifyCodeView.kt @@ -134,4 +134,4 @@ private fun splitToParts(s: String, length: Int): String { return (0..(s.length - 1) / length) .map { s.drop(it * length).take(length) } .joinToString(separator = "\n") -} \ No newline at end of file +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CICallItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CICallItemView.kt index 5ab80774c6..4c9bbae077 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CICallItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CICallItemView.kt @@ -154,4 +154,4 @@ fun AcceptCallButton(cInfo: ChatInfo, acceptCall: (Contact) -> Unit) { // Image(systemName: "phone.arrow.down.left").foregroundColor(.secondary) // } // } -//} \ No newline at end of file +//} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt index 86647a0ef1..89f0a07743 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.unit.* +import androidx.fragment.app.FragmentActivity import chat.simplex.app.* import chat.simplex.app.R import chat.simplex.app.model.* @@ -36,7 +37,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch @Composable -fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped: Boolean) { +fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean, FragmentActivity) -> Unit, stopped: Boolean) { val newChatSheetState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) } val userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) } val showNewChatSheet = { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseEncryptionView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseEncryptionView.kt index 7a616301b7..ff6229e9e6 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseEncryptionView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseEncryptionView.kt @@ -42,9 +42,9 @@ fun DatabaseEncryptionView(m: ChatModel) { val prefs = m.controller.appPrefs val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) } val initialRandomDBPassphrase = remember { mutableStateOf(prefs.initialRandomDBPassphrase.get()) } - val storedKey = remember { val key = DatabaseUtils.getDatabaseKey(); mutableStateOf(key != null && key != "") } + val storedKey = remember { val key = DatabaseUtils.ksDatabasePassword.get(); mutableStateOf(key != null && key != "") } // Do not do rememberSaveable on current key to prevent saving it on disk in clear text - val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.getDatabaseKey() ?: "" else "") } + val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.ksDatabasePassword.get() ?: "" else "") } val newKey = rememberSaveable { mutableStateOf("") } val confirmNewKey = rememberSaveable { mutableStateOf("") } @@ -89,7 +89,7 @@ fun DatabaseEncryptionView(m: ChatModel) { prefs.initialRandomDBPassphrase.set(false) initialRandomDBPassphrase.value = false if (useKeychain.value) { - DatabaseUtils.setDatabaseKey(newKey.value) + DatabaseUtils.ksDatabasePassword.set(newKey.value) } resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value) operationEnded(m, progressIndicator) { @@ -150,7 +150,7 @@ fun DatabaseEncryptionLayout( text = generalGetString(R.string.notifications_will_be_hidden) + "\n" + storeSecurelyDanger(), confirmText = generalGetString(R.string.remove_passphrase), onConfirm = { - DatabaseUtils.removeDatabaseKey() + DatabaseUtils.ksDatabasePassword.remove() setUseKeychain(false, useKeychain, prefs) storedKey.value = false }, @@ -522,4 +522,4 @@ fun PreviewDatabaseEncryptionLayout() { onConfirmEncrypt = {}, ) } -} \ No newline at end of file +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt index 05a8288ada..a59e2820be 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt @@ -38,7 +38,7 @@ fun DatabaseErrorView( ) { val progressIndicator = remember { mutableStateOf(false) } val dbKey = remember { mutableStateOf("") } - var storedDBKey by remember { mutableStateOf(DatabaseUtils.getDatabaseKey()) } + var storedDBKey by remember { mutableStateOf(DatabaseUtils.ksDatabasePassword.get()) } var useKeychain by remember { mutableStateOf(appPreferences.storeDBPassphrase.get()) } val context = LocalContext.current val restoreDbFromBackup = remember { mutableStateOf(shouldShowRestoreDbButton(appPreferences, context)) } @@ -49,7 +49,7 @@ fun DatabaseErrorView( } fun saveAndRunChatOnClick() { - DatabaseUtils.setDatabaseKey(dbKey.value) + DatabaseUtils.ksDatabasePassword.set(dbKey.value) storedDBKey = dbKey.value appPreferences.storeDBPassphrase.set(true) useKeychain = true diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt index 4c9bcf6b9f..869a52cd23 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt @@ -52,7 +52,7 @@ fun DatabaseView( ) { val context = LocalContext.current val progressIndicator = remember { mutableStateOf(false) } - val runChat = remember { mutableStateOf(m.chatRunning.value ?: true) } + val runChat = remember { m.chatRunning } val prefs = m.controller.appPrefs val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) } val chatArchiveName = remember { mutableStateOf(prefs.chatArchiveName.get()) } @@ -76,7 +76,7 @@ fun DatabaseView( ) { DatabaseLayout( progressIndicator.value, - runChat.value, + runChat.value != false, m.chatDbChanged.value, useKeychain.value, m.chatDbEncrypted.value, @@ -388,7 +388,7 @@ fun chatArchiveTitle(chatArchiveTime: Instant, chatLastStart: Instant): String { return stringResource(if (chatArchiveTime < chatLastStart) R.string.old_database_archive else R.string.new_database_archive) } -private fun startChat(m: ChatModel, runChat: MutableState, chatLastStart: MutableState, chatDbChanged: MutableState) { +private fun startChat(m: ChatModel, runChat: MutableState, chatLastStart: MutableState, chatDbChanged: MutableState) { withApi { try { if (chatDbChanged.value) { @@ -417,7 +417,7 @@ private fun startChat(m: ChatModel, runChat: MutableState, chatLastStar } } -private fun stopChatAlert(m: ChatModel, runChat: MutableState, context: Context) { +private fun stopChatAlert(m: ChatModel, runChat: MutableState, context: Context) { AlertManager.shared.showAlertDialog( title = generalGetString(R.string.stop_chat_question), text = generalGetString(R.string.stop_chat_to_export_import_or_delete_chat_database), @@ -434,7 +434,7 @@ private fun exportProhibitedAlert() { ) } -private fun authStopChat(m: ChatModel, runChat: MutableState, context: Context) { +private fun authStopChat(m: ChatModel, runChat: MutableState, context: Context) { if (m.controller.appPrefs.performLA.get()) { authenticate( generalGetString(R.string.auth_stop_chat), @@ -442,12 +442,13 @@ private fun authStopChat(m: ChatModel, runChat: MutableState, context: context as FragmentActivity, completed = { laResult -> when (laResult) { - LAResult.Success, LAResult.Unavailable -> { + LAResult.Success, is LAResult.Unavailable -> { stopChat(m, runChat, context) } is LAResult.Error -> { + runChat.value = true } - LAResult.Failed -> { + is LAResult.Failed -> { runChat.value = true } } @@ -458,7 +459,7 @@ private fun authStopChat(m: ChatModel, runChat: MutableState, context: } } -private fun stopChat(m: ChatModel, runChat: MutableState, context: Context) { +private fun stopChat(m: ChatModel, runChat: MutableState, context: Context) { withApi { try { m.controller.apiStopChat() @@ -592,7 +593,7 @@ private fun importArchive( try { val config = ArchiveConfig(archivePath, parentTempDirectory = context.cacheDir.toString()) m.controller.apiImportArchive(config) - DatabaseUtils.removeDatabaseKey() + DatabaseUtils.ksDatabasePassword.remove() appFilesCountAndSize.value = directoryFileCountAndSize(getAppFilesDirectory(context)) operationEnded(m, progressIndicator) { AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_imported), generalGetString(R.string.restart_the_app_to_use_imported_chat_database)) @@ -647,7 +648,7 @@ private fun deleteChat(m: ChatModel, progressIndicator: MutableState) { try { m.controller.apiDeleteStorage() m.chatDbDeleted.value = true - DatabaseUtils.removeDatabaseKey() + DatabaseUtils.ksDatabasePassword.remove() m.controller.appPrefs.storeDBPassphrase.set(true) operationEnded(m, progressIndicator) { AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_deleted), generalGetString(R.string.restart_the_app_to_create_a_new_chat_profile)) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DatabaseUtils.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DatabaseUtils.kt index b723c69465..c82f2773d4 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DatabaseUtils.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DatabaseUtils.kt @@ -3,6 +3,7 @@ package chat.simplex.app.views.helpers import android.util.Log import chat.simplex.app.* import chat.simplex.app.model.AppPreferences +import chat.simplex.app.model.SharedPreference import chat.simplex.app.views.usersettings.Cryptor import kotlinx.serialization.* import java.io.File @@ -16,30 +17,36 @@ object DatabaseUtils { } private const val DATABASE_PASSWORD_ALIAS: String = "databasePassword" + private const val APP_PASSWORD_ALIAS: String = "appPassword" + + val ksDatabasePassword = KeyStoreItem(DATABASE_PASSWORD_ALIAS, appPreferences.encryptedDBPassphrase, appPreferences.initializationVectorDBPassphrase) + val ksAppPassword = KeyStoreItem(APP_PASSWORD_ALIAS, appPreferences.encryptedAppPassphrase, appPreferences.initializationVectorAppPassphrase) + + class KeyStoreItem(private val alias: String, val passphrase: SharedPreference, val initVector: SharedPreference) { + fun get(): String? { + return cryptor.decryptData( + passphrase.get()?.toByteArrayFromBase64() ?: return null, + initVector.get()?.toByteArrayFromBase64() ?: return null, + alias, + ) + } + + fun set(key: String) { + val data = cryptor.encryptText(key, alias) + passphrase.set(data.first.toBase64String()) + initVector.set(data.second.toBase64String()) + } + + fun remove() { + cryptor.deleteKey(alias) + passphrase.set(null) + initVector.set(null) + } + } private fun hasDatabase(rootDir: String): Boolean = File(rootDir + File.separator + "files_chat.db").exists() && File(rootDir + File.separator + "files_agent.db").exists() - fun getDatabaseKey(): String? { - return cryptor.decryptData( - appPreferences.encryptedDBPassphrase.get()?.toByteArrayFromBase64() ?: return null, - appPreferences.initializationVectorDBPassphrase.get()?.toByteArrayFromBase64() ?: return null, - DATABASE_PASSWORD_ALIAS, - ) - } - - fun setDatabaseKey(key: String) { - val data = cryptor.encryptText(key, DATABASE_PASSWORD_ALIAS) - appPreferences.encryptedDBPassphrase.set(data.first.toBase64String()) - appPreferences.initializationVectorDBPassphrase.set(data.second.toBase64String()) - } - - fun removeDatabaseKey() { - cryptor.deleteKey(DATABASE_PASSWORD_ALIAS) - appPreferences.encryptedDBPassphrase.set(null) - appPreferences.initializationVectorDBPassphrase.set(null) - } - fun useDatabaseKey(): String { Log.d(TAG, "useDatabaseKey ${appPreferences.storeDBPassphrase.get()}") var dbKey = "" @@ -47,10 +54,10 @@ object DatabaseUtils { if (useKeychain) { if (!hasDatabase(SimplexApp.context.dataDir.absolutePath)) { dbKey = randomDatabasePassword() - setDatabaseKey(dbKey) + ksDatabasePassword.set(dbKey) appPreferences.initialRandomDBPassphrase.set(true) } else { - dbKey = getDatabaseKey() ?: "" + dbKey = ksDatabasePassword.get() ?: "" } } return dbKey @@ -101,4 +108,4 @@ data class UpMigration( sealed class MTRError { @Serializable @SerialName("noDown") class NoDown(val dbMigrations: List): MTRError() @Serializable @SerialName("different") class Different(val appMigration: String, val dbMigration: String): MTRError() -} \ No newline at end of file +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DefaultBasicTextField.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DefaultBasicTextField.kt index 51c9870897..18253a3a69 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DefaultBasicTextField.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DefaultBasicTextField.kt @@ -225,4 +225,4 @@ fun DefaultConfigurableTextField( } } } -} \ No newline at end of file +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/LocalAuthentication.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/LocalAuthentication.kt index eb52ed137a..818f6b247b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/LocalAuthentication.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/LocalAuthentication.kt @@ -1,36 +1,66 @@ package chat.simplex.app.views.helpers -import android.content.Context import android.os.Build.VERSION.SDK_INT -import android.widget.Toast import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager.Authenticators.* import androidx.biometric.BiometricPrompt +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.ui.Modifier import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity import chat.simplex.app.R +import chat.simplex.app.SimplexApp +import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword +import chat.simplex.app.views.localauth.LocalAuthView +import chat.simplex.app.views.usersettings.LAMode sealed class LAResult { object Success: LAResult() class Error(val errString: CharSequence): LAResult() - object Failed: LAResult() - object Unavailable: LAResult() + class Failed(val errString: CharSequence? = null): LAResult() + class Unavailable(val errString: CharSequence? = null): LAResult() +} + +data class LocalAuthRequest ( + val title: String?, + val reason: String, + val password: String, + val completed: (LAResult) -> Unit +) { + companion object { + val sample = LocalAuthRequest(generalGetString(R.string.la_enter_app_passcode), generalGetString(R.string.la_authenticate), "") { } + } } fun authenticate( promptTitle: String, promptSubtitle: String, activity: FragmentActivity, + usingLAMode: LAMode = SimplexApp.context.chatModel.controller.appPrefs.laMode.get(), completed: (LAResult) -> Unit ) { - when { - SDK_INT in 28..29 -> - // KeyguardManager.isDeviceSecure()? https://developer.android.com/training/sign-in/biometric-auth#declare-supported-authentication-types - authenticateWithBiometricManager(promptTitle, promptSubtitle, activity, completed, BIOMETRIC_WEAK or DEVICE_CREDENTIAL) - SDK_INT > 29 -> - authenticateWithBiometricManager(promptTitle, promptSubtitle, activity, completed, BIOMETRIC_STRONG or DEVICE_CREDENTIAL) - else -> - completed(LAResult.Unavailable) + when (usingLAMode) { + LAMode.SYSTEM -> when { + SDK_INT in 28..29 -> + // KeyguardManager.isDeviceSecure()? https://developer.android.com/training/sign-in/biometric-auth#declare-supported-authentication-types + authenticateWithBiometricManager(promptTitle, promptSubtitle, activity, completed, BIOMETRIC_WEAK or DEVICE_CREDENTIAL) + SDK_INT > 29 -> + authenticateWithBiometricManager(promptTitle, promptSubtitle, activity, completed, BIOMETRIC_STRONG or DEVICE_CREDENTIAL) + else -> completed(LAResult.Unavailable()) + } + LAMode.PASSCODE -> { + val password = ksAppPassword.get() ?: return completed(LAResult.Unavailable(generalGetString(R.string.la_no_app_password))) + ModalManager.shared.showCustomModal(animated = false) { close -> + Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { + LocalAuthView(SimplexApp.context.chatModel, LocalAuthRequest(promptTitle, promptSubtitle, password) { + close() + completed(it) + }) + } + } + } } } @@ -66,7 +96,7 @@ private fun authenticateWithBiometricManager( override fun onAuthenticationFailed() { super.onAuthenticationFailed() - completed(LAResult.Failed) + completed(LAResult.Failed()) } } ) @@ -78,9 +108,7 @@ private fun authenticateWithBiometricManager( .build() biometricPrompt.authenticate(promptInfo) } - else -> { - completed(LAResult.Unavailable) - } + else -> completed(LAResult.Unavailable()) } } @@ -89,6 +117,18 @@ fun laTurnedOnAlert() = AlertManager.shared.showAlertMsg( generalGetString(R.string.auth_you_will_be_required_to_authenticate_when_you_start_or_resume) ) +fun laPasscodeNotSetAlert() = AlertManager.shared.showAlertMsg( + generalGetString(R.string.lock_not_enabled), + generalGetString(R.string.you_can_turn_on_lock) +) + +fun laFailedAlert() { + AlertManager.shared.showAlertMsg( + title = generalGetString(R.string.la_auth_failed), + text = generalGetString(R.string.la_could_not_be_verified) + ) +} + fun laUnavailableInstructionAlert() = AlertManager.shared.showAlertMsg( generalGetString(R.string.auth_unavailable), generalGetString(R.string.auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt index 252c5e4edc..ccb00f4613 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt @@ -640,4 +640,20 @@ fun DisposableEffectOnGone(always: () -> Unit = {}, whenDispose: () -> Unit = {} } } } -} \ No newline at end of file +} + +@Composable +fun DisposableEffectOnRotate(always: () -> Unit = {}, whenDispose: () -> Unit = {}, whenRotate: () -> Unit) { + val context = LocalContext.current + DisposableEffect(Unit) { + always() + val activity = context as? Activity ?: return@DisposableEffect onDispose {} + val orientation = activity.resources.configuration.orientation + onDispose { + whenDispose() + if (orientation != activity.resources.configuration.orientation) { + whenRotate() + } + } + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/localauth/LocalAuthView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/localauth/LocalAuthView.kt new file mode 100644 index 0000000000..25772c701a --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/localauth/LocalAuthView.kt @@ -0,0 +1,22 @@ +package chat.simplex.app.views.localauth + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.res.stringResource +import chat.simplex.app.R +import chat.simplex.app.model.ChatModel +import chat.simplex.app.views.helpers.* + +@Composable +fun LocalAuthView(m: ChatModel, authRequest: LocalAuthRequest) { + val passcode = rememberSaveable { mutableStateOf("") } + PasscodeView(passcode, authRequest.title ?: stringResource(R.string.la_enter_app_passcode), authRequest.reason, stringResource(R.string.submit_passcode), + submit = { + val r: LAResult = if (passcode.value == authRequest.password) LAResult.Success else LAResult.Error(generalGetString(R.string.incorrect_passcode)) + authRequest.completed(r) + }, + cancel = { + authRequest.completed(LAResult.Error(generalGetString(R.string.authentication_cancelled))) + }) +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/localauth/PasscodeView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/localauth/PasscodeView.kt new file mode 100644 index 0000000000..e6ef8de892 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/localauth/PasscodeView.kt @@ -0,0 +1,100 @@ +package chat.simplex.app.views.localauth + +import android.content.res.Configuration +import androidx.compose.foundation.layout.* +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Done +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import chat.simplex.app.R +import chat.simplex.app.ui.theme.DEFAULT_PADDING +import chat.simplex.app.ui.theme.SimpleButton +import chat.simplex.app.views.helpers.* + +@Composable +fun PasscodeView( + passcode: MutableState, + title: String, + reason: String? = null, + submitLabel: String, + submitEnabled: ((String) -> Boolean)? = null, + submit: () -> Unit, + cancel: () -> Unit, +) { + @Composable + fun VerticalLayout() { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceEvenly + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text(title, style = MaterialTheme.typography.h1) + if (reason != null) { + Text(reason, Modifier.padding(top = 5.dp), style = MaterialTheme.typography.subtitle1) + } + } + PasscodeEntry(passcode, true) + Row { + SimpleButton(generalGetString(R.string.cancel_verb), icon = Icons.Default.Close, click = cancel) + Spacer(Modifier.size(20.dp)) + SimpleButton(submitLabel, icon = Icons.Default.Done, disabled = submitEnabled?.invoke(passcode.value) == false || passcode.value.length < 4, click = submit) + } + } + } + + @Composable + fun HorizontalLayout() { + Row(Modifier.padding(horizontal = DEFAULT_PADDING), horizontalArrangement = Arrangement.Center) { + Column( + Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING * 4), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceBetween + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text(title, style = MaterialTheme.typography.h1) + if (reason != null) { + Text(reason, Modifier.padding(top = 5.dp), style = MaterialTheme.typography.subtitle1) + } + } + PasscodeEntry(passcode, false) + } + + Column( + Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING * 4), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceBetween + ) { + // Just to fill space to correctly calculate the height + Column { + Text("", style = MaterialTheme.typography.h1) + if (reason != null) { + Text("", Modifier.padding(top = 5.dp), style = MaterialTheme.typography.subtitle1) + } + PasscodeView(remember { mutableStateOf("") }) + } + BoxWithConstraints { + val s = minOf(maxWidth, maxHeight) / 3.5f + Column( + Modifier.padding(start = 30.dp).height(s * 3), + verticalArrangement = Arrangement.SpaceEvenly + ) { + SimpleButton(generalGetString(R.string.cancel_verb), icon = Icons.Default.Close, click = cancel) + SimpleButton(submitLabel, icon = Icons.Default.Done, disabled = submitEnabled?.invoke(passcode.value) == false || passcode.value.length < 4, click = submit) + } + } + } + } + } + + if (LocalContext.current.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { + VerticalLayout() + } else { + HorizontalLayout() + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/localauth/PasswordEntry.kt b/apps/android/app/src/main/java/chat/simplex/app/views/localauth/PasswordEntry.kt new file mode 100644 index 0000000000..d42264e301 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/localauth/PasswordEntry.kt @@ -0,0 +1,183 @@ +package chat.simplex.app.views.localauth + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.outlined.Backspace +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.* +import chat.simplex.app.model.ChatModel +import chat.simplex.app.ui.theme.HighOrLowlight + +@Composable +fun PasscodeEntry( + password: MutableState, + vertical: Boolean, +) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + PasscodeView(password) + BoxWithConstraints { + if (vertical) { + VerticalPasswordGrid(password) + } else { + HorizontalPasswordGrid(password) + } + } + } +} + +@Composable +fun PasscodeView(password: MutableState) { + var showPasscode by rememberSaveable { mutableStateOf(false) } + Text( + if (password.value.isEmpty()) " " else remember(password.value, showPasscode) { splitPassword(showPasscode, password.value) }, + Modifier.padding(vertical = 10.dp).clickable { showPasscode = !showPasscode }, + style = MaterialTheme.typography.body1 + ) +} + +@Composable +private fun BoxWithConstraintsScope.VerticalPasswordGrid(password: MutableState) { + val s = minOf(maxWidth, maxHeight) / 4 - 1.dp + Column(Modifier.width(IntrinsicSize.Min)) { + DigitsRow(s, 1, 2, 3, password) + Divider() + DigitsRow(s, 4, 5, 6, password) + Divider() + DigitsRow(s, 7, 8, 9, password) + Divider() + Row(Modifier.requiredHeight(s)) { + PasswordEdit(s, Icons.Default.Close) { + password.value = "" + } + VerticalDivider() + PasswordDigit(s, 0, password) + VerticalDivider() + PasswordEdit(s, Icons.Outlined.Backspace) { + password.value = password.value.dropLast(1) + } + } + } +} + +@Composable +private fun BoxWithConstraintsScope.HorizontalPasswordGrid(password: MutableState) { + val s = minOf(maxWidth, maxHeight) / 3.5f - 1.dp + Column(Modifier.width(IntrinsicSize.Min)) { + Row(Modifier.height(IntrinsicSize.Min)) { + DigitsRow(s, 1, 2, 3, password); + VerticalDivider() + PasswordEdit(s, Icons.Default.Close) { + password.value = "" + } + } + Divider() + Row(Modifier.height(IntrinsicSize.Min)) { + DigitsRow(s, 4, 5, 6, password) + VerticalDivider() + PasswordDigit(s, 0, password) + } + Divider() + Row(Modifier.height(IntrinsicSize.Min)) { + DigitsRow(s, 7, 8, 9, password) + VerticalDivider() + PasswordEdit(s, Icons.Outlined.Backspace) { + password.value = password.value.dropLast(1) + } + } + } +} + +private fun splitPassword(showPassword: Boolean, password: String): String { + val n = if (password.length < 8) 8 else 4 + return password.mapIndexed { index, c -> (if (showPassword) c.toString() else "●") + (if ((index + 1) % n == 0) " " else "") }.joinToString("") +} + +@Composable +private fun DigitsRow(size: Dp, d1: Int, d2: Int, d3: Int, password: MutableState) { + Row(Modifier.height(size)) { + PasswordDigit(size, d1, password) + VerticalDivider() + PasswordDigit(size, d2, password) + VerticalDivider() + PasswordDigit(size, d3, password) + } +} + +@Composable +private fun PasswordDigit(size: Dp, d: Int, password: MutableState) { + val s = d.toString() + return PasswordButton(size, action = { + if (password.value.length < 16) { + password.value += s + } + }) { + Text( + s, + style = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 30.sp, + letterSpacing = (-0.5).sp + ), + color = HighOrLowlight + ) + } +} + +@Composable +private fun PasswordEdit(size: Dp, image: ImageVector, action: () -> Unit) { + PasswordButton(size, action) { + Icon(image, null, tint = HighOrLowlight) + } +} + +@Composable +private fun PasswordButton(size: Dp, action: () -> Unit, content: @Composable BoxScope.() -> Unit) { + return Box( + Modifier.size(size) + .background(MaterialTheme.colors.background, RoundedCornerShape(50)) + .clickable { action() }, + contentAlignment = Alignment.Center + ) { + content() + } +} + +@Composable +fun VerticalDivider( + modifier: Modifier = Modifier, + color: Color = MaterialTheme.colors.onSurface.copy(alpha = DividerAlpha), + thickness: Dp = 1.dp, + startIndent: Dp = 0.dp +) { + val indentMod = if (startIndent.value != 0f) { + Modifier.padding(top = startIndent) + } else { + Modifier + } + val targetThickness = if (thickness == Dp.Hairline) { + (1f / LocalDensity.current.density).dp + } else { + thickness + } + Box( + modifier.then(indentMod) + .fillMaxHeight() + .width(targetThickness) + .background(color = color) + ) +} + +private const val DividerAlpha = 0.12f diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/localauth/SetAppPasscodeView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/localauth/SetAppPasscodeView.kt new file mode 100644 index 0000000000..0506c592dd --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/localauth/SetAppPasscodeView.kt @@ -0,0 +1,48 @@ +package chat.simplex.app.views.localauth + +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import chat.simplex.app.R +import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword +import chat.simplex.app.views.helpers.generalGetString + +@Composable +fun SetAppPasscodeView( + submit: () -> Unit, + cancel: () -> Unit, + close: () -> Unit +) { + val passcode = rememberSaveable { mutableStateOf("") } + var enteredPassword by rememberSaveable { mutableStateOf("") } + var confirming by rememberSaveable { mutableStateOf(false) } + + @Composable + fun SetPasswordView(title: String, submitLabel: String, submitEnabled: (((String) -> Boolean))? = null, submit: () -> Unit) { + PasscodeView(passcode, title = title, submitLabel = submitLabel, submitEnabled = submitEnabled, submit = submit) { + close() + cancel() + } + } + + if (confirming) { + SetPasswordView( + generalGetString(R.string.confirm_passcode), + generalGetString(R.string.confirm_verb), + submitEnabled = { pwd -> pwd == enteredPassword } + ) { + if (passcode.value == enteredPassword) { + ksAppPassword.set(passcode.value) + enteredPassword = "" + passcode.value = "" + close() + submit() + } + } + } else { + SetPasswordView(generalGetString(R.string.new_passcode), generalGetString(R.string.save_verb)) { + enteredPassword = passcode.value + passcode.value = "" + confirming = true + } + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/onboarding/HowItWorks.kt b/apps/android/app/src/main/java/chat/simplex/app/views/onboarding/HowItWorks.kt index 3e13b7cf5f..33c4e7ea1a 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/onboarding/HowItWorks.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/onboarding/HowItWorks.kt @@ -70,4 +70,4 @@ fun PreviewHowItWorks() { SimpleXTheme { HowItWorks(user = null) } -} \ No newline at end of file +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/DeveloperView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/DeveloperView.kt index 55a6dee8ab..e6776a84d5 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/DeveloperView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/DeveloperView.kt @@ -56,4 +56,3 @@ fun DeveloperView( } } } - diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/HiddenProfileView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/HiddenProfileView.kt index a755313126..6571b8c4af 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/HiddenProfileView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/HiddenProfileView.kt @@ -87,4 +87,4 @@ private fun HiddenProfileLayout( } SectionTextFooter(stringResource(R.string.to_reveal_profile_enter_password)) } -} \ No newline at end of file +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/PrivacySettings.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/PrivacySettings.kt index 597a4b4df5..50cffd2976 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/PrivacySettings.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/PrivacySettings.kt @@ -7,22 +7,41 @@ import SectionTextFooter import SectionView import android.view.WindowManager import androidx.compose.foundation.layout.* +import androidx.compose.material.* import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.outlined.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import androidx.fragment.app.FragmentActivity import chat.simplex.app.R import chat.simplex.app.model.* +import chat.simplex.app.ui.theme.HighOrLowlight +import chat.simplex.app.ui.theme.SimplexGreen import chat.simplex.app.views.helpers.* +import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword +import chat.simplex.app.views.localauth.SetAppPasscodeView + +enum class LAMode { + SYSTEM, + PASSCODE; + + val text: String + get() = when (this) { + SYSTEM -> generalGetString(R.string.la_mode_system) + PASSCODE -> generalGetString(R.string.la_mode_passcode) + } +} @Composable fun PrivacySettingsView( chatModel: ChatModel, - setPerformLA: (Boolean) -> Unit + showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), + setPerformLA: (Boolean, FragmentActivity) -> Unit ) { Column( Modifier.fillMaxWidth(), @@ -31,7 +50,7 @@ fun PrivacySettingsView( val simplexLinkMode = chatModel.controller.appPrefs.simplexLinkMode AppBarTitle(stringResource(R.string.your_privacy)) SectionView(stringResource(R.string.settings_section_title_device)) { - ChatLockItem(chatModel.performLA, setPerformLA) + ChatLockItem(chatModel, showSettingsModal, setPerformLA) SectionDivider() val context = LocalContext.current SettingsPreferenceItem(Icons.Outlined.VisibilityOff, stringResource(R.string.protect_app_screen), chatModel.controller.appPrefs.privacyProtectScreen) { on -> @@ -52,10 +71,12 @@ fun PrivacySettingsView( SectionDivider() SettingsPreferenceItem(Icons.Outlined.TravelExplore, stringResource(R.string.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews) SectionDivider() - SectionItemView { SimpleXLinkOptions(chatModel.simplexLinkMode, onSelected = { - simplexLinkMode.set(it) - chatModel.simplexLinkMode.value = it - }) } + SectionItemView { + SimpleXLinkOptions(chatModel.simplexLinkMode, onSelected = { + simplexLinkMode.set(it) + chatModel.simplexLinkMode.value = it + }) + } } if (chatModel.simplexLinkMode.value == SimplexLinkMode.BROWSER) { SectionTextFooter(stringResource(R.string.simplex_link_mode_browser_warning)) @@ -83,3 +104,236 @@ private fun SimpleXLinkOptions(simplexLinkModeState: State, onS onSelected = onSelected ) } + +private val laDelays = listOf(10, 30, 60, 180, 0) + +@Composable +fun SimplexLockView( + chatModel: ChatModel, + currentLAMode: SharedPreference, + setPerformLA: (Boolean, FragmentActivity) -> Unit +) { + val performLA = remember { chatModel.performLA } + val laMode = remember { chatModel.controller.appPrefs.laMode.state } + val laLockDelay = remember { chatModel.controller.appPrefs.laLockDelay } + val showChangePasscode = remember { derivedStateOf { performLA.value && currentLAMode.state.value == LAMode.PASSCODE } } + val activity = LocalContext.current as FragmentActivity + + fun resetLAEnabled(onOff: Boolean) { + chatModel.controller.appPrefs.performLA.set(onOff) + chatModel.performLA.value = onOff + } + + fun disableUnavailableLA() { + resetLAEnabled(false) + currentLAMode.set(LAMode.SYSTEM) + laUnavailableInstructionAlert() + } + + fun toggleLAMode(toLAMode: LAMode) { + authenticate( + if (toLAMode == LAMode.SYSTEM) { + generalGetString(R.string.la_enter_app_passcode) + } else { + generalGetString(R.string.chat_lock) + }, + generalGetString(R.string.change_lock_mode), activity + ) { laResult -> + when (laResult) { + is LAResult.Error -> { + laFailedAlert() + } + is LAResult.Failed -> { /* Can be called multiple times on every failure */ } + LAResult.Success -> { + when (toLAMode) { + LAMode.SYSTEM -> { + authenticate(generalGetString(R.string.auth_enable_simplex_lock), promptSubtitle = "", activity, toLAMode) { laResult -> + when (laResult) { + LAResult.Success -> { + currentLAMode.set(toLAMode) + ksAppPassword.remove() + laTurnedOnAlert() + } + is LAResult.Unavailable, is LAResult.Error -> { + laFailedAlert() + } + is LAResult.Failed -> { /* Can be called multiple times on every failure */ } + } + } + } + LAMode.PASSCODE -> { + ModalManager.shared.showCustomModal { close -> + Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { + SetAppPasscodeView( + submit = { + laLockDelay.set(30) + currentLAMode.set(toLAMode) + passcodeAlert(generalGetString(R.string.passcode_set)) + }, + cancel = {}, + close + ) + } + } + } + } + } + is LAResult.Unavailable -> disableUnavailableLA() + } + } + } + + fun changeLAPassword() { + authenticate(generalGetString(R.string.la_current_app_passcode), generalGetString(R.string.la_change_app_passcode), activity) { laResult -> + when (laResult) { + LAResult.Success -> { + ModalManager.shared.showCustomModal { close -> + Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { + SetAppPasscodeView( + submit = { + passcodeAlert(generalGetString(R.string.passcode_changed)) + }, cancel = { + passcodeAlert(generalGetString(R.string.passcode_not_changed)) + }, close + ) + } + } + } + is LAResult.Error -> laFailedAlert() + is LAResult.Failed -> {} + is LAResult.Unavailable -> disableUnavailableLA() + } + } + } + + Column( + Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.Start + ) { + AppBarTitle(stringResource(R.string.chat_lock)) + SectionView { + EnableLock(performLA) { performLAToggle -> + performLA.value = performLAToggle + chatModel.controller.appPrefs.laNoticeShown.set(true) + if (performLAToggle) { + when (currentLAMode.state.value) { + LAMode.SYSTEM -> { + setPerformLA(true, activity) + } + LAMode.PASSCODE -> { + ModalManager.shared.showCustomModal { close -> + Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { + SetAppPasscodeView( + submit = { + laLockDelay.set(30) + chatModel.controller.appPrefs.performLA.set(true) + passcodeAlert(generalGetString(R.string.passcode_set)) + }, + cancel = { + resetLAEnabled(false) + }, close + ) + } + } + } + } + } else { + setPerformLA(false, activity) + } + } + SectionDivider() + SectionItemView { + LockModeSelector(laMode) { newLAMode -> + if (laMode.value == newLAMode) return@LockModeSelector + if (chatModel.controller.appPrefs.performLA.get()) { + toggleLAMode(newLAMode) + } else { + currentLAMode.set(newLAMode) + } + } + } + + if (performLA.value) { + SectionDivider() + SectionItemView { + LockDelaySelector(remember { laLockDelay.state }) { laLockDelay.set(it) } + } + if (showChangePasscode.value && laMode.value == LAMode.PASSCODE) { + SectionDivider() + SectionItemView({ changeLAPassword() }) { + Text(generalGetString(R.string.la_change_app_passcode)) + } + } + } + } + } +} + +@Composable +private fun EnableLock(performLA: MutableState, onCheckedChange: (Boolean) -> Unit) { + SectionItemView { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + stringResource(R.string.enable_lock), Modifier + .padding(end = 24.dp) + .fillMaxWidth() + .weight(1F) + ) + Switch( + checked = performLA.value, + onCheckedChange = onCheckedChange, + colors = SwitchDefaults.colors( + checkedThumbColor = MaterialTheme.colors.primary, + uncheckedThumbColor = HighOrLowlight + ) + ) + } + } +} + +@Composable +private fun LockModeSelector(state: State, onSelected: (LAMode) -> Unit) { + val values by remember { mutableStateOf(LAMode.values().map { it to it.text }) } + ExposedDropDownSettingRow( + generalGetString(R.string.lock_mode), + values, + state, + icon = null, + enabled = remember { mutableStateOf(true) }, + onSelected = onSelected + ) +} + +@Composable +private fun LockDelaySelector(state: State, onSelected: (Int) -> Unit) { + val delays = remember { if (laDelays.contains(state.value)) laDelays else listOf(state.value) + laDelays } + val values by remember { mutableStateOf(delays.map { it to laDelayText(it) }) } + ExposedDropDownSettingRow( + generalGetString(R.string.lock_after), + values, + state, + icon = null, + enabled = remember { mutableStateOf(true) }, + onSelected = onSelected + ) +} + +private fun laDelayText(t: Int): String { + val m = t / 60 + val s = t % 60 + return if (t == 0) { + generalGetString(R.string.la_immediately) + } else if (m == 0 || s != 0) { + // there are no options where both minutes and seconds are needed + generalGetString(R.string.la_seconds).format(s) + } else { + generalGetString(R.string.la_minutes).format(m) + } +} + +private fun passcodeAlert(title: String) { + AlertManager.shared.showAlertMsg( + title = title, + text = generalGetString(R.string.la_please_remember_to_store_password) + ) +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt index e07b3443d0..ca03fbf388 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt @@ -42,7 +42,7 @@ import chat.simplex.app.views.onboarding.SimpleXInfo import chat.simplex.app.views.onboarding.WhatsNewView @Composable -fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) { +fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean, FragmentActivity) -> Unit) { val user = chatModel.currentUser.value val stopped = chatModel.chatRunning.value == false @@ -126,7 +126,7 @@ fun SettingsLayout( incognito: MutableState, incognitoPref: SharedPreference, userDisplayName: String, - setPerformLA: (Boolean) -> Unit, + setPerformLA: (Boolean, FragmentActivity) -> Unit, showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showSettingsModalWithSearch: (@Composable (ChatModel, MutableState) -> Unit) -> Unit, @@ -174,7 +174,7 @@ fun SettingsLayout( SectionDivider() SettingsActionItem(Icons.Outlined.Videocam, stringResource(R.string.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped) SectionDivider() - SettingsActionItem(Icons.Outlined.Lock, stringResource(R.string.privacy_and_security), showSettingsModal { PrivacySettingsView(it, setPerformLA) }, disabled = stopped) + SettingsActionItem(Icons.Outlined.Lock, stringResource(R.string.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped) SectionDivider() SettingsActionItem(Icons.Outlined.LightMode, stringResource(R.string.appearance_settings), showSettingsModal { AppearanceView(it) }, disabled = stopped) SectionDivider() @@ -294,13 +294,20 @@ fun MaintainIncognitoState(chatModel: ChatModel) { ) } -@Composable fun ChatLockItem(performLA: MutableState, setPerformLA: (Boolean) -> Unit) { - SectionItemView() { +@Composable +fun ChatLockItem( + chatModel: ChatModel, + showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), + setPerformLA: (Boolean, FragmentActivity) -> Unit +) { + val performLA = remember { chatModel.performLA } + val currentLAMode = remember { chatModel.controller.appPrefs.laMode } + SectionItemView(showSettingsModal { SimplexLockView(chatModel, currentLAMode, setPerformLA) }) { Row(verticalAlignment = Alignment.CenterVertically) { Icon( - Icons.Outlined.Lock, + if (performLA.value) Icons.Filled.Lock else Icons.Outlined.Lock, contentDescription = stringResource(R.string.chat_lock), - tint = HighOrLowlight, + tint = if (performLA.value) SimplexGreen else HighOrLowlight, ) Spacer(Modifier.padding(horizontal = 4.dp)) Text( @@ -309,14 +316,7 @@ fun MaintainIncognitoState(chatModel: ChatModel) { .fillMaxWidth() .weight(1F) ) - Switch( - checked = performLA.value, - onCheckedChange = { setPerformLA(it) }, - colors = SwitchDefaults.colors( - checkedThumbColor = MaterialTheme.colors.primary, - uncheckedThumbColor = HighOrLowlight - ) - ) + Text(if (performLA.value) remember { currentLAMode.state }.value.text else generalGetString(androidx.compose.ui.R.string.off), color = HighOrLowlight) } } } @@ -517,7 +517,7 @@ private fun runAuth(context: Context, onFinish: (success: Boolean) -> Unit) { generalGetString(R.string.auth_log_in_using_credential), context as FragmentActivity, completed = { laResult -> - onFinish(laResult == LAResult.Success || laResult == LAResult.Unavailable) + onFinish(laResult == LAResult.Success || laResult is LAResult.Unavailable) } ) } @@ -538,7 +538,7 @@ fun PreviewSettingsLayout() { incognito = remember { mutableStateOf(false) }, incognitoPref = SharedPreference({ false }, {}), userDisplayName = "Alice", - setPerformLA = {}, + setPerformLA = { _, _ -> }, showModal = { {} }, showSettingsModal = { {} }, showSettingsModalWithSearch = { }, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfilesView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfilesView.kt index 2bf1ee352f..8a4675b5ce 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfilesView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfilesView.kt @@ -388,4 +388,4 @@ private fun showMuteProfileAlert(showMuteProfileAlert: SharedPreference showMuteProfileAlert.set(false) }, ) -} \ No newline at end of file +} diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 7a4526d43c..873305dde9 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -165,6 +165,20 @@ SimpleX Lock To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled. Turn on + SimpleX Lock mode + System authentication + Passcode entry + Authentication failed + You could not be verified; please try again. + No app passcode + Enter Passcode + Current Passcode + Change passcode + Authenticate + Immediately + %d seconds + %d minutes + Please remember or store it securely - there is no way to recover a lost password! SimpleX Lock turned on @@ -179,6 +193,8 @@ Device authentication is disabled. Turning off SimpleX Lock. Stop chat Open chat console + SimpleX Lock not enabled! + You can turn on SimpleX Lock via Settings. Message delivery error @@ -735,6 +751,20 @@ Auto-accept images Send link previews App data backup + Enable lock + Lock mode + Lock after + Submit + Confirm Passcode + Incorrect passcode + New Passcode + Authentication cancelled + System + Passcode + Passcode set! + Passcode changed! + Passcode not changed! + Change lock mode YOU diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift index 5be4979f12..dd21ea26c0 100644 --- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift +++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift @@ -186,7 +186,7 @@ struct SimplexLockView: View { .onChange(of: laMode) { _ in if performLAModeReset { performLAModeReset = false - } else if performLA { + } else if prefPerformLA { toggleLAMode() } else { updateLAMode()