android: support down migrations

This commit is contained in:
Evgeny Poberezkin
2023-03-27 23:16:07 +01:00
parent 1f4bb8a224
commit 31bb744ba7
8 changed files with 189 additions and 71 deletions

View File

@@ -35,7 +35,7 @@ Java_chat_simplex_app_SimplexAppKt_initHS(__unused JNIEnv *env, __unused jclass
// from simplex-chat
typedef long* chat_ctrl;
extern char *chat_migrate_init(const char *path, const char *key, chat_ctrl *ctrl);
extern char *chat_migrate_init(const char *path, const char *key, const char *confirm, chat_ctrl *ctrl);
extern char *chat_send_cmd(chat_ctrl ctrl, const char *cmd);
extern char *chat_recv_msg(chat_ctrl ctrl); // deprecated
extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait);
@@ -44,13 +44,15 @@ extern char *chat_parse_server(const char *str);
extern char *chat_password_hash(const char *pwd, const char *salt);
JNIEXPORT jobjectArray JNICALL
Java_chat_simplex_app_SimplexAppKt_chatMigrateInit(JNIEnv *env, __unused jclass clazz, jstring dbPath, jstring dbKey) {
Java_chat_simplex_app_SimplexAppKt_chatMigrateInit(JNIEnv *env, __unused jclass clazz, jstring dbPath, jstring dbKey, jstring confirm) {
const char *_dbPath = (*env)->GetStringUTFChars(env, dbPath, JNI_FALSE);
const char *_dbKey = (*env)->GetStringUTFChars(env, dbKey, JNI_FALSE);
const char *_confirm = (*env)->GetStringUTFChars(env, confirm, JNI_FALSE);
jlong _ctrl = (jlong) 0;
jstring res = (*env)->NewStringUTF(env, chat_migrate_init(_dbPath, _dbKey, &_ctrl));
jstring res = (*env)->NewStringUTF(env, chat_migrate_init(_dbPath, _dbKey, _confirm, &_ctrl));
(*env)->ReleaseStringUTFChars(env, dbPath, _dbPath);
(*env)->ReleaseStringUTFChars(env, dbKey, _dbKey);
(*env)->ReleaseStringUTFChars(env, dbKey, _confirm);
// Creating array of Object's (boxed values can be passed, eg. Long instead of long)
jobjectArray ret = (jobjectArray)(*env)->NewObjectArray(env, 2, (*env)->FindClass(env, "java/lang/Object"), NULL);

View File

@@ -26,7 +26,7 @@ external fun pipeStdOutToSocket(socketName: String) : Int
// SimpleX API
typealias ChatCtrl = Long
external fun chatMigrateInit(dbPath: String, dbKey: String): Array<Any>
external fun chatMigrateInit(dbPath: String, dbKey: String, confirm: String): Array<Any>
external fun chatSendCmd(ctrl: ChatCtrl, msg: String): String
external fun chatRecvMsg(ctrl: ChatCtrl): String
external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String
@@ -41,10 +41,11 @@ class SimplexApp: Application(), LifecycleEventObserver {
val defaultLocale: Locale = Locale.getDefault()
fun initChatController(useKey: String? = null, startChat: Boolean = true) {
fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: Boolean = true) {
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
val dbAbsolutePathPrefix = getFilesDirectory(SimplexApp.context)
val migrated: Array<Any> = chatMigrateInit(dbAbsolutePathPrefix, dbKey)
val confirm = confirmMigrations ?: if (appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
val migrated: Array<Any> = chatMigrateInit(dbAbsolutePathPrefix, dbKey, confirm.value)
val res: DBMigrationResult = kotlin.runCatching {
json.decodeFromString<DBMigrationResult>(migrated[0] as String)
}.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) }

View File

@@ -142,6 +142,7 @@ class AppPreferences(val context: Context) {
val encryptedDBPassphrase = mkStrPreference(SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE, null)
val initializationVectorDBPassphrase = mkStrPreference(SHARED_PREFS_INITIALIZATION_VECTOR_DB_PASSPHRASE, null)
val encryptionStartedAt = mkDatePreference(SHARED_PREFS_ENCRYPTION_STARTED_AT, null, true)
val confirmDBUpgrades = mkBoolPreference(SHARED_PREFS_CONFIRM_DB_UPGRADES, false)
val currentTheme = mkStrPreference(SHARED_PREFS_CURRENT_THEME, DefaultTheme.SYSTEM.name)
val primaryColor = mkIntPreference(SHARED_PREFS_PRIMARY_COLOR, LightColorPalette.primary.toArgb())
@@ -242,6 +243,7 @@ class AppPreferences(val context: Context) {
private const val SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE = "EncryptedDBPassphrase"
private const val SHARED_PREFS_INITIALIZATION_VECTOR_DB_PASSPHRASE = "InitializationVectorDBPassphrase"
private const val SHARED_PREFS_ENCRYPTION_STARTED_AT = "EncryptionStartedAt"
private const val SHARED_PREFS_CONFIRM_DB_UPGRADES = "ConfirmDBUpgrades"
private const val SHARED_PREFS_CURRENT_THEME = "CurrentTheme"
private const val SHARED_PREFS_PRIMARY_COLOR = "PrimaryColor"
private const val SHARED_PREFS_WHATS_NEW_VERSION = "WhatsNewVersion"
@@ -3024,7 +3026,7 @@ sealed class CR {
@Serializable @SerialName("callEnded") class CallEnded(val user: User, val contact: Contact): CR()
@Serializable @SerialName("newContactConnection") class NewContactConnection(val user: User, val connection: PendingContactConnection): CR()
@Serializable @SerialName("contactConnectionDeleted") class ContactConnectionDeleted(val user: User, val connection: PendingContactConnection): CR()
@Serializable @SerialName("versionInfo") class VersionInfo(val versionInfo: CoreVersionInfo): CR()
@Serializable @SerialName("versionInfo") class VersionInfo(val versionInfo: CoreVersionInfo, val chatMigrations: List<UpMigration>, val agentMigrations: List<UpMigration>): CR()
@Serializable @SerialName("apiParsedMarkdown") class ParsedMarkdown(val formattedText: List<FormattedText>? = null): CR()
@Serializable @SerialName("cmdOk") class CmdOk(val user: User?): CR()
@Serializable @SerialName("chatCmdError") class ChatCmdError(val user_: User?, val chatError: ChatError): CR()
@@ -3228,7 +3230,9 @@ sealed class CR {
is CallEnded -> withUser(user, "contact: ${contact.id}")
is NewContactConnection -> withUser(user, json.encodeToString(connection))
is ContactConnectionDeleted -> withUser(user, json.encodeToString(connection))
is VersionInfo -> json.encodeToString(versionInfo)
is VersionInfo -> "version ${json.encodeToString(versionInfo)}\n\n" +
"chat migrations: ${json.encodeToString(chatMigrations.map { it.upName })}\n\n" +
"agent migrations: ${json.encodeToString(agentMigrations.map { it.upName })}"
is CmdOk -> withUser(user, noDetails())
is ChatCmdError -> withUser(user_, chatError.string)
is ChatRespError -> withUser(user_, chatError.string)

View File

@@ -4,6 +4,7 @@ import SectionSpacer
import SectionView
import android.content.Context
import android.util.Log
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
@@ -47,33 +48,42 @@ fun DatabaseErrorView(
appPreferences.initialRandomDBPassphrase.set(false)
runChat(dbKey.value, chatDbStatus, progressIndicator, appPreferences)
}
val title = when (chatDbStatus.value) {
is DBMigrationResult.OK -> ""
is DBMigrationResult.ErrorNotADatabase -> if (useKeychain && !storedDBKey.isNullOrEmpty())
generalGetString(R.string.wrong_passphrase)
else
generalGetString(R.string.encrypted_database)
is DBMigrationResult.Error -> generalGetString(R.string.database_error)
is DBMigrationResult.ErrorKeychain -> generalGetString(R.string.keychain_error)
is DBMigrationResult.Unknown -> generalGetString(R.string.database_error)
null -> "" // should never be here
}
// val status = chatDbStatus.value
// val title = when (status) {
// is DBMigrationResult.OK -> ""
// is DBMigrationResult.ErrorNotADatabase -> if (useKeychain && !storedDBKey.isNullOrEmpty())
// generalGetString(R.string.wrong_passphrase)
// else
// generalGetString(R.string.encrypted_database)
// is DBMigrationResult.ErrorMigration -> when (status.migrationError) {
// is MigrationError.Upgrade -> generalGetString(R.string.database_upgrade)
// is MigrationError.Downgrade -> generalGetString(R.string.database_downgrade)
// is MigrationError.Error -> generalGetString(R.string.incompatible_database_version)
// }
// is DBMigrationResult.ErrorSQL -> generalGetString(R.string.database_error)
// is DBMigrationResult.ErrorKeychain -> generalGetString(R.string.keychain_error)
// this can only happen if incorrect parameter is passed
// is DBMigrationResult.InvalidConfirmation -> "Invalid migration confirmation"
// is DBMigrationResult.Unknown -> generalGetString(R.string.database_error)
// null -> "" // should never be here
// }
Column(
Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Center,
) {
Text(
title,
Modifier.padding(start = 16.dp, top = 16.dp, bottom = 24.dp),
style = MaterialTheme.typography.h1
)
SectionView(null, padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF)) {
val buttonEnabled = validKey(dbKey.value) && !progressIndicator.value
when (val status = chatDbStatus.value) {
is DBMigrationResult.ErrorNotADatabase -> {
if (useKeychain && !storedDBKey.isNullOrEmpty()) {
// Text(
// title,
// Modifier.padding(start = 16.dp, top = 16.dp, bottom = 24.dp),
// style = MaterialTheme.typography.h1
// )
// SectionView(null, padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF)) {
val buttonEnabled = validKey(dbKey.value) && !progressIndicator.value
when (val status = chatDbStatus.value) {
is DBMigrationResult.ErrorNotADatabase ->
if (useKeychain && !storedDBKey.isNullOrEmpty()) {
DatabaseErrorDetails(R.string.wrong_passphrase) {
Text(generalGetString(R.string.passphrase_is_different))
DatabaseKeyField(dbKey, buttonEnabled) {
saveAndRunChatOnClick()
@@ -81,7 +91,9 @@ fun DatabaseErrorView(
SaveAndOpenButton(buttonEnabled, saveAndRunChatOnClick)
SectionSpacer()
Text(String.format(generalGetString(R.string.file_with_path), status.dbFile))
} else {
}
} else {
DatabaseErrorDetails(R.string.encrypted_database) {
Text(generalGetString(R.string.database_passphrase_is_required))
DatabaseKeyField(dbKey, buttonEnabled) {
if (useKeychain) saveAndRunChatOnClick() else runChat(dbKey.value, chatDbStatus, progressIndicator, appPreferences)
@@ -93,36 +105,55 @@ fun DatabaseErrorView(
}
}
}
is DBMigrationResult.Error -> {
is DBMigrationResult.ErrorMigration -> when (status.migrationError) {
is MigrationError.Upgrade ->
DatabaseErrorDetails(R.string.database_upgrade) {
}
is MigrationError.Downgrade ->
DatabaseErrorDetails(R.string.database_downgrade) {
}
is MigrationError.Error ->
DatabaseErrorDetails(R.string.incompatible_database_version) {
}
}
is DBMigrationResult.ErrorSQL ->
DatabaseErrorDetails(R.string.database_error) {
Text(String.format(generalGetString(R.string.file_with_path), status.dbFile))
Text(String.format(generalGetString(R.string.error_with_info), status.migrationError))
Text(String.format(generalGetString(R.string.error_with_info), status.migrationSQLError))
}
is DBMigrationResult.ErrorKeychain -> {
is DBMigrationResult.ErrorKeychain ->
DatabaseErrorDetails(R.string.keychain_error) {
Text(generalGetString(R.string.cannot_access_keychain))
}
is DBMigrationResult.Unknown -> {
is DBMigrationResult.InvalidConfirmation ->
DatabaseErrorDetails(R.string.invalid_migration_confirmation) {
// this can only happen if incorrect parameter is passed
}
is DBMigrationResult.Unknown ->
DatabaseErrorDetails(R.string.database_error) {
Text(String.format(generalGetString(R.string.unknown_database_error_with_info), status.json))
}
is DBMigrationResult.OK -> {
}
null -> {
}
}
if (restoreDbFromBackup.value) {
SectionSpacer()
Text(generalGetString(R.string.database_backup_can_be_restored))
Spacer(Modifier.size(16.dp))
RestoreDbButton {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.restore_database_alert_title),
text = generalGetString(R.string.restore_database_alert_desc),
confirmText = generalGetString(R.string.restore_database_alert_confirm),
onConfirm = { restoreDb(restoreDbFromBackup, appPreferences, context) },
destructive = true,
)
}
is DBMigrationResult.OK -> {}
null -> {}
}
if (restoreDbFromBackup.value) {
SectionSpacer()
Text(generalGetString(R.string.database_backup_can_be_restored))
Spacer(Modifier.size(16.dp))
RestoreDbButton {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.restore_database_alert_title),
text = generalGetString(R.string.restore_database_alert_desc),
confirmText = generalGetString(R.string.restore_database_alert_confirm),
onConfirm = { restoreDb(restoreDbFromBackup, appPreferences, context) },
destructive = true,
)
}
}
// }
}
if (progressIndicator.value) {
Box(
@@ -140,6 +171,16 @@ fun DatabaseErrorView(
}
}
@Composable
private fun DatabaseErrorDetails(@StringRes title: Int, content: @Composable ColumnScope.() -> Unit) {
Text(
generalGetString(title),
Modifier.padding(start = 16.dp, top = 16.dp, bottom = 24.dp),
style = MaterialTheme.typography.h1
)
SectionView(null, padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), content)
}
private fun runChat(
dbKey: String,
chatDbStatus: State<DBMigrationResult?>,
@@ -166,8 +207,8 @@ private fun runChat(
is DBMigrationResult.ErrorNotADatabase -> {
AlertManager.shared.showAlertMsg(generalGetString(R.string.wrong_passphrase_title), generalGetString(R.string.enter_correct_passphrase))
}
is DBMigrationResult.Error -> {
AlertManager.shared.showAlertMsg(generalGetString(R.string.database_error), status.migrationError)
is DBMigrationResult.ErrorSQL -> {
AlertManager.shared.showAlertMsg(generalGetString(R.string.database_error), status.migrationSQLError)
}
is DBMigrationResult.ErrorKeychain -> {
AlertManager.shared.showAlertMsg(generalGetString(R.string.keychain_error))

View File

@@ -66,8 +66,39 @@ object DatabaseUtils {
@Serializable
sealed class DBMigrationResult {
@Serializable @SerialName("ok") object OK: DBMigrationResult()
@Serializable @SerialName("invalidConfirmation") object InvalidConfirmation: DBMigrationResult()
@Serializable @SerialName("errorNotADatabase") class ErrorNotADatabase(val dbFile: String): DBMigrationResult()
@Serializable @SerialName("error") class Error(val dbFile: String, val migrationError: String): DBMigrationResult()
@Serializable @SerialName("errorMigration") class ErrorMigration(val dbFile: String, val migrationError: MigrationError): DBMigrationResult()
@Serializable @SerialName("errorSQL") class ErrorSQL(val dbFile: String, val migrationSQLError: String): DBMigrationResult()
@Serializable @SerialName("errorKeychain") object ErrorKeychain: DBMigrationResult()
@Serializable @SerialName("unknown") class Unknown(val json: String): DBMigrationResult()
}
enum class MigrationConfirmation(val value: String) {
YesUp("yesUp"),
YesUpDown ("yesUpDown"),
Error("error")
}
fun defaultMigrationConfirmation(appPrefs: AppPreferences): MigrationConfirmation =
if (appPrefs.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
@Serializable
sealed class MigrationError {
@Serializable @SerialName("upgrade") class Upgrade(val upMigrations: List<UpMigration>): MigrationError()
@Serializable @SerialName("downgrade") class Downgrade(val downMigrations: List<String>): MigrationError()
@Serializable @SerialName("migrationError") class Error(val mtrError: MTRError): MigrationError()
}
@Serializable
data class UpMigration(
val upName: String,
// val withDown: Boolean
)
@Serializable
sealed class MTRError {
@Serializable @SerialName("noDown") class NoDown(val dbMigrations: List<String>): MTRError()
@Serializable @SerialName("different") class Different(val appMigration: String, val dbMigration: String): MTRError()
}

View File

@@ -0,0 +1,47 @@
package chat.simplex.app.views.usersettings
import SectionDivider
import SectionView
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.views.TerminalView
import chat.simplex.app.views.helpers.AppBarTitle
@Composable
fun DeveloperView(
m: ChatModel,
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
withAuth: (block: () -> Unit) -> Unit
) {
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start
) {
val developerTools = m.controller.appPrefs.developerTools
val confirmDBUpgrades = m.controller.appPrefs.confirmDBUpgrades
val uriHandler = LocalUriHandler.current
AppBarTitle(stringResource(R.string.settings_developer_tools))
SectionView() {
ChatConsoleItem { withAuth(showCustomModal { it, close -> TerminalView(it, close) }) }
SectionDivider()
val devTools = remember { mutableStateOf(developerTools.get()) }
SettingsPreferenceItem(Icons.Outlined.Construction, stringResource(R.string.settings_developer_tools), developerTools, devTools)
SectionDivider()
var confirm = remember { mutableStateOf(confirmDBUpgrades.get()) }
SettingsPreferenceItem(Icons.Outlined.DriveFolderUpload, stringResource(R.string.confirm_database_upgrades), confirmDBUpgrades, confirm)
SectionDivider()
InstallTerminalAppItem(uriHandler)
SectionDivider()
}
}
}

View File

@@ -57,7 +57,6 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
chatModel.chatDbEncrypted.value == true,
chatModel.incognito,
chatModel.controller.appPrefs.incognito,
developerTools = chatModel.controller.appPrefs.developerTools,
user.displayName,
setPerformLA = setPerformLA,
showModal = { modalView -> { ModalManager.shared.showModal { modalView(chatModel) } } },
@@ -126,7 +125,6 @@ fun SettingsLayout(
encrypted: Boolean,
incognito: MutableState<Boolean>,
incognitoPref: SharedPreference<Boolean>,
developerTools: SharedPreference<Boolean>,
userDisplayName: String,
setPerformLA: (Boolean) -> Unit,
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
@@ -207,17 +205,7 @@ fun SettingsLayout(
SectionSpacer()
SectionView(stringResource(R.string.settings_section_title_develop)) {
val devTools = remember { mutableStateOf(developerTools.get()) }
SettingsPreferenceItem(Icons.Outlined.Construction, stringResource(R.string.settings_developer_tools), developerTools, devTools)
SectionDivider()
if (devTools.value) {
ChatConsoleItem { withAuth(showCustomModal { it, close -> TerminalView(it, close) }) }
SectionDivider()
InstallTerminalAppItem(uriHandler)
SectionDivider()
}
// SettingsActionItem(Icons.Outlined.Science, stringResource(R.string.settings_experimental_features), showSettingsModal { ExperimentalFeaturesView(it, enableCalls) })
// SectionDivider()
SettingsActionItem(Icons.Outlined.Construction, stringResource(R.string.settings_developer_tools), showSettingsModal { DeveloperView(it, showCustomModal, withAuth) })
AppVersionItem(showVersion)
}
}
@@ -370,7 +358,7 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
}
}
@Composable private fun ChatConsoleItem(showTerminal: () -> Unit) {
@Composable fun ChatConsoleItem(showTerminal: () -> Unit) {
SectionItemView(showTerminal) {
Icon(
painter = painterResource(id = R.drawable.ic_outline_terminal),
@@ -382,7 +370,7 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
}
}
@Composable private fun InstallTerminalAppItem(uriHandler: UriHandler) {
@Composable fun InstallTerminalAppItem(uriHandler: UriHandler) {
SectionItemView({ uriHandler.openUriCatching("https://github.com/simplex-chat/simplex-chat") }) {
Icon(
painter = painterResource(id = R.drawable.ic_github),
@@ -544,7 +532,6 @@ fun PreviewSettingsLayout() {
encrypted = false,
incognito = remember { mutableStateOf(false) },
incognitoPref = SharedPreference({ false }, {}),
developerTools = SharedPreference({ false }, {}),
userDisplayName = "Alice",
setPerformLA = {},
showModal = { {} },

View File

@@ -823,6 +823,11 @@
<string name="restore_database_alert_confirm">Restore</string>
<string name="database_restore_error">Restore database error</string>
<string name="restore_passphrase_not_found_desc">Passphrase not found in Keystore, please enter it manually. This may have happened if you restored the app\'s data using a backup tool. If it\'s not the case, please, contact developers.</string>
<string name="database_upgrade">Database upgrade</string>
<string name="database_downgrade">Database downgrade</string>
<string name="incompatible_database_version">Incompatible database version</string>
<string name="confirm_database_upgrades">Confirm database upgrades</string>
<string name="invalid_migration_confirmation">"Invalid migration confirmation"</string>
<!-- ChatModel.chatRunning interactions -->
<string name="chat_is_stopped_indication">Chat is stopped</string>