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

View File

@ -97,6 +97,7 @@ actual class GlobalExceptionsHandler: Thread.UncaughtExceptionHandler {
mainActivity.get()?.recreate() mainActivity.get()?.recreate()
} else { } else {
mainActivity.get()?.apply { mainActivity.get()?.apply {
runOnUiThread {
window window
?.decorView ?.decorView
?.findViewById<ViewGroup>(android.R.id.content) ?.findViewById<ViewGroup>(android.R.id.content)
@ -106,6 +107,7 @@ actual class GlobalExceptionsHandler: Thread.UncaughtExceptionHandler {
} }
} }
} }
}
// Wait until activity recreates to prevent showing two alerts (in case `main` was crashed) // Wait until activity recreates to prevent showing two alerts (in case `main` was crashed)
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(

View File

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

View File

@ -1,8 +1,13 @@
package chat.simplex.common.platform package chat.simplex.common.platform
import chat.simplex.common.model.* 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.*
import chat.simplex.common.views.helpers.DatabaseUtils.ksDatabasePassword
import chat.simplex.common.views.onboarding.OnboardingStage import chat.simplex.common.views.onboarding.OnboardingStage
import chat.simplex.res.MR
import kotlinx.coroutines.*
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import java.nio.ByteBuffer import java.nio.ByteBuffer
@ -39,13 +44,17 @@ val chatController: ChatController = ChatController
fun initChatControllerAndRunMigrations(ignoreSelfDestruct: Boolean) { fun initChatControllerAndRunMigrations(ignoreSelfDestruct: Boolean) {
if (ignoreSelfDestruct || DatabaseUtils.ksSelfDestructPassword.get() == null) { if (ignoreSelfDestruct || DatabaseUtils.ksSelfDestructPassword.get() == null) {
withBGApi { withBGApi {
if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) {
initChatController(startChat = ::showStartChatAfterRestartAlert)
} else {
initChatController() initChatController()
}
runMigrations() 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 { try {
chatModel.ctrlInitInProgress.value = true chatModel.ctrlInitInProgress.value = true
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey() val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
@ -62,10 +71,19 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
chatModel.chatDbStatus.value = res chatModel.chatDbStatus.value = res
if (res != DBMigrationResult.OK) { if (res != DBMigrationResult.OK) {
Log.d(TAG, "Unable to migrate successfully: $res") Log.d(TAG, "Unable to migrate successfully: $res")
} else if (startChat) { 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 we migrated successfully means previous re-encryption process on database level finished successfully too
if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null) if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null)
val user = chatController.apiGetActiveUser(null) val user = chatController.apiGetActiveUser(null)
chatModel.currentUser.value = user
if (user == null) { if (user == null) {
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
chatModel.currentUser.value = null chatModel.currentUser.value = null
@ -83,7 +101,7 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
} else { } else {
chatController.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) chatController.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
} }
} else { } else if (startChat().await()) {
val savedOnboardingStage = appPreferences.onboardingStage.get() val savedOnboardingStage = appPreferences.onboardingStage.get()
val newStage = if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) { val newStage = if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) {
OnboardingStage.Step3_CreateSimpleXAddress OnboardingStage.Step3_CreateSimpleXAddress
@ -93,14 +111,26 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
if (appPreferences.onboardingStage.get() != newStage) { if (appPreferences.onboardingStage.get() != newStage) {
appPreferences.onboardingStage.set(newStage) appPreferences.onboardingStage.set(newStage)
} }
if (appPreferences.onboardingStage.get() == OnboardingStage.OnboardingComplete && !chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.get()) {
chatModel.setDeliveryReceipts.value = true
}
chatController.startChat(user) chatController.startChat(user)
platform.androidChatInitializedAndStarted() platform.androidChatInitializedAndStarted()
} } else {
chatController.getUserChatData(null)
chatModel.localUserCreated.value = currentUser.value != null
chatModel.chatRunning.value = false
} }
} finally { } finally {
chatModel.ctrlInitInProgress.value = false 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) { fun callRunChat(confirmMigrations: MigrationConfirmation? = null) {
val useKey = if (useKeychain) null else dbKey.value val useKey = if (useKeychain) null else dbKey.value
runChat(useKey, confirmMigrations, chatDbStatus, progressIndicator, appPreferences) runChat(useKey, confirmMigrations, chatDbStatus, progressIndicator)
} }
fun saveAndRunChatOnClick() { fun saveAndRunChatOnClick() {
@ -190,13 +190,14 @@ private fun runChat(
confirmMigrations: MigrationConfirmation? = null, confirmMigrations: MigrationConfirmation? = null,
chatDbStatus: State<DBMigrationResult?>, chatDbStatus: State<DBMigrationResult?>,
progressIndicator: MutableState<Boolean>, progressIndicator: MutableState<Boolean>,
prefs: AppPreferences
) = CoroutineScope(Dispatchers.Default).launch { ) = CoroutineScope(Dispatchers.Default).launch {
// Don't do things concurrently. Shouldn't be here concurrently, just in case // Don't do things concurrently. Shouldn't be here concurrently, just in case
if (progressIndicator.value) return@launch if (progressIndicator.value) return@launch
progressIndicator.value = true progressIndicator.value = true
try { try {
initChatController(dbKey, confirmMigrations) initChatController(dbKey, confirmMigrations,
startChat = if (appPreferences.chatStopped.get()) ::showStartChatAfterRestartAlert else { { CompletableDeferred(true) } }
)
} catch (e: Exception) { } catch (e: Exception) {
Log.d(TAG, "initializeChat ${e.stackTraceToString()}") Log.d(TAG, "initializeChat ${e.stackTraceToString()}")
} }

View File

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

View File

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

View File

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

View File

@ -1112,6 +1112,8 @@
<!-- ChatModel.chatRunning interactions --> <!-- ChatModel.chatRunning interactions -->
<string name="chat_is_stopped_indication">Chat is stopped</string> <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="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 --> <!-- ChatArchiveView.kt -->
<string name="chat_archive_header">Chat archive</string> <string name="chat_archive_header">Chat archive</string>