android, desktop: run with stopped chat (#3624)

* android, desktop: run with stopped chat

* way to prevent starting a chat in case of not saved database key

* rename

* change position of a call

* new way of doing the same

* better

* exit process

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
Stanislav Dmitrenko 2024-01-02 21:21:39 +07:00 committed by GitHub
parent c9b1d54f13
commit e6b5727003
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 100 additions and 68 deletions

View File

@ -13,7 +13,6 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import chat.simplex.common.platform.Log
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.work.*
@ -21,12 +20,13 @@ import chat.simplex.common.AppLock
import chat.simplex.common.helpers.requiresIgnoringBattery
import chat.simplex.common.model.ChatController
import chat.simplex.common.model.NotificationsMode
import chat.simplex.common.platform.androidAppContext
import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.*
import kotlinx.coroutines.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlin.system.exitProcess
// based on:
// https://robertohuertas.com/2019/06/29/android_foreground_services/
@ -173,6 +173,11 @@ class SimplexService: Service() {
// Just to make sure that after restart of the app the user will need to re-authenticate
AppLock.clearAuthState()
if (appPreferences.chatStopped.get()) {
stopSelf()
exitProcess(0)
}
// If notification service isn't enabled or battery optimization isn't disabled, we shouldn't restart the service
if (!SimplexApp.context.allowToStartServiceAfterAppExit()) {
return

View File

@ -97,12 +97,14 @@ actual class GlobalExceptionsHandler: Thread.UncaughtExceptionHandler {
mainActivity.get()?.recreate()
} else {
mainActivity.get()?.apply {
window
?.decorView
?.findViewById<ViewGroup>(android.R.id.content)
?.removeViewAt(0)
setContent {
AppScreen()
runOnUiThread {
window
?.decorView
?.findViewById<ViewGroup>(android.R.id.content)
?.removeViewAt(0)
setContent {
AppScreen()
}
}
}
}

View File

@ -106,6 +106,7 @@ class AppPreferences {
val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null)
val chatArchiveTime = mkDatePreference(SHARED_PREFS_CHAT_ARCHIVE_TIME, null)
val chatLastStart = mkDatePreference(SHARED_PREFS_CHAT_LAST_START, null)
val chatStopped = mkBoolPreference(SHARED_PREFS_CHAT_STOPPED, false)
val developerTools = mkBoolPreference(SHARED_PREFS_DEVELOPER_TOOLS, false)
val terminalAlwaysVisible = mkBoolPreference(SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE, false)
val networkUseSocksProxy = mkBoolPreference(SHARED_PREFS_NETWORK_USE_SOCKS_PROXY, false)
@ -273,6 +274,7 @@ class AppPreferences {
private const val SHARED_PREFS_APP_LANGUAGE = "AppLanguage"
private const val SHARED_PREFS_ONBOARDING_STAGE = "OnboardingStage"
private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart"
private const val SHARED_PREFS_CHAT_STOPPED = "ChatStopped"
private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools"
private const val SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE = "TerminalAlwaysVisible"
private const val SHARED_PREFS_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy"
@ -346,14 +348,8 @@ object ChatController {
try {
if (chatModel.chatRunning.value == true) return
apiSetNetworkConfig(getNetCfg())
apiSetTempFolder(coreTmpDir.absolutePath)
apiSetFilesFolder(appFilesDir.absolutePath)
if (appPlatform.isDesktop) {
apiSetRemoteHostsFolder(remoteHostsDir.absolutePath)
}
apiSetXFTPConfig(getXFTPCfg())
apiSetEncryptLocalFiles(appPrefs.privacyEncryptLocalFiles.get())
val justStarted = apiStartChat()
appPrefs.chatStopped.set(false)
val users = listUsers(null)
chatModel.users.clear()
chatModel.users.addAll(users)
@ -365,6 +361,9 @@ object ChatController {
chatModel.chatRunning.value = true
startReceiver()
setLocalDeviceName(appPrefs.deviceNameForRemoteAccess.get()!!)
if (appPreferences.onboardingStage.get() == OnboardingStage.OnboardingComplete && !chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.get()) {
chatModel.setDeliveryReceipts.value = true
}
Log.d(TAG, "startChat: started")
} else {
updatingChatsMutex.withLock {
@ -383,13 +382,6 @@ object ChatController {
Log.d(TAG, "user: null")
try {
if (chatModel.chatRunning.value == true) return
apiSetTempFolder(coreTmpDir.absolutePath)
apiSetFilesFolder(appFilesDir.absolutePath)
if (appPlatform.isDesktop) {
apiSetRemoteHostsFolder(remoteHostsDir.absolutePath)
}
apiSetXFTPConfig(getXFTPCfg())
apiSetEncryptLocalFiles(appPrefs.privacyEncryptLocalFiles.get())
chatModel.users.clear()
chatModel.currentUser.value = null
chatModel.localUserCreated.value = false
@ -596,19 +588,19 @@ object ChatController {
}
}
private suspend fun apiSetTempFolder(tempFolder: String) {
suspend fun apiSetTempFolder(tempFolder: String) {
val r = sendCmd(null, CC.SetTempFolder(tempFolder))
if (r is CR.CmdOk) return
throw Error("failed to set temp folder: ${r.responseType} ${r.details}")
}
private suspend fun apiSetFilesFolder(filesFolder: String) {
suspend fun apiSetFilesFolder(filesFolder: String) {
val r = sendCmd(null, CC.SetFilesFolder(filesFolder))
if (r is CR.CmdOk) return
throw Error("failed to set files folder: ${r.responseType} ${r.details}")
}
private suspend fun apiSetRemoteHostsFolder(remoteHostsFolder: String) {
suspend fun apiSetRemoteHostsFolder(remoteHostsFolder: String) {
val r = sendCmd(null, CC.SetRemoteHostsFolder(remoteHostsFolder))
if (r is CR.CmdOk) return
throw Error("failed to set remote hosts folder: ${r.responseType} ${r.details}")

View File

@ -1,8 +1,13 @@
package chat.simplex.common.platform
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.model.ChatModel.currentUser
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.helpers.DatabaseUtils.ksDatabasePassword
import chat.simplex.common.views.onboarding.OnboardingStage
import chat.simplex.res.MR
import kotlinx.coroutines.*
import kotlinx.serialization.decodeFromString
import java.nio.ByteBuffer
@ -39,13 +44,17 @@ val chatController: ChatController = ChatController
fun initChatControllerAndRunMigrations(ignoreSelfDestruct: Boolean) {
if (ignoreSelfDestruct || DatabaseUtils.ksSelfDestructPassword.get() == null) {
withBGApi {
initChatController()
if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) {
initChatController(startChat = ::showStartChatAfterRestartAlert)
} else {
initChatController()
}
runMigrations()
}
}
}
suspend fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: Boolean = true) {
suspend fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: () -> CompletableDeferred<Boolean> = { CompletableDeferred(true) }) {
try {
chatModel.ctrlInitInProgress.value = true
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
@ -62,45 +71,66 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
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 {
return
}
controller.apiSetTempFolder(coreTmpDir.absolutePath)
controller.apiSetFilesFolder(appFilesDir.absolutePath)
if (appPlatform.isDesktop) {
controller.apiSetRemoteHostsFolder(remoteHostsDir.absolutePath)
}
controller.apiSetXFTPConfig(controller.getXFTPCfg())
controller.apiSetEncryptLocalFiles(controller.appPrefs.privacyEncryptLocalFiles.get())
// 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)
chatModel.currentUser.value = user
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 {
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()
chatController.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
}
} else if (startChat().await()) {
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)
}
chatController.startChat(user)
platform.androidChatInitializedAndStarted()
} else {
chatController.getUserChatData(null)
chatModel.localUserCreated.value = currentUser.value != null
chatModel.chatRunning.value = false
}
} finally {
chatModel.ctrlInitInProgress.value = false
}
}
fun showStartChatAfterRestartAlert(): CompletableDeferred<Boolean> {
val deferred = CompletableDeferred<Boolean>()
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.start_chat_question),
text = generalGetString(MR.strings.chat_is_stopped_you_should_transfer_database),
onConfirm = { deferred.complete(true) },
onDismiss = { deferred.complete(false) },
onDismissRequest = { deferred.complete(false) }
)
return deferred
}

View File

@ -44,7 +44,7 @@ fun DatabaseErrorView(
fun callRunChat(confirmMigrations: MigrationConfirmation? = null) {
val useKey = if (useKeychain) null else dbKey.value
runChat(useKey, confirmMigrations, chatDbStatus, progressIndicator, appPreferences)
runChat(useKey, confirmMigrations, chatDbStatus, progressIndicator)
}
fun saveAndRunChatOnClick() {
@ -190,13 +190,14 @@ private fun runChat(
confirmMigrations: MigrationConfirmation? = null,
chatDbStatus: State<DBMigrationResult?>,
progressIndicator: MutableState<Boolean>,
prefs: AppPreferences
) = CoroutineScope(Dispatchers.Default).launch {
// Don't do things concurrently. Shouldn't be here concurrently, just in case
if (progressIndicator.value) return@launch
progressIndicator.value = true
try {
initChatController(dbKey, confirmMigrations)
initChatController(dbKey, confirmMigrations,
startChat = if (appPreferences.chatStopped.get()) ::showStartChatAfterRestartAlert else { { CompletableDeferred(true) } }
)
} catch (e: Exception) {
Log.d(TAG, "initializeChat ${e.stackTraceToString()}")
}

View File

@ -378,12 +378,12 @@ private fun startChat(m: ChatModel, chatLastStart: MutableState<Instant?>, chatD
ModalManager.closeAllModalsEverywhere()
return@withApi
}
if (m.currentUser.value == null) {
val user = m.currentUser.value
if (user == null) {
ModalManager.closeAllModalsEverywhere()
return@withApi
} else {
m.controller.apiStartChat()
m.chatRunning.value = true
m.controller.startChat(user)
}
val ts = Clock.System.now()
m.controller.appPrefs.chatLastStart.set(ts)
@ -453,6 +453,7 @@ private fun stopChat(m: ChatModel) {
suspend fun stopChatAsync(m: ChatModel) {
m.controller.apiStopChat()
m.chatRunning.value = false
controller.appPrefs.chatStopped.set(true)
}
suspend fun deleteChatAsync(m: ChatModel) {

View File

@ -82,7 +82,6 @@ sealed class DBMigrationResult {
@Serializable @SerialName("unknown") data class Unknown(val json: String): DBMigrationResult()
}
enum class MigrationConfirmation(val value: String) {
YesUp("yesUp"),
YesUpDown ("yesUpDown"),

View File

@ -84,7 +84,7 @@ private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: (
m.chatDbChanged.value = true
m.chatDbStatus.value = null
try {
initChatController(startChat = true)
initChatController()
} catch (e: Exception) {
Log.d(TAG, "initializeChat ${e.stackTraceToString()}")
}

View File

@ -1112,6 +1112,8 @@
<!-- ChatModel.chatRunning interactions -->
<string name="chat_is_stopped_indication">Chat is stopped</string>
<string name="you_can_start_chat_via_setting_or_by_restarting_the_app">You can start chat via app Settings / Database or by restarting the app.</string>
<string name="start_chat_question">Start chat?</string>
<string name="chat_is_stopped_you_should_transfer_database">Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</string>
<!-- ChatArchiveView.kt -->
<string name="chat_archive_header">Chat archive</string>