Merge branch 'master' into master-android

This commit is contained in:
Evgeny Poberezkin
2024-07-02 15:08:07 +01:00
44 changed files with 1113 additions and 183 deletions
@@ -52,6 +52,7 @@ object ChatModel {
val chatDbStatus = mutableStateOf<DBMigrationResult?>(null)
val ctrlInitInProgress = mutableStateOf(false)
val dbMigrationInProgress = mutableStateOf(false)
val incompleteInitializedDbRemoved = mutableStateOf(false)
val chats = mutableStateListOf<Chat>()
// map of connections network statuses, key is agent connection id
val networkStatuses = mutableStateMapOf<String, NetworkStatus>()
@@ -176,6 +176,12 @@ class AppPreferences {
val selfDestruct = mkBoolPreference(SHARED_PREFS_SELF_DESTRUCT, false)
val selfDestructDisplayName = mkStrPreference(SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME, null)
// This flag is set when database is first initialized and resets only when the database is removed.
// This is needed for recover from incomplete initialization when only one database file is created.
// If false - the app will clear database folder on missing file and re-initialize.
// Note that this situation can only happen if passphrase for the first database is incorrect because, otherwise, backend will re-create second database automatically
val newDatabaseInitialized = mkBoolPreference(SHARED_PREFS_NEW_DATABASE_INITIALIZED, false)
val currentTheme = mkStrPreference(SHARED_PREFS_CURRENT_THEME, DefaultTheme.SYSTEM_THEME_NAME)
val systemDarkTheme = mkStrPreference(SHARED_PREFS_SYSTEM_DARK_THEME, DefaultTheme.SIMPLEX.themeName)
val currentThemeIds = mkMapPreference(SHARED_PREFS_CURRENT_THEME_IDs, mapOf(), encode = {
@@ -361,6 +367,7 @@ class AppPreferences {
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_NEW_DATABASE_INITIALIZED = "NewDatabaseInitialized"
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"
@@ -3018,7 +3025,7 @@ data class ServerCfg(
val server: String,
val preset: Boolean,
val tested: Boolean? = null,
val enabled: Boolean
val enabled: ServerEnabled
) {
@Transient
private val createdAt: Date = Date()
@@ -3032,7 +3039,7 @@ data class ServerCfg(
get() = server.isBlank()
companion object {
val empty = ServerCfg(remoteHostId = null, server = "", preset = false, tested = null, enabled = true)
val empty = ServerCfg(remoteHostId = null, server = "", preset = false, tested = null, enabled = ServerEnabled.Enabled)
class SampleData(
val preset: ServerCfg,
@@ -3046,26 +3053,33 @@ data class ServerCfg(
server = "smp://abcd@smp8.simplex.im",
preset = true,
tested = true,
enabled = true
enabled = ServerEnabled.Enabled
),
custom = ServerCfg(
remoteHostId = null,
server = "smp://abcd@smp9.simplex.im",
preset = false,
tested = false,
enabled = false
enabled = ServerEnabled.Disabled
),
untested = ServerCfg(
remoteHostId = null,
server = "smp://abcd@smp10.simplex.im",
preset = false,
tested = null,
enabled = true
enabled = ServerEnabled.Enabled
)
)
}
}
@Serializable
enum class ServerEnabled {
@SerialName("disabled") Disabled,
@SerialName("enabled") Enabled,
@SerialName("known") Known;
}
@Serializable
enum class ProtocolTestStep {
@SerialName("connect") Connect,
@@ -88,8 +88,23 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
chatModel.chatDbStatus.value = res
if (res != DBMigrationResult.OK) {
Log.d(TAG, "Unable to migrate successfully: $res")
if (!appPrefs.newDatabaseInitialized.get() && DatabaseUtils.hasOnlyOneDatabase(dataDir.absolutePath)) {
if (chatModel.incompleteInitializedDbRemoved.value) {
Log.d(TAG, "Incomplete initialized databases were removed but after repeated migration only one database exists again, not trying to remove again")
} else {
val dbPath = dbAbsolutePrefixPath
File(dbPath + "_chat.db").delete()
File(dbPath + "_agent.db").delete()
chatModel.incompleteInitializedDbRemoved.value = true
Log.d(TAG, "Incomplete initialized databases were removed for the first time, repeating migration")
chatModel.ctrlInitInProgress.value = false
initChatController(useKey, confirmMigrations, startChat)
}
}
return
}
appPrefs.newDatabaseInitialized.set(true)
chatModel.incompleteInitializedDbRemoved.value = false
platform.androidRestartNetworkObserver()
controller.apiSetAppFilePaths(
appFilesDir.absolutePath,
@@ -22,8 +22,11 @@ import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.usersettings.AppVersionText
import chat.simplex.common.views.usersettings.SettingsActionItem
import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.*
import kotlinx.datetime.Clock
import java.io.File
@@ -106,7 +109,7 @@ fun DatabaseErrorView(
}
}
is DBMigrationResult.ErrorMigration -> when (val err = status.migrationError) {
is MigrationError.Upgrade ->
is MigrationError.Upgrade -> {
DatabaseErrorDetails(MR.strings.database_upgrade) {
TextButton({ callRunChat(confirmMigrations = MigrationConfirmation.YesUp) }, Modifier.align(Alignment.CenterHorizontally), enabled = !progressIndicator.value) {
Text(generalGetString(MR.strings.upgrade_and_open_chat))
@@ -116,7 +119,9 @@ fun DatabaseErrorView(
MigrationsText(err.upMigrations.map { it.upName })
AppVersionText()
}
is MigrationError.Downgrade ->
OpenDatabaseDirectoryButton()
}
is MigrationError.Downgrade -> {
DatabaseErrorDetails(MR.strings.database_downgrade) {
TextButton({ callRunChat(confirmMigrations = MigrationConfirmation.YesUpDown) }, Modifier.align(Alignment.CenterHorizontally), enabled = !progressIndicator.value) {
Text(generalGetString(MR.strings.downgrade_and_open_chat))
@@ -127,29 +132,41 @@ fun DatabaseErrorView(
MigrationsText(err.downMigrations)
AppVersionText()
}
is MigrationError.Error ->
OpenDatabaseDirectoryButton()
}
is MigrationError.Error -> {
DatabaseErrorDetails(MR.strings.incompatible_database_version) {
FileNameText(status.dbFile)
Text(String.format(generalGetString(MR.strings.error_with_info), mtrErrorDescription(err.mtrError)))
}
OpenDatabaseDirectoryButton()
}
}
is DBMigrationResult.ErrorSQL ->
is DBMigrationResult.ErrorSQL -> {
DatabaseErrorDetails(MR.strings.database_error) {
FileNameText(status.dbFile)
Text(String.format(generalGetString(MR.strings.error_with_info), status.migrationSQLError))
}
is DBMigrationResult.ErrorKeychain ->
OpenDatabaseDirectoryButton()
}
is DBMigrationResult.ErrorKeychain -> {
DatabaseErrorDetails(MR.strings.keychain_error) {
Text(generalGetString(MR.strings.cannot_access_keychain))
}
is DBMigrationResult.InvalidConfirmation ->
OpenDatabaseDirectoryButton()
}
is DBMigrationResult.InvalidConfirmation -> {
DatabaseErrorDetails(MR.strings.invalid_migration_confirmation) {
// this can only happen if incorrect parameter is passed
}
is DBMigrationResult.Unknown ->
OpenDatabaseDirectoryButton()
}
is DBMigrationResult.Unknown -> {
DatabaseErrorDetails(MR.strings.database_error) {
Text(String.format(generalGetString(MR.strings.unknown_database_error_with_info), status.json))
}
OpenDatabaseDirectoryButton()
}
is DBMigrationResult.OK -> {}
null -> {}
}
@@ -294,6 +311,18 @@ private fun ColumnScope.SaveAndOpenButton(enabled: Boolean, onClick: () -> Unit)
}
}
@Composable
private fun OpenDatabaseDirectoryButton() {
if (appPlatform.isDesktop) {
Spacer(Modifier.padding(top = DEFAULT_PADDING))
SettingsActionItem(
painterResource(MR.images.ic_folder_open),
stringResource(MR.strings.open_database_folder),
::desktopOpenDatabaseDir
)
}
}
@Composable
private fun ColumnScope.OpenChatButton(enabled: Boolean, onClick: () -> Unit) {
TextButton(onClick, Modifier.align(Alignment.CenterHorizontally), enabled = enabled) {
@@ -18,6 +18,7 @@ import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.model.ChatModel.updatingChatsMutex
import chat.simplex.common.ui.theme.*
@@ -492,6 +493,7 @@ fun deleteChatDatabaseFilesAndState() {
wallpapersDir.deleteRecursively()
wallpapersDir.mkdirs()
DatabaseUtils.ksDatabasePassword.remove()
appPrefs.newDatabaseInitialized.set(false)
controller.appPrefs.storeDBPassphrase.set(true)
controller.ctrl = null
@@ -39,22 +39,25 @@ object DatabaseUtils {
}
}
private fun hasDatabase(rootDir: String): Boolean =
File(rootDir + File.separator + chatDatabaseFileName).exists() && File(rootDir + File.separator + agentDatabaseFileName).exists()
private fun hasAtLeastOneDatabase(rootDir: String): Boolean =
File(rootDir + File.separator + chatDatabaseFileName).exists() || File(rootDir + File.separator + agentDatabaseFileName).exists()
fun hasOnlyOneDatabase(rootDir: String): Boolean =
File(rootDir + File.separator + chatDatabaseFileName).exists() != File(rootDir + File.separator + agentDatabaseFileName).exists()
fun useDatabaseKey(): String {
Log.d(TAG, "useDatabaseKey ${appPreferences.storeDBPassphrase.get()}")
var dbKey = ""
val useKeychain = appPreferences.storeDBPassphrase.get()
if (useKeychain) {
if (!hasDatabase(dataDir.absolutePath)) {
if (!hasAtLeastOneDatabase(dataDir.absolutePath)) {
dbKey = randomDatabasePassword()
ksDatabasePassword.set(dbKey)
appPreferences.initialRandomDBPassphrase.set(true)
} else {
dbKey = ksDatabasePassword.get() ?: ""
}
} else if (appPlatform.isDesktop && !hasDatabase(dataDir.absolutePath)) {
} else if (appPlatform.isDesktop && !hasAtLeastOneDatabase(dataDir.absolutePath)) {
// In case of database was deleted by hand
dbKey = randomDatabasePassword()
ksDatabasePassword.set(dbKey)
@@ -175,8 +175,10 @@ private fun UseServerSection(
Text(stringResource(MR.strings.smp_servers_test_server), color = if (valid && !testing) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary)
ShowTestStatus(server)
}
val enabled = rememberUpdatedState(server.enabled)
PreferenceToggle(stringResource(MR.strings.smp_servers_use_server_for_new_conn), enabled.value) { onUpdate(server.copy(enabled = it)) }
val enabled = rememberUpdatedState(server.enabled == ServerEnabled.Enabled)
PreferenceToggle(stringResource(MR.strings.smp_servers_use_server_for_new_conn), enabled.value) { enable ->
onUpdate(server.copy(enabled = if (enable) ServerEnabled.Enabled else ServerEnabled.Disabled))
}
SectionItemView(onDelete, disabled = testing) {
Text(stringResource(MR.strings.smp_servers_delete_server), color = if (testing) MaterialTheme.colors.secondary else MaterialTheme.colors.error)
}
@@ -34,7 +34,7 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser
val currServers = remember(rhId) { mutableStateOf(servers) }
val testing = rememberSaveable(rhId) { mutableStateOf(false) }
val serversUnchanged = remember(servers) { derivedStateOf { servers == currServers.value || testing.value } }
val allServersDisabled = remember { derivedStateOf { servers.none { it.enabled } } }
val allServersDisabled = remember { derivedStateOf { servers.none { it.enabled == ServerEnabled.Enabled } } }
val saveDisabled = remember(servers) {
derivedStateOf {
servers.isEmpty() ||
@@ -250,12 +250,12 @@ private fun ProtocolServerView(serverProtocol: ServerProtocol, srv: ServerCfg, s
val address = parseServerAddress(srv.server)
when {
address == null || !address.valid || address.serverProtocol != serverProtocol || !uniqueAddress(srv, address, servers) -> InvalidServer()
!srv.enabled -> Icon(painterResource(MR.images.ic_do_not_disturb_on), null, tint = MaterialTheme.colors.secondary)
srv.enabled != ServerEnabled.Enabled -> Icon(painterResource(MR.images.ic_do_not_disturb_on), null, tint = MaterialTheme.colors.secondary)
else -> ShowTestStatus(srv)
}
Spacer(Modifier.padding(horizontal = 4.dp))
val text = address?.hostnames?.firstOrNull() ?: srv.server
if (srv.enabled) {
if (srv.enabled == ServerEnabled.Enabled) {
Text(text, color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.onBackground, maxLines = 1)
} else {
Text(text, maxLines = 1, color = MaterialTheme.colors.secondary)
@@ -292,7 +292,7 @@ private fun addAllPresets(rhId: Long?, presetServers: List<String>, servers: Lis
val toAdd = ArrayList<ServerCfg>()
for (srv in presetServers) {
if (!hasPreset(srv, servers)) {
toAdd.add(ServerCfg(remoteHostId = rhId, srv, preset = true, tested = null, enabled = true))
toAdd.add(ServerCfg(remoteHostId = rhId, srv, preset = true, tested = null, enabled = ServerEnabled.Enabled))
}
}
return toAdd
@@ -319,7 +319,7 @@ private suspend fun testServers(testing: MutableState<Boolean>, servers: List<Se
private fun resetTestStatus(servers: List<ServerCfg>): List<ServerCfg> {
val copy = ArrayList(servers)
for ((index, server) in servers.withIndex()) {
if (server.enabled) {
if (server.enabled == ServerEnabled.Enabled) {
copy.removeAt(index)
copy.add(index, server.copy(tested = null))
}
@@ -331,7 +331,7 @@ private suspend fun runServersTest(servers: List<ServerCfg>, m: ChatModel, onUpd
val fs: MutableMap<String, ProtocolTestFailure> = mutableMapOf()
val updatedServers = ArrayList<ServerCfg>(servers)
for ((index, server) in servers.withIndex()) {
if (server.enabled) {
if (server.enabled == ServerEnabled.Enabled) {
interruptIfCancelled()
val (updatedServer, f) = testServerConnection(server, m)
updatedServers.removeAt(index)
@@ -7,6 +7,7 @@ import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress
import chat.simplex.common.model.ServerCfg
import chat.simplex.common.model.ServerEnabled
import chat.simplex.common.ui.theme.DEFAULT_PADDING
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.newchat.QRCodeScanner
@@ -25,7 +26,7 @@ fun ScanProtocolServerLayout(rhId: Long?, onNext: (ServerCfg) -> Unit) {
QRCodeScanner { text ->
val res = parseServerAddress(text)
if (res != null) {
onNext(ServerCfg(remoteHostId = rhId, text, false, null, true))
onNext(ServerCfg(remoteHostId = rhId, text, false, null, ServerEnabled.Enabled))
} else {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.smp_servers_invalid_address),