From 1438fd00e2164608e99cdd42917978dc304de16e Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Sat, 30 Dec 2023 00:47:25 +0700 Subject: [PATCH] android, desktop: self destruct becomes better (#3598) * android, desktop: self destruct becomes better * better way of doing it * fix script * firstOrNull * changes for review * comment --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../main/java/chat/simplex/app/SimplexApp.kt | 5 +- .../src/commonMain/cpp/android/simplex-api.c | 7 ++ .../src/commonMain/cpp/desktop/simplex-api.c | 7 ++ .../kotlin/chat/simplex/common/App.kt | 5 +- .../chat/simplex/common/model/ChatModel.kt | 1 + .../chat/simplex/common/model/SimpleXAPI.kt | 10 +- .../chat/simplex/common/platform/Core.kt | 117 ++++++++++-------- .../common/views/chatlist/UserPicker.kt | 5 +- .../common/views/database/DatabaseView.kt | 20 ++- .../common/views/helpers/DatabaseUtils.kt | 10 +- .../common/views/localauth/LocalAuthView.kt | 57 +++++++-- .../common/views/localauth/PasscodeView.kt | 13 +- .../views/localauth/SetAppPasscodeView.kt | 5 +- .../views/usersettings/PrivacySettings.kt | 4 +- .../commonMain/resources/MR/base/strings.xml | 1 + .../common/platform/AppCommon.desktop.kt | 8 +- libsimplex.dll.def | 1 + 17 files changed, 188 insertions(+), 88 deletions(-) diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt index a8c8b5c1b2..84b39e983f 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt @@ -63,10 +63,7 @@ class SimplexApp: Application(), LifecycleEventObserver { tmpDir.deleteRecursively() tmpDir.mkdir() - withBGApi { - initChatController() - runMigrations() - } + initChatControllerAndRunMigrations(false) ProcessLifecycleOwner.get().lifecycle.addObserver(this@SimplexApp) } diff --git a/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c b/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c index 676c58fb49..5936bd5ff2 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c +++ b/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c @@ -57,6 +57,7 @@ typedef long* chat_ctrl; */ extern char *chat_migrate_init(const char *path, const char *key, const char *confirm, chat_ctrl *ctrl); +extern char *chat_close_store(chat_ctrl ctrl); extern char *chat_send_cmd(chat_ctrl ctrl, const char *cmd); extern char *chat_send_remote_cmd(chat_ctrl ctrl, const int rhId, const char *cmd); extern char *chat_recv_msg(chat_ctrl ctrl); // deprecated @@ -93,6 +94,12 @@ Java_chat_simplex_common_platform_CoreKt_chatMigrateInit(JNIEnv *env, __unused j return ret; } +JNIEXPORT jstring JNICALL +Java_chat_simplex_common_platform_CoreKt_chatCloseStore(JNIEnv *env, __unused jclass clazz, jlong controller) { + jstring res = (*env)->NewStringUTF(env, chat_close_store((void*)controller)); + return res; +} + JNIEXPORT jstring JNICALL Java_chat_simplex_common_platform_CoreKt_chatSendCmd(JNIEnv *env, __unused jclass clazz, jlong controller, jstring msg) { const char *_msg = (*env)->GetStringUTFChars(env, msg, JNI_FALSE); diff --git a/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c b/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c index 292715bdc5..f15689285a 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c +++ b/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c @@ -30,6 +30,7 @@ typedef long* chat_ctrl; */ extern char *chat_migrate_init(const char *path, const char *key, const char *confirm, chat_ctrl *ctrl); +extern char *chat_close_store(chat_ctrl ctrl); extern char *chat_send_cmd(chat_ctrl ctrl, const char *cmd); extern char *chat_send_remote_cmd(chat_ctrl ctrl, const int rhId, const char *cmd); extern char *chat_recv_msg(chat_ctrl ctrl); // deprecated @@ -106,6 +107,12 @@ Java_chat_simplex_common_platform_CoreKt_chatMigrateInit(JNIEnv *env, jclass cla return ret; } +JNIEXPORT jstring JNICALL +Java_chat_simplex_common_platform_CoreKt_chatCloseStore(JNIEnv *env, jclass clazz, jlong controller) { + jstring res = decode_to_utf8_string(env, chat_close_store((void*)controller)); + return res; +} + JNIEXPORT jstring JNICALL Java_chat_simplex_common_platform_CoreKt_chatSendCmd(JNIEnv *env, jclass clazz, jlong controller, jstring msg) { const char *_msg = encode_to_utf8_chars(env, msg); diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt index 950515f055..f61f3b4b80 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -103,13 +103,15 @@ fun MainScreen() { } Box { + val unauthorized = remember { derivedStateOf { AppLock.userAuthorized.value != true } } val onboarding by remember { chatModel.controller.appPrefs.onboardingStage.state } val localUserCreated = chatModel.localUserCreated.value var showInitializationView by remember { mutableStateOf(false) } when { chatModel.chatDbStatus.value == null && showInitializationView -> DefaultProgressView(stringResource(MR.strings.opening_database)) showChatDatabaseError -> { - chatModel.chatDbStatus.value?.let { + // Prevent showing keyboard on Android when: passcode enabled and database password not saved + if (!unauthorized.value && chatModel.chatDbStatus.value != null) { DatabaseErrorView(chatModel.chatDbStatus, chatModel.controller.appPrefs) } } @@ -150,7 +152,6 @@ fun MainScreen() { SwitchingUsersView() } - val unauthorized = remember { derivedStateOf { AppLock.userAuthorized.value != true } } if (unauthorized.value && !(chatModel.activeCallViewIsVisible.value && chatModel.showCallView.value)) { LaunchedEffect(Unit) { // With these constrains when user presses back button while on ChatList, activity destroys and shows auth request diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index f51f6986f8..b8d421b07b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -47,6 +47,7 @@ object ChatModel { val chatDbChanged = mutableStateOf(false) val chatDbEncrypted = mutableStateOf(false) val chatDbStatus = mutableStateOf(null) + val ctrlInitInProgress = mutableStateOf(false) val chats = mutableStateListOf() // map of connections network statuses, key is agent connection id val networkStatuses = mutableStateMapOf() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index e2ab3a4d99..07e091b484 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -2145,7 +2145,15 @@ class SharedPreference(val get: () -> T, set: (T) -> Unit) { init { this.set = { value -> set(value) - _state.value = value + try { + _state.value = value + } catch (e: IllegalStateException) { + // Can be `Reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied` + Log.i(TAG, e.stackTraceToString()) + withApi { + _state.value = value + } + } } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt index 52ff269f54..07e59a55e0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt @@ -14,6 +14,7 @@ external fun pipeStdOutToSocket(socketName: String) : Int // SimpleX API typealias ChatCtrl = Long external fun chatMigrateInit(dbPath: String, dbKey: String, confirm: String): Array +external fun chatCloseStore(ctrl: ChatCtrl): String external fun chatSendCmd(ctrl: ChatCtrl, msg: String): String external fun chatSendRemoteCmd(ctrl: ChatCtrl, rhId: Int, msg: String): String external fun chatRecvMsg(ctrl: ChatCtrl): String @@ -35,57 +36,71 @@ val appPreferences: AppPreferences val chatController: ChatController = ChatController -suspend fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: Boolean = true) { - val dbKey = useKey ?: DatabaseUtils.useDatabaseKey() - val confirm = confirmMigrations ?: if (appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp - val migrated: Array = chatMigrateInit(dbAbsolutePrefixPath, dbKey, confirm.value) - val res: DBMigrationResult = kotlin.runCatching { - json.decodeFromString(migrated[0] as String) - }.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) } - val ctrl = if (res is DBMigrationResult.OK) { - migrated[1] as Long - } else null - chatController.ctrl = ctrl - chatModel.chatDbEncrypted.value = dbKey != "" - chatModel.chatDbStatus.value = res - if (res != DBMigrationResult.OK) { - Log.d(TAG, "Unable to migrate successfully: $res") - } else if (startChat) { - // If we migrated successfully means previous re-encryption process on database level finished successfully too - if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null) - val user = chatController.apiGetActiveUser(null) - if (user == null) { - chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) - chatModel.currentUser.value = null - chatModel.users.clear() - if (appPlatform.isDesktop) { - /** - * Setting it here to null because otherwise the screen will flash in [MainScreen] after the first start - * because of default value of [OnboardingStage.OnboardingComplete] - * */ - chatModel.localUserCreated.value = null - if (chatController.listRemoteHosts()?.isEmpty() == true) { - chatController.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) - } - chatController.startChatWithoutUser() - } else { - chatController.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) - } - } else { - val savedOnboardingStage = appPreferences.onboardingStage.get() - val newStage = if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) { - OnboardingStage.Step3_CreateSimpleXAddress - } else { - savedOnboardingStage - } - if (appPreferences.onboardingStage.get() != newStage) { - appPreferences.onboardingStage.set(newStage) - } - if (appPreferences.onboardingStage.get() == OnboardingStage.OnboardingComplete && !chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.get()) { - chatModel.setDeliveryReceipts.value = true - } - chatController.startChat(user) - platform.androidChatInitializedAndStarted() +fun initChatControllerAndRunMigrations(ignoreSelfDestruct: Boolean) { + if (ignoreSelfDestruct || DatabaseUtils.ksSelfDestructPassword.get() == null) { + withBGApi { + initChatController() + runMigrations() } } } + +suspend fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: Boolean = true) { + try { + chatModel.ctrlInitInProgress.value = true + val dbKey = useKey ?: DatabaseUtils.useDatabaseKey() + val confirm = confirmMigrations ?: if (appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp + val migrated: Array = chatMigrateInit(dbAbsolutePrefixPath, dbKey, confirm.value) + val res: DBMigrationResult = kotlin.runCatching { + json.decodeFromString(migrated[0] as String) + }.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) } + val ctrl = if (res is DBMigrationResult.OK) { + migrated[1] as Long + } else null + chatController.ctrl = ctrl + chatModel.chatDbEncrypted.value = dbKey != "" + chatModel.chatDbStatus.value = res + if (res != DBMigrationResult.OK) { + Log.d(TAG, "Unable to migrate successfully: $res") + } else if (startChat) { + // If we migrated successfully means previous re-encryption process on database level finished successfully too + if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null) + val user = chatController.apiGetActiveUser(null) + if (user == null) { + chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) + chatModel.currentUser.value = null + chatModel.users.clear() + if (appPlatform.isDesktop) { + /** + * Setting it here to null because otherwise the screen will flash in [MainScreen] after the first start + * because of default value of [OnboardingStage.OnboardingComplete] + * */ + chatModel.localUserCreated.value = null + if (chatController.listRemoteHosts()?.isEmpty() == true) { + chatController.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) + } + chatController.startChatWithoutUser() + } else { + chatController.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) + } + } else { + val savedOnboardingStage = appPreferences.onboardingStage.get() + val newStage = if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) { + OnboardingStage.Step3_CreateSimpleXAddress + } else { + savedOnboardingStage + } + if (appPreferences.onboardingStage.get() != newStage) { + appPreferences.onboardingStage.set(newStage) + } + if (appPreferences.onboardingStage.get() == OnboardingStage.OnboardingComplete && !chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.get()) { + chatModel.setDeliveryReceipts.value = true + } + chatController.startChat(user) + platform.androidChatInitializedAndStarted() + } + } + } finally { + chatModel.ctrlInitInProgress.value = false + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt index d87c05a913..a0db4188ad 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt @@ -116,7 +116,10 @@ fun UserPicker( } } LaunchedEffect(Unit) { - controller.reloadRemoteHosts() + // Controller.ctrl can be null when self-destructing activates + if (controller.ctrl != null && controller.ctrl != -1L) { + controller.reloadRemoteHosts() + } } val UsersView: @Composable ColumnScope.() -> Unit = { users.forEach { u -> diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt index 224317f949..5e1abb6846 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt @@ -4,7 +4,6 @@ import SectionBottomSpacer import SectionDividerSpaced import SectionTextFooter import SectionItemView -import SectionSpacer import SectionView import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.* @@ -426,6 +425,7 @@ private fun authStopChat(m: ChatModel) { } is LAResult.Error -> { m.chatRunning.value = true + laFailedAlert() } is LAResult.Failed -> { m.chatRunning.value = true @@ -459,6 +459,24 @@ suspend fun deleteChatAsync(m: ChatModel) { m.controller.apiDeleteStorage() DatabaseUtils.ksDatabasePassword.remove() m.controller.appPrefs.storeDBPassphrase.set(true) + deleteChatDatabaseFiles() +} + +fun deleteChatDatabaseFiles() { + val chat = File(dataDir, chatDatabaseFileName) + val chatBak = File(dataDir, "$chatDatabaseFileName.bak") + val agent = File(dataDir, agentDatabaseFileName) + val agentBak = File(dataDir, "$agentDatabaseFileName.bak") + chat.delete() + chatBak.delete() + agent.delete() + agentBak.delete() + filesDir.deleteRecursively() + remoteHostsDir.deleteRecursively() + tmpDir.deleteRecursively() + tmpDir.mkdir() + DatabaseUtils.ksDatabasePassword.remove() + controller.appPrefs.storeDBPassphrase.set(true) } private fun exportArchive( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt index e7da47f8f0..c984e16452 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt @@ -17,7 +17,7 @@ object DatabaseUtils { val ksAppPassword = KeyStoreItem(APP_PASSWORD_ALIAS, appPreferences.encryptedAppPassphrase, appPreferences.initializationVectorAppPassphrase) val ksSelfDestructPassword = KeyStoreItem(SELF_DESTRUCT_PASSWORD_ALIAS, appPreferences.encryptedSelfDestructPassphrase, appPreferences.initializationVectorSelfDestructPassphrase) - class KeyStoreItem(private val alias: String, val passphrase: SharedPreference, val initVector: SharedPreference) { + class KeyStoreItem(val alias: String, val passphrase: SharedPreference, val initVector: SharedPreference) { fun get(): String? { return cryptor.decryptData( passphrase.get()?.toByteArrayFromBase64ForPassphrase() ?: return null, @@ -75,11 +75,11 @@ object DatabaseUtils { sealed class DBMigrationResult { @Serializable @SerialName("ok") object OK: DBMigrationResult() @Serializable @SerialName("invalidConfirmation") object InvalidConfirmation: DBMigrationResult() - @Serializable @SerialName("errorNotADatabase") class ErrorNotADatabase(val dbFile: String): DBMigrationResult() - @Serializable @SerialName("errorMigration") class ErrorMigration(val dbFile: String, val migrationError: MigrationError): DBMigrationResult() - @Serializable @SerialName("errorSQL") class ErrorSQL(val dbFile: String, val migrationSQLError: String): DBMigrationResult() + @Serializable @SerialName("errorNotADatabase") data class ErrorNotADatabase(val dbFile: String): DBMigrationResult() + @Serializable @SerialName("errorMigration") data class ErrorMigration(val dbFile: String, val migrationError: MigrationError): DBMigrationResult() + @Serializable @SerialName("errorSQL") data class ErrorSQL(val dbFile: String, val migrationSQLError: String): DBMigrationResult() @Serializable @SerialName("errorKeychain") object ErrorKeychain: DBMigrationResult() - @Serializable @SerialName("unknown") class Unknown(val json: String): DBMigrationResult() + @Serializable @SerialName("unknown") data class Unknown(val json: String): DBMigrationResult() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt index 468dd8580e..0401906527 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt @@ -1,32 +1,45 @@ package chat.simplex.common.views.localauth -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable +import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.controller import dev.icerock.moko.resources.compose.stringResource -import chat.simplex.common.views.database.deleteChatAsync -import chat.simplex.common.views.database.stopChatAsync import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.DatabaseUtils.ksSelfDestructPassword import chat.simplex.common.views.helpers.DatabaseUtils.ksAppPassword import chat.simplex.common.views.onboarding.OnboardingStage -import chat.simplex.common.model.ChatModel -import chat.simplex.common.model.Profile import chat.simplex.common.platform.* +import chat.simplex.common.views.database.* import chat.simplex.res.MR +import kotlinx.coroutines.delay @Composable fun LocalAuthView(m: ChatModel, authRequest: LocalAuthRequest) { val passcode = rememberSaveable { mutableStateOf("") } - PasscodeView(passcode, authRequest.title ?: stringResource(MR.strings.la_enter_app_passcode), authRequest.reason, stringResource(MR.strings.submit_passcode), + val allowToReact = rememberSaveable { mutableStateOf(true) } + if (!allowToReact.value) { + BackHandler { + // do nothing until submit action finishes to prevent concurrent removing of storage + } + } + PasscodeView(passcode, authRequest.title ?: stringResource(MR.strings.la_enter_app_passcode), authRequest.reason, stringResource(MR.strings.submit_passcode), buttonsEnabled = allowToReact, submit = { val sdPassword = ksSelfDestructPassword.get() if (sdPassword == passcode.value && authRequest.selfDestruct) { + allowToReact.value = false deleteStorageAndRestart(m, sdPassword) { r -> authRequest.completed(r) } } else { - val r: LAResult = if (passcode.value == authRequest.password) LAResult.Success else LAResult.Error(generalGetString(MR.strings.incorrect_passcode)) + val r: LAResult = if (passcode.value == authRequest.password) { + if (authRequest.selfDestruct && sdPassword != null && controller.ctrl == -1L) { + initChatControllerAndRunMigrations(true) + } + LAResult.Success + } else { + LAResult.Error(generalGetString(MR.strings.incorrect_passcode)) + } authRequest.completed(r) } }, @@ -38,8 +51,28 @@ fun LocalAuthView(m: ChatModel, authRequest: LocalAuthRequest) { private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: (LAResult) -> Unit) { withBGApi { try { - stopChatAsync(m) - deleteChatAsync(m) + /** Waiting until [initChatController] finishes */ + while (m.ctrlInitInProgress.value) { + delay(50) + } + if (m.chatRunning.value == true) { + stopChatAsync(m) + } + val ctrl = m.controller.ctrl + if (ctrl != null && ctrl != -1L) { + /** + * The following sequence can bring a user here: + * the user opened the app, entered app passcode, went to background, returned back, entered self-destruct code. + * In this case database should be closed to prevent possible situation when OS can deny database removal command + * */ + chatCloseStore(ctrl) + } + deleteChatDatabaseFiles() + // Clear sensitive data on screen just in case ModalManager will fail to prevent hiding its modals while database encrypts itself + m.chatId.value = null + m.chatItems.clear() + m.chats.clear() + m.users.clear() ksAppPassword.set(password) ksSelfDestructPassword.remove() ntfManager.cancelAllNotifications() @@ -67,13 +100,15 @@ private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: ( m.currentUser.value = createdUser m.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete) if (createdUser != null) { + controller.chatModel.chatRunning.value = false m.controller.startChat(createdUser) } - ModalManager.fullscreen.closeModals() + ModalManager.closeAllModalsEverywhere() AlertManager.shared.hideAllAlerts() AlertManager.privacySensitive.hideAllAlerts() completed(LAResult.Success) } catch (e: Exception) { + Log.e(TAG, "Unable to delete storage: ${e.stackTraceToString()}") completed(LAResult.Error(generalGetString(MR.strings.incorrect_passcode))) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasscodeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasscodeView.kt index 4784951ad0..2b2f006d25 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasscodeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasscodeView.kt @@ -12,6 +12,7 @@ import dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.unit.dp import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.common.views.chat.group.ProgressIndicator import chat.simplex.common.views.helpers.SimpleButton import chat.simplex.common.views.helpers.* import chat.simplex.res.MR @@ -23,6 +24,7 @@ fun PasscodeView( reason: String? = null, submitLabel: String, submitEnabled: ((String) -> Boolean)? = null, + buttonsEnabled: State = remember { mutableStateOf(true) }, submit: () -> Unit, cancel: () -> Unit, ) { @@ -74,9 +76,9 @@ fun PasscodeView( } PasscodeEntry(passcode, true) Row(Modifier.heightIn(min = 70.dp), verticalAlignment = Alignment.CenterVertically) { - SimpleButton(generalGetString(MR.strings.cancel_verb), icon = painterResource(MR.images.ic_close), click = cancel) + SimpleButton(generalGetString(MR.strings.cancel_verb), icon = painterResource(MR.images.ic_close), disabled = !buttonsEnabled.value, click = cancel) Spacer(Modifier.size(20.dp)) - SimpleButton(submitLabel, icon = painterResource(MR.images.ic_done_filled), disabled = submitEnabled?.invoke(passcode.value) == false || passcode.value.length < 4, click = submit) + SimpleButton(submitLabel, icon = painterResource(MR.images.ic_done_filled), disabled = submitEnabled?.invoke(passcode.value) == false || passcode.value.length < 4 || !buttonsEnabled.value, click = submit) } } } @@ -117,8 +119,8 @@ fun PasscodeView( Modifier.padding(start = 30.dp).height(s * 3), verticalArrangement = Arrangement.SpaceEvenly ) { - SimpleButton(generalGetString(MR.strings.cancel_verb), icon = painterResource(MR.images.ic_close), click = cancel) - SimpleButton(submitLabel, icon = painterResource(MR.images.ic_done_filled), disabled = submitEnabled?.invoke(passcode.value) == false || passcode.value.length < 4, click = submit) + SimpleButton(generalGetString(MR.strings.cancel_verb), icon = painterResource(MR.images.ic_close), disabled = !buttonsEnabled.value, click = cancel) + SimpleButton(submitLabel, icon = painterResource(MR.images.ic_done_filled), disabled = submitEnabled?.invoke(passcode.value) == false || passcode.value.length < 4 || !buttonsEnabled.value, click = submit) } } } @@ -130,6 +132,9 @@ fun PasscodeView( } else { HorizontalLayout() } + if (!buttonsEnabled.value) { + ProgressIndicator() + } LaunchedEffect(Unit) { focusRequester.requestFocus() // Disallow to steal a focus by clicking on buttons or using Tab diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/SetAppPasscodeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/SetAppPasscodeView.kt index 18437dbf98..eadd399428 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/SetAppPasscodeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/SetAppPasscodeView.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import chat.simplex.common.platform.BackHandler import chat.simplex.common.views.helpers.DatabaseUtils import chat.simplex.common.views.helpers.DatabaseUtils.ksAppPassword +import chat.simplex.common.views.helpers.DatabaseUtils.ksSelfDestructPassword import chat.simplex.common.views.helpers.generalGetString import chat.simplex.res.MR @@ -48,7 +49,9 @@ fun SetAppPasscodeView( } } } else { - SetPasswordView(title, generalGetString(MR.strings.save_verb)) { + SetPasswordView(title, generalGetString(MR.strings.save_verb), + // Do not allow to set app passcode == selfDestruct passcode + submitEnabled = { pwd -> pwd != (if (passcodeKeychain.alias == ksSelfDestructPassword.alias) ksAppPassword else ksSelfDestructPassword).get() }) { enteredPassword = passcode.value passcode.value = "" confirming = true diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt index 3d2b7b7fa5..9a4f083746 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt @@ -429,6 +429,7 @@ fun SimplexLockView( ModalManager.fullscreen.showCustomModal { close -> Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { SetAppPasscodeView( + reason = generalGetString(MR.strings.la_app_passcode), submit = { passcodeAlert(generalGetString(MR.strings.passcode_changed)) }, cancel = { @@ -453,6 +454,7 @@ fun SimplexLockView( Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { SetAppPasscodeView( passcodeKeychain = ksSelfDestructPassword, + reason = generalGetString(MR.strings.self_destruct), submit = { selfDestructPasscodeAlert(generalGetString(MR.strings.self_destruct_passcode_changed)) }, cancel = { @@ -553,7 +555,7 @@ fun SimplexLockView( fontSize = 16.sp, modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF) ) - ProfileNameField(selfDestructDisplayName, "", ::isValidDisplayName) + ProfileNameField(selfDestructDisplayName, "", { isValidDisplayName(it.trim()) }) LaunchedEffect(selfDestructDisplayName.value) { val new = selfDestructDisplayName.value if (isValidDisplayName(new) && selfDestructDisplayNamePref.get() != new) { diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 09ccf1e409..8f5f6338fa 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -919,6 +919,7 @@ Authentication cancelled System Passcode + App passcode Off Passcode set! Passcode changed! diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt index 92111f162a..4e70956be7 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt @@ -2,8 +2,7 @@ package chat.simplex.common.platform import chat.simplex.common.model.* import chat.simplex.common.views.call.RcvCallInvitation -import chat.simplex.common.views.helpers.generalGetString -import chat.simplex.common.views.helpers.withBGApi +import chat.simplex.common.views.helpers.* import java.util.* import chat.simplex.res.MR @@ -25,10 +24,7 @@ fun initApp() { override fun cancelAllNotifications() = chat.simplex.common.model.NtfManager.cancelAllNotifications() } applyAppLocale() - withBGApi { - initChatController() - runMigrations() - } + initChatControllerAndRunMigrations(false) // LALAL //testCrypto() } diff --git a/libsimplex.dll.def b/libsimplex.dll.def index 4255f4409c..f927e3ee24 100644 --- a/libsimplex.dll.def +++ b/libsimplex.dll.def @@ -3,6 +3,7 @@ EXPORTS hs_init hs_init_with_rtsopts chat_migrate_init + chat_close_store chat_send_cmd chat_send_remote_cmd chat_recv_msg