android, desktop: handle situation when not all databases created before shut down (#4294)

* android, desktop: handle situation when not all databases created before shut down

* more logic of choosing whether to delete databases or not

* comment

* rename

* refactoring
This commit is contained in:
Stanislav Dmitrenko
2024-07-02 21:02:36 +07:00
committed by GitHub
parent ddeaa1c7c3
commit 0d3928bd51
6 changed files with 68 additions and 11 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"
@@ -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)