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 a3bb82218c..8d70506f26 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 @@ -35,6 +35,7 @@ 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.helpers.DatabaseUtils.ksSelfDestructPassword import chat.simplex.app.views.localauth.SetAppPasscodeView import chat.simplex.app.views.newchat.* import chat.simplex.app.views.onboarding.* @@ -179,6 +180,7 @@ class MainActivity: FragmentActivity() { generalGetString(R.string.auth_log_in_using_credential) else generalGetString(R.string.auth_unlock), + selfDestruct = true, this@MainActivity, completed = { laResult -> when (laResult) { @@ -248,7 +250,7 @@ class MainActivity: FragmentActivity() { authenticate( generalGetString(R.string.auth_enable_simplex_lock), generalGetString(R.string.auth_confirm_credential), - activity, + activity = activity, completed = { laResult -> when (laResult) { LAResult.Success -> { @@ -289,7 +291,8 @@ class MainActivity: FragmentActivity() { appPrefs.performLA.set(false) laPasscodeNotSetAlert() }, - close) + close = close + ) } } } @@ -314,7 +317,7 @@ class MainActivity: FragmentActivity() { generalGetString(R.string.auth_confirm_credential) else "", - activity, + activity = activity, completed = { laResult -> val prefPerformLA = m.controller.appPrefs.performLA when (laResult) { @@ -350,14 +353,17 @@ class MainActivity: FragmentActivity() { generalGetString(R.string.auth_confirm_credential) else generalGetString(R.string.auth_disable_simplex_lock), - activity, + activity = activity, completed = { laResult -> val prefPerformLA = m.controller.appPrefs.performLA + val selfDestructPref = m.controller.appPrefs.selfDestruct when (laResult) { LAResult.Success -> { m.performLA.value = false prefPerformLA.set(false) ksAppPassword.remove() + selfDestructPref.set(false) + ksSelfDestructPassword.remove() } is LAResult.Failed -> { /* Can be called multiple times on every failure */ } is LAResult.Error -> { diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt index b2d4a868cc..ed2c03b897 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt @@ -42,7 +42,7 @@ class SimplexApp: Application(), LifecycleEventObserver { val defaultLocale: Locale = Locale.getDefault() - fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: Boolean = true) { + suspend fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: Boolean = true) { val dbKey = useKey ?: DatabaseUtils.useDatabaseKey() val dbAbsolutePathPrefix = getFilesDirectory(SimplexApp.context) val confirm = confirmMigrations ?: if (appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp @@ -65,25 +65,25 @@ class SimplexApp: Application(), LifecycleEventObserver { } 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) - withApi { - val user = chatController.apiGetActiveUser() - if (user == null) { - chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) - chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo + val user = chatController.apiGetActiveUser() + if (user == null) { + chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) + chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo + chatModel.currentUser.value = null + chatModel.users.clear() + } else { + val savedOnboardingStage = appPreferences.onboardingStage.get() + chatModel.onboardingStage.value = if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) { + OnboardingStage.Step3_CreateSimpleXAddress } else { - val savedOnboardingStage = appPreferences.onboardingStage.get() - chatModel.onboardingStage.value = if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) { - OnboardingStage.Step3_CreateSimpleXAddress - } else { - savedOnboardingStage - } - chatController.startChat(user) - // Prevents from showing "Enable notifications" alert when onboarding wasn't complete yet - if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete) { - chatController.showBackgroundServiceNoticeIfNeeded() - if (appPreferences.notificationsMode.get() == NotificationsMode.SERVICE.name) - SimplexService.start(applicationContext) - } + savedOnboardingStage + } + chatController.startChat(user) + // Prevents from showing "Enable notifications" alert when onboarding wasn't complete yet + if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete) { + chatController.showBackgroundServiceNoticeIfNeeded() + if (appPreferences.notificationsMode.get() == NotificationsMode.SERVICE.name) + SimplexService.start(applicationContext) } } } @@ -103,10 +103,12 @@ class SimplexApp: Application(), LifecycleEventObserver { override fun onCreate() { super.onCreate() context = this - initChatController() - ProcessLifecycleOwner.get().lifecycle.addObserver(this) context.getDir("temp", MODE_PRIVATE).deleteRecursively() - runMigrations() + runBlocking { + initChatController() + ProcessLifecycleOwner.get().lifecycle.addObserver(this@SimplexApp) + runMigrations() + } } override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 99930c4df7..65a2992917 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -41,7 +41,6 @@ class ChatModel(val controller: ChatController) { val chatDbChanged = mutableStateOf(false) val chatDbEncrypted = mutableStateOf(false) val chatDbStatus = mutableStateOf(null) - val chatDbDeleted = mutableStateOf(false) val chats = mutableStateListOf() // map of connections network statuses, key is agent connection id val networkStatuses = mutableStateMapOf() diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt b/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt index a4c4c8aeab..bc5985c5e5 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt @@ -231,6 +231,10 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference manager.cancel(CallNotificationId) } + fun cancelAllNotifications() { + manager.cancelAll() + } + fun hasNotificationsForChat(chatId: String): Boolean = manager.activeNotifications.any { it.id == chatId.hashCode() } private fun hideSecrets(cItem: ChatItem) : String { 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 0b719b4b7d..9f0af623e2 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 @@ -148,8 +148,12 @@ class AppPreferences(val context: Context) { 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 encryptedSelfDestructPassphrase = mkStrPreference(SHARED_PREFS_ENCRYPTED_SELF_DESTRUCT_PASSPHRASE, null) + val initializationVectorSelfDestructPassphrase = mkStrPreference(SHARED_PREFS_INITIALIZATION_VECTOR_SELF_DESTRUCT_PASSPHRASE, null) val encryptionStartedAt = mkDatePreference(SHARED_PREFS_ENCRYPTION_STARTED_AT, null, true) val confirmDBUpgrades = mkBoolPreference(SHARED_PREFS_CONFIRM_DB_UPGRADES, false) + val selfDestruct = mkBoolPreference(SHARED_PREFS_SELF_DESTRUCT, false) + val selfDestructDisplayName = mkStrPreference(SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME, null) val currentTheme = mkStrPreference(SHARED_PREFS_CURRENT_THEME, DefaultTheme.SYSTEM.name) val systemDarkTheme = mkStrPreference(SHARED_PREFS_SYSTEM_DARK_THEME, DefaultTheme.SIMPLEX.name) @@ -274,8 +278,12 @@ class AppPreferences(val context: Context) { 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_ENCRYPTED_SELF_DESTRUCT_PASSPHRASE = "EncryptedSelfDestructPassphrase" + private const val SHARED_PREFS_INITIALIZATION_VECTOR_SELF_DESTRUCT_PASSPHRASE = "InitializationVectorSelfDestructPassphrase" private const val SHARED_PREFS_ENCRYPTION_STARTED_AT = "EncryptionStartedAt" private const val SHARED_PREFS_CONFIRM_DB_UPGRADES = "ConfirmDBUpgrades" + private const val SHARED_PREFS_SELF_DESTRUCT = "LocalAuthenticationSelfDestruct" + private const val SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME = "LocalAuthenticationSelfDestructDisplayName" private const val SHARED_PREFS_CURRENT_THEME = "CurrentTheme" private const val SHARED_PREFS_SYSTEM_DARK_THEME = "SystemDarkTheme" private const val SHARED_PREFS_THEMES = "Themes" @@ -434,8 +442,8 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a return null } - suspend fun apiCreateActiveUser(p: Profile): User? { - val r = sendCmd(CC.CreateActiveUser(p)) + suspend fun apiCreateActiveUser(p: Profile?, sameServers: Boolean = false, pastTimestamp: Boolean = false): User? { + val r = sendCmd(CC.CreateActiveUser(p, sameServers = sameServers, pastTimestamp = pastTimestamp)) if (r is CR.ActiveUser) return r.user else if ( r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore && r.chatError.storeError is StoreError.DuplicateName || @@ -1853,7 +1861,7 @@ class SharedPreference(val get: () -> T, set: (T) -> Unit) { sealed class CC { class Console(val cmd: String): CC() class ShowActiveUser: CC() - class CreateActiveUser(val profile: Profile): CC() + class CreateActiveUser(val profile: Profile?, val sameServers: Boolean, val pastTimestamp: Boolean): CC() class ListUsers: CC() class ApiSetActiveUser(val userId: Long, val viewPwd: String?): CC() class ApiHideUser(val userId: Long, val viewPwd: String): CC() @@ -1938,7 +1946,10 @@ sealed class CC { val cmdString: String get() = when (this) { is Console -> cmd is ShowActiveUser -> "/u" - is CreateActiveUser -> "/create user ${profile.displayName} ${profile.fullName}" + is CreateActiveUser -> { + val user = NewUser(profile, sameServers = sameServers, pastTimestamp = pastTimestamp) + "/_create user ${json.encodeToString(user)}" + } is ListUsers -> "/users" is ApiSetActiveUser -> "/_user $userId${maybePwd(viewPwd)}" is ApiHideUser -> "/_hide user $userId ${json.encodeToString(viewPwd)}" @@ -2144,6 +2155,13 @@ sealed class CC { } } +@Serializable +data class NewUser( + val profile: Profile?, + val sameServers: Boolean, + val pastTimestamp: Boolean +) + sealed class ChatPagination { class Last(val count: Int): ChatPagination() class After(val chatItemId: Long, val count: Int): ChatPagination() 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 2a74524b3c..a9cfc1b906 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 @@ -64,7 +64,6 @@ fun DatabaseView( importArchiveAlert(m, context, uri, appFilesCountAndSize, progressIndicator) } } - val chatDbDeleted = remember { m.chatDbDeleted } LaunchedEffect(m.chatRunning) { runChat.value = m.chatRunning.value ?: true } @@ -83,7 +82,6 @@ fun DatabaseView( chatArchiveName, chatArchiveTime, chatLastStart, - chatDbDeleted.value, m.controller.appPrefs.privacyFullBackup, appFilesCountAndSize, chatItemTTL, @@ -134,7 +132,6 @@ fun DatabaseLayout( chatArchiveName: MutableState, chatArchiveTime: MutableState, chatLastStart: MutableState, - chatDbDeleted: Boolean, privacyFullBackup: SharedPreference, appFilesCountAndSize: MutableState>, chatItemTTL: MutableState, @@ -173,7 +170,7 @@ fun DatabaseLayout( SectionDividerSpaced(maxTopPadding = true) SectionView(stringResource(R.string.run_chat_section)) { - RunChatSetting(runChat, stopped, chatDbDeleted, startChat, stopChatAlert) + RunChatSetting(runChat, stopped, startChat, stopChatAlert) } SectionDividerSpaced() @@ -330,7 +327,6 @@ private fun TtlOptions(current: State, enabled: State, onS fun RunChatSetting( runChat: Boolean, stopped: Boolean, - chatDbDeleted: Boolean, startChat: () -> Unit, stopChatAlert: () -> Unit ) { @@ -341,7 +337,6 @@ fun RunChatSetting( iconColor = if (stopped) Color.Red else MaterialTheme.colors.primary, ) { DefaultSwitch( - enabled = !chatDbDeleted, checked = runChat, onCheckedChange = { runChatSwitch -> if (runChatSwitch) { @@ -371,9 +366,14 @@ private fun startChat(m: ChatModel, runChat: MutableState, chatLastSta ModalManager.shared.closeModals() return@withApi } - m.controller.apiStartChat() - runChat.value = true - m.chatRunning.value = true + if (m.currentUser.value == null) { + ModalManager.shared.closeModals() + return@withApi + } else { + m.controller.apiStartChat() + runChat.value = true + m.chatRunning.value = true + } val ts = Clock.System.now() m.controller.appPrefs.chatLastStart.set(ts) chatLastStart.value = ts @@ -410,7 +410,7 @@ private fun authStopChat(m: ChatModel, runChat: MutableState, context: authenticate( generalGetString(R.string.auth_stop_chat), generalGetString(R.string.auth_log_in_using_credential), - context as FragmentActivity, + activity = context as FragmentActivity, completed = { laResult -> when (laResult) { LAResult.Success, is LAResult.Unavailable -> { @@ -433,18 +433,28 @@ private fun authStopChat(m: ChatModel, runChat: MutableState, context: private fun stopChat(m: ChatModel, runChat: MutableState, context: Context) { withApi { try { - m.controller.apiStopChat() runChat.value = false - m.chatRunning.value = false - SimplexService.safeStopService(context) + stopChatAsync(m) + SimplexService.safeStopService(SimplexApp.context) MessagesFetcherWorker.cancelAll() } catch (e: Error) { runChat.value = true - AlertManager.shared.showAlertMsg(generalGetString(R.string.error_starting_chat), e.toString()) + AlertManager.shared.showAlertMsg(generalGetString(R.string.error_stopping_chat), e.toString()) } } } +suspend fun stopChatAsync(m: ChatModel) { + m.controller.apiStopChat() + m.chatRunning.value = false +} + +suspend fun deleteChatAsync(m: ChatModel) { + m.controller.apiDeleteStorage() + DatabaseUtils.ksDatabasePassword.remove() + m.controller.appPrefs.storeDBPassphrase.set(true) +} + private fun exportArchive( context: Context, m: ChatModel, @@ -619,10 +629,7 @@ private fun deleteChat(m: ChatModel, progressIndicator: MutableState) { progressIndicator.value = true withApi { try { - m.controller.apiDeleteStorage() - m.chatDbDeleted.value = true - DatabaseUtils.ksDatabasePassword.remove() - m.controller.appPrefs.storeDBPassphrase.set(true) + deleteChatAsync(m) operationEnded(m, progressIndicator) { AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_deleted), generalGetString(R.string.restart_the_app_to_create_a_new_chat_profile)) } @@ -717,7 +724,6 @@ fun PreviewDatabaseLayout() { chatArchiveName = remember { mutableStateOf("dummy_archive") }, chatArchiveTime = remember { mutableStateOf(Clock.System.now()) }, chatLastStart = remember { mutableStateOf(Clock.System.now()) }, - chatDbDeleted = false, privacyFullBackup = SharedPreference({ true }, {}), appFilesCountAndSize = remember { mutableStateOf(0 to 0L) }, chatItemTTL = remember { mutableStateOf(ChatItemTTL.None) }, 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 c82f2773d4..68829da559 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 @@ -18,9 +18,11 @@ object DatabaseUtils { private const val DATABASE_PASSWORD_ALIAS: String = "databasePassword" private const val APP_PASSWORD_ALIAS: String = "appPassword" + private const val SELF_DESTRUCT_PASSWORD_ALIAS: String = "selfDestructPassword" val ksDatabasePassword = KeyStoreItem(DATABASE_PASSWORD_ALIAS, appPreferences.encryptedDBPassphrase, appPreferences.initializationVectorDBPassphrase) 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) { fun get(): String? { 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 97ef5e1082..74c8cdedd0 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 @@ -28,16 +28,18 @@ data class LocalAuthRequest ( val title: String?, val reason: String, val password: String, + val selfDestruct: Boolean, val completed: (LAResult) -> Unit ) { companion object { - val sample = LocalAuthRequest(generalGetString(R.string.la_enter_app_passcode), generalGetString(R.string.la_authenticate), "") { } + val sample = LocalAuthRequest(generalGetString(R.string.la_enter_app_passcode), generalGetString(R.string.la_authenticate), "", selfDestruct = false) { } } } fun authenticate( promptTitle: String, promptSubtitle: String, + selfDestruct: Boolean = false, activity: FragmentActivity, usingLAMode: LAMode = SimplexApp.context.chatModel.controller.appPrefs.laMode.get(), completed: (LAResult) -> Unit @@ -59,7 +61,7 @@ fun authenticate( completed(LAResult.Error(generalGetString(R.string.authentication_cancelled))) } Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { - LocalAuthView(SimplexApp.context.chatModel, LocalAuthRequest(promptTitle, promptSubtitle, password) { + LocalAuthView(SimplexApp.context.chatModel, LocalAuthRequest(promptTitle, promptSubtitle, password, selfDestruct && SimplexApp.context.chatModel.controller.appPrefs.selfDestruct.get()) { close() completed(it) }) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ModalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ModalView.kt index ab1e123960..d4b48bdb9d 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ModalView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ModalView.kt @@ -78,8 +78,9 @@ class ModalManager { } fun closeModals() { - while (modalCount.value > 0) closeModal() - passcodeView.value = null + modalViews.clear() + toRemove.clear() + modalCount.value = 0 } @OptIn(ExperimentalAnimationApi::class) 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 index 25772c701a..e972ad8620 100644 --- 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 @@ -1,22 +1,80 @@ package chat.simplex.app.views.localauth +import android.util.Log 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.* +import chat.simplex.app.model.* +import chat.simplex.app.views.database.deleteChatAsync +import chat.simplex.app.views.database.stopChatAsync import chat.simplex.app.views.helpers.* +import chat.simplex.app.views.helpers.DatabaseUtils.ksSelfDestructPassword +import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword +import chat.simplex.app.views.onboarding.OnboardingStage +import kotlinx.coroutines.delay @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) + val sdPassword = ksSelfDestructPassword.get() + if (sdPassword == passcode.value && authRequest.selfDestruct) { + deleteStorageAndRestart(m, sdPassword) { r -> + authRequest.completed(r) + } + } else { + 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))) }) } + +private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: (LAResult) -> Unit) { + withBGApi { + try { + stopChatAsync(m) + deleteChatAsync(m) + ksAppPassword.set(password) + ksSelfDestructPassword.remove() + m.controller.ntfManager.cancelAllNotifications() + val selfDestructPref = m.controller.appPrefs.selfDestruct + val displayNamePref = m.controller.appPrefs.selfDestructDisplayName + val displayName = displayNamePref.get() + selfDestructPref.set(false) + displayNamePref.set(null) + m.chatDbChanged.value = true + m.chatDbStatus.value = null + try { + SimplexApp.context.initChatController(startChat = true) + } catch (e: Exception) { + Log.d(TAG, "initializeChat ${e.stackTraceToString()}") + } + m.chatDbChanged.value = false + if (m.currentUser.value != null) { + return@withBGApi + } + var profile: Profile? = null + if (!displayName.isNullOrEmpty()) { + profile = Profile(displayName = displayName, fullName = "") + } + val createdUser = m.controller.apiCreateActiveUser(profile, pastTimestamp = true) + m.currentUser.value = createdUser + m.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete) + m.onboardingStage.value = OnboardingStage.OnboardingComplete + if (createdUser != null) { + m.controller.startChat(createdUser) + } + ModalManager.shared.closeModals() + AlertManager.shared.hideAlert() + completed(LAResult.Success) + } catch (e: Exception) { + completed(LAResult.Error(generalGetString(R.string.incorrect_passcode))) + } + } +} 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 index cb6c6368d0..d7214a4143 100644 --- 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 @@ -4,11 +4,15 @@ import androidx.activity.compose.BackHandler import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import chat.simplex.app.R +import chat.simplex.app.views.helpers.DatabaseUtils import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword import chat.simplex.app.views.helpers.generalGetString @Composable fun SetAppPasscodeView( + passcodeKeychain: DatabaseUtils.KeyStoreItem = ksAppPassword, + title: String = generalGetString(R.string.new_passcode), + reason: String? = null, submit: () -> Unit, cancel: () -> Unit, close: () -> Unit @@ -23,7 +27,7 @@ fun SetAppPasscodeView( close() cancel() } - PasscodeView(passcode, title = title, submitLabel = submitLabel, submitEnabled = submitEnabled, submit = submit) { + PasscodeView(passcode, title = title, reason = reason, submitLabel = submitLabel, submitEnabled = submitEnabled, submit = submit) { close() cancel() } @@ -36,7 +40,7 @@ fun SetAppPasscodeView( submitEnabled = { pwd -> pwd == enteredPassword } ) { if (passcode.value == enteredPassword) { - ksAppPassword.set(passcode.value) + passcodeKeychain.set(passcode.value) enteredPassword = "" passcode.value = "" close() @@ -44,7 +48,7 @@ fun SetAppPasscodeView( } } } else { - SetPasswordView(generalGetString(R.string.new_passcode), generalGetString(R.string.save_verb)) { + SetPasswordView(title, 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/usersettings/CallSettings.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/CallSettings.kt index 997a9f9e54..ae38a01520 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/CallSettings.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/CallSettings.kt @@ -126,6 +126,30 @@ fun SharedPreferenceToggleWithIcon( } } +@Composable +fun SharedPreferenceToggleWithIcon( + text: String, + icon: Painter, + onClickInfo: () -> Unit, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, +) { + Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + Text(text, Modifier.padding(end = 4.dp)) + Icon( + icon, + null, + Modifier.clickable(onClick = onClickInfo), + tint = MaterialTheme.colors.primary + ) + Spacer(Modifier.fillMaxWidth().weight(1f)) + DefaultSwitch( + checked = checked, + onCheckedChange = onCheckedChange, + ) + } +} + @Composable fun SharedPreferenceRadioButton(text: String, prefState: MutableState, preference: SharedPreference, value: T) { Row(verticalAlignment = Alignment.CenterVertically) { 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 d2efb8416e..020d1d300c 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 @@ -6,9 +6,8 @@ import SectionItemView import SectionTextFooter import SectionView import android.view.WindowManager +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -17,12 +16,18 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.fragment.app.FragmentActivity import chat.simplex.app.R import chat.simplex.app.model.* +import chat.simplex.app.ui.theme.* +import chat.simplex.app.views.ProfileNameField import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword +import chat.simplex.app.views.helpers.DatabaseUtils.ksSelfDestructPassword +import chat.simplex.app.views.isValidDisplayName import chat.simplex.app.views.localauth.SetAppPasscodeView +import chat.simplex.app.views.onboarding.ReadableText enum class LAMode { SYSTEM, @@ -111,6 +116,9 @@ fun SimplexLockView( val laLockDelay = remember { chatModel.controller.appPrefs.laLockDelay } val showChangePasscode = remember { derivedStateOf { performLA.value && currentLAMode.state.value == LAMode.PASSCODE } } val activity = LocalContext.current as FragmentActivity + val selfDestructPref = remember { chatModel.controller.appPrefs.selfDestruct } + val selfDestructDisplayName = remember { mutableStateOf(chatModel.controller.appPrefs.selfDestructDisplayName.get() ?: "") } + val selfDestructDisplayNamePref = remember { chatModel.controller.appPrefs.selfDestructDisplayName } fun resetLAEnabled(onOff: Boolean) { chatModel.controller.appPrefs.performLA.set(onOff) @@ -123,6 +131,11 @@ fun SimplexLockView( laUnavailableInstructionAlert() } + fun resetSelfDestruct() { + selfDestructPref.set(false) + ksSelfDestructPassword.remove() + } + fun toggleLAMode(toLAMode: LAMode) { authenticate( if (toLAMode == LAMode.SYSTEM) { @@ -130,7 +143,7 @@ fun SimplexLockView( } else { generalGetString(R.string.chat_lock) }, - generalGetString(R.string.change_lock_mode), activity + generalGetString(R.string.change_lock_mode), activity = activity ) { laResult -> when (laResult) { is LAResult.Error -> { @@ -140,16 +153,15 @@ fun SimplexLockView( LAResult.Success -> { when (toLAMode) { LAMode.SYSTEM -> { - authenticate(generalGetString(R.string.auth_enable_simplex_lock), promptSubtitle = "", activity, toLAMode) { laResult -> + authenticate(generalGetString(R.string.auth_enable_simplex_lock), promptSubtitle = "", activity = activity, usingLAMode = toLAMode) { laResult -> when (laResult) { LAResult.Success -> { currentLAMode.set(toLAMode) ksAppPassword.remove() + resetSelfDestruct() laTurnedOnAlert() } - is LAResult.Unavailable, is LAResult.Error -> { - laFailedAlert() - } + is LAResult.Unavailable, is LAResult.Error -> laFailedAlert() is LAResult.Failed -> { /* Can be called multiple times on every failure */ } } } @@ -164,7 +176,7 @@ fun SimplexLockView( passcodeAlert(generalGetString(R.string.passcode_set)) }, cancel = {}, - close + close = close ) } } @@ -176,8 +188,27 @@ fun SimplexLockView( } } + fun toggleSelfDestruct(selfDestruct: SharedPreference) { + authenticate(generalGetString(R.string.la_current_app_passcode), generalGetString(R.string.change_self_destruct_mode), activity = activity) { laResult -> + when (laResult) { + is LAResult.Error -> laFailedAlert() + is LAResult.Failed -> { /* Can be called multiple times on every failure */ } + LAResult.Success -> { + if (!selfDestruct.get()) { + ModalManager.shared.showCustomModal { close -> + EnableSelfDestruct(selfDestruct, close) + } + } else { + resetSelfDestruct() + } + } + is LAResult.Unavailable -> disableUnavailableLA() + } + } + } + fun changeLAPassword() { - authenticate(generalGetString(R.string.la_current_app_passcode), generalGetString(R.string.la_change_app_passcode), activity) { laResult -> + authenticate(generalGetString(R.string.la_current_app_passcode), generalGetString(R.string.la_change_app_passcode), activity = activity) { laResult -> when (laResult) { LAResult.Success -> { ModalManager.shared.showCustomModal { close -> @@ -187,7 +218,32 @@ fun SimplexLockView( passcodeAlert(generalGetString(R.string.passcode_changed)) }, cancel = { passcodeAlert(generalGetString(R.string.passcode_not_changed)) - }, close + }, close = close + ) + } + } + } + is LAResult.Error -> laFailedAlert() + is LAResult.Failed -> {} + is LAResult.Unavailable -> disableUnavailableLA() + } + } + } + + fun changeSelfDestructPassword() { + authenticate(generalGetString(R.string.la_current_app_passcode), generalGetString(R.string.change_self_destruct_passcode), activity = activity) { laResult -> + when (laResult) { + LAResult.Success -> { + ModalManager.shared.showCustomModal { close -> + Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { + SetAppPasscodeView( + passcodeKeychain = ksSelfDestructPassword, + submit = { + selfDestructPasscodeAlert(generalGetString(R.string.self_destruct_passcode_changed)) + }, cancel = { + passcodeAlert(generalGetString(R.string.passcode_not_changed)) + }, + close = close ) } } @@ -223,7 +279,8 @@ fun SimplexLockView( }, cancel = { resetLAEnabled(false) - }, close + }, + close = close ) } } @@ -250,11 +307,85 @@ fun SimplexLockView( } } } + if (performLA.value && laMode.value == LAMode.PASSCODE) { + SectionDividerSpaced() + SectionView(stringResource(R.string.self_destruct_passcode).uppercase()) { + val openInfo = { + ModalManager.shared.showModal { + SelfDestructInfoView() + } + } + SettingsActionItemWithContent(null, null, click = openInfo) { + SharedPreferenceToggleWithIcon( + stringResource(R.string.enable_self_destruct), + painterResource(R.drawable.ic_info), + openInfo, + remember { selfDestructPref.state }.value + ) { + toggleSelfDestruct(selfDestructPref) + } + } + + if (remember { selfDestructPref.state }.value) { + Column(Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF)) { + Text( + stringResource(R.string.self_destruct_new_display_name), + fontSize = 16.sp, + modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF) + ) + ProfileNameField(selfDestructDisplayName, "", ::isValidDisplayName) + LaunchedEffect(selfDestructDisplayName.value) { + val new = selfDestructDisplayName.value + if (isValidDisplayName(new) && selfDestructDisplayNamePref.get() != new) { + selfDestructDisplayNamePref.set(new) + } + } + } + SectionItemView({ changeSelfDestructPassword() }) { + Text(stringResource(R.string.change_self_destruct_passcode)) + } + } + } + } } SectionBottomSpacer() } } +@Composable +private fun SelfDestructInfoView() { + Column( + Modifier.fillMaxWidth().verticalScroll(rememberScrollState()).padding(horizontal = DEFAULT_PADDING), + ) { + AppBarTitle(stringResource(R.string.self_destruct), withPadding = false) + ReadableText(stringResource(R.string.if_you_enter_self_destruct_code)) + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + TextListItem("1.", stringResource(R.string.all_app_data_will_be_cleared)) + TextListItem("2.", stringResource(R.string.app_passcode_replaced_with_self_destruct)) + TextListItem("3.", stringResource(R.string.empty_chat_profile_is_created)) + } + SectionBottomSpacer() + } +} + +@Composable +private fun EnableSelfDestruct( + selfDestruct: SharedPreference, + close: () -> Unit +) { + Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { + SetAppPasscodeView( + passcodeKeychain = ksSelfDestructPassword, title = generalGetString(R.string.set_passcode), reason = generalGetString(R.string.enabled_self_destruct_passcode), + submit = { + selfDestruct.set(true) + selfDestructPasscodeAlert(generalGetString(R.string.self_destruct_passcode_enabled)) + }, + cancel = {}, + close = close + ) + } +} + @Composable private fun EnableLock(performLA: MutableState, onCheckedChange: (Boolean) -> Unit) { SectionItemView { @@ -300,6 +431,14 @@ private fun LockDelaySelector(state: State, onSelected: (Int) -> Unit) { ) } +@Composable +private fun TextListItem(n: String, text: String) { + Box { + Text(n) + Text(text, Modifier.padding(start = 20.dp)) + } +} + private fun laDelayText(t: Int): String { val m = t / 60 val s = t % 60 @@ -319,3 +458,7 @@ private fun passcodeAlert(title: String) { text = generalGetString(R.string.la_please_remember_to_store_password) ) } + +private fun selfDestructPasscodeAlert(title: String) { + AlertManager.shared.showAlertMsg(title, generalGetString(R.string.if_you_enter_passcode_data_removed)) +} 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 65482116a0..1875dc9cac 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 @@ -483,7 +483,7 @@ private fun runAuth(title: String, desc: String, context: Context, onFinish: (su authenticate( title, desc, - context as FragmentActivity, + activity = context as FragmentActivity, completed = { laResult -> onFinish(laResult == LAResult.Success || laResult is LAResult.Unavailable) } diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index e8628988d0..ed8189604f 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -808,7 +808,7 @@ Lock mode Lock after Submit - Confirm Passcode + Confirm passcode Incorrect passcode New Passcode Authentication cancelled @@ -818,6 +818,21 @@ Passcode changed! Passcode not changed! Change lock mode + Self-destruct + Enable self-destruct passcode + Change self-destruct mode + Change self-destruct passcode + Self-destruct passcode enabled! + Self-destruct passcode changed! + Self-destruct passcode + Enable self-destruct + New display name: + If you enter your self-destruct passcode while opening the app: + All app data is deleted. + App passcode is replaced with self-destruct passcode. + An empty chat profile with the provided name is created, and the app opens as usual. + If you enter this passcode when opening the app, all app data will be irreversibly removed! + Set passcode YOU diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift index 92bf35867f..982768710a 100644 --- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift +++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift @@ -120,8 +120,8 @@ struct SimplexLockView: View { case laUnavailableTurningOffAlert case laPasscodeSetAlert case laPasscodeChangedAlert - case laSeldDestructPasscodeSetAlert - case laSeldDestructPasscodeChangedAlert + case laSelfDestructPasscodeSetAlert + case laSelfDestructPasscodeChangedAlert case laPasscodeNotChangedAlert var id: Self { self } @@ -238,8 +238,8 @@ struct SimplexLockView: View { case .laUnavailableTurningOffAlert: return laUnavailableTurningOffAlert() case .laPasscodeSetAlert: return passcodeAlert("Passcode set!") case .laPasscodeChangedAlert: return passcodeAlert("Passcode changed!") - case .laSeldDestructPasscodeSetAlert: return selfDestructPasscodeAlert("Self-destruct passcode enabled!") - case .laSeldDestructPasscodeChangedAlert: return selfDestructPasscodeAlert("Self-destruct passcode changed!") + case .laSelfDestructPasscodeSetAlert: return selfDestructPasscodeAlert("Self-destruct passcode enabled!") + case .laSelfDestructPasscodeChangedAlert: return selfDestructPasscodeAlert("Self-destruct passcode changed!") case .laPasscodeNotChangedAlert: return mkAlert(title: "Passcode not changed!") } } @@ -272,13 +272,13 @@ struct SimplexLockView: View { case .enableSelfDestruct: SetAppPasscodeView(passcodeKeychain: kcSelfDestructPassword, title: "Set passcode", reason: NSLocalizedString("Enable self-destruct passcode", comment: "set passcode view")) { updateSelfDestruct() - showLAAlert(.laSeldDestructPasscodeSetAlert) + showLAAlert(.laSelfDestructPasscodeSetAlert) } cancel: { revertSelfDestruct() } case .changeSelfDestructPasscode: SetAppPasscodeView(passcodeKeychain: kcSelfDestructPassword, reason: NSLocalizedString("Change self-destruct passcode", comment: "set passcode view")) { - showLAAlert(.laSeldDestructPasscodeChangedAlert) + showLAAlert(.laSelfDestructPasscodeChangedAlert) } cancel: { showLAAlert(.laPasscodeNotChangedAlert) } @@ -296,17 +296,17 @@ struct SimplexLockView: View { private func selfDestructInfoView() -> some View { VStack(alignment: .leading) { - Text("Self-desctruct") + Text("Self-destruct") .font(.largeTitle) .bold() .padding(.vertical) ScrollView { VStack(alignment: .leading) { Group { - Text("If you enter your self-desctruct passcode while opening the app:") + Text("If you enter your self-destruct passcode while opening the app:") VStack(spacing: 8) { textListItem("1.", "All app data is deleted.") - textListItem("2.", "App passcode is replaced with self-desctruct passcode.") + textListItem("2.", "App passcode is replaced with self-destruct passcode.") textListItem("3.", "An empty chat profile with the provided name is created, and the app opens as usual.") } }