diff --git a/apps/android/app/src/main/cpp/simplex-api.c b/apps/android/app/src/main/cpp/simplex-api.c index edd07bd63b..9a3c9c48ab 100644 --- a/apps/android/app/src/main/cpp/simplex-api.c +++ b/apps/android/app/src/main/cpp/simplex-api.c @@ -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); diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt index dce371a351..1559002080 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt @@ -26,7 +26,7 @@ external fun pipeStdOutToSocket(socketName: String) : Int // SimpleX API typealias ChatCtrl = Long -external fun chatMigrateInit(dbPath: String, dbKey: String): Array +external fun chatMigrateInit(dbPath: String, dbKey: String, confirm: String): Array 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 = chatMigrateInit(dbAbsolutePathPrefix, dbKey) + val confirm = confirmMigrations ?: if (appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp + val migrated: Array = chatMigrateInit(dbAbsolutePathPrefix, dbKey, confirm.value) val res: DBMigrationResult = kotlin.runCatching { json.decodeFromString(migrated[0] as String) }.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 2b909e6991..56794e41b2 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -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, val agentMigrations: List): CR() @Serializable @SerialName("apiParsedMarkdown") class ParsedMarkdown(val formattedText: List? = 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) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt index 9f69ca2799..623e6049f5 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt @@ -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, @@ -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)) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DatabaseUtils.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DatabaseUtils.kt index 0409789e1f..b723c69465 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DatabaseUtils.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DatabaseUtils.kt @@ -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): MigrationError() + @Serializable @SerialName("downgrade") class Downgrade(val downMigrations: List): 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): MTRError() + @Serializable @SerialName("different") class Different(val appMigration: String, val dbMigration: String): MTRError() } \ No newline at end of file diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/DeveloperView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/DeveloperView.kt new file mode 100644 index 0000000000..204d86dc29 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/DeveloperView.kt @@ -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() + } + } +} + diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt index 27c6add259..84ece0f9da 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt @@ -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, incognitoPref: SharedPreference, - developerTools: SharedPreference, 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 = { {} }, diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 3cb0dd9a82..388c456727 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -823,6 +823,11 @@ Restore Restore database error 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. + Database upgrade + Database downgrade + Incompatible database version + Confirm database upgrades + "Invalid migration confirmation" Chat is stopped