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 5d8f09c4ae..04f721ec3a 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()) @@ -244,6 +245,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" @@ -3055,7 +3057,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() @@ -3259,7 +3261,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..05a8288ada 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 @@ -13,6 +14,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import chat.simplex.app.* @@ -20,6 +22,7 @@ import chat.simplex.app.R import chat.simplex.app.model.AppPreferences import chat.simplex.app.ui.theme.* import chat.simplex.app.views.helpers.* +import chat.simplex.app.views.usersettings.AppVersionText import chat.simplex.app.views.usersettings.NotificationsMode import kotlinx.coroutines.* import kotlinx.datetime.Clock @@ -39,24 +42,39 @@ fun DatabaseErrorView( var useKeychain by remember { mutableStateOf(appPreferences.storeDBPassphrase.get()) } val context = LocalContext.current val restoreDbFromBackup = remember { mutableStateOf(shouldShowRestoreDbButton(appPreferences, context)) } - val saveAndRunChatOnClick: () -> Unit = { + + fun callRunChat(confirmMigrations: MigrationConfirmation? = null) { + val useKey = if (useKeychain) null else dbKey.value + runChat(useKey, confirmMigrations, chatDbStatus, progressIndicator, appPreferences) + } + + fun saveAndRunChatOnClick() { DatabaseUtils.setDatabaseKey(dbKey.value) storedDBKey = dbKey.value appPreferences.storeDBPassphrase.set(true) useKeychain = true appPreferences.initialRandomDBPassphrase.set(false) - runChat(dbKey.value, chatDbStatus, progressIndicator, appPreferences) + callRunChat() } - 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 + + @Composable + fun DatabaseErrorDetails(@StringRes title: Int, content: @Composable ColumnScope.() -> Unit) { + Text( + generalGetString(title), + Modifier.padding(start = 16.dp, top = 16.dp, bottom = 16.dp), + style = MaterialTheme.typography.h1 + ) + SectionView(null, padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), content) + } + + @Composable + fun FileNameText(dbFile: String) { + Text(String.format(generalGetString(R.string.file_with_path), dbFile.split("/").lastOrNull() ?: dbFile)) + } + + @Composable + fun MigrationsText(ms: List) { + Text(String.format(generalGetString(R.string.database_migrations), ms.joinToString(", "))) } Column( @@ -64,63 +82,91 @@ fun DatabaseErrorView( 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()) { + 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() } - SaveAndOpenButton(buttonEnabled, saveAndRunChatOnClick) + SaveAndOpenButton(buttonEnabled, ::saveAndRunChatOnClick) SectionSpacer() - Text(String.format(generalGetString(R.string.file_with_path), status.dbFile)) - } else { + FileNameText(status.dbFile) + } + } 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) - } if (useKeychain) { - SaveAndOpenButton(buttonEnabled, saveAndRunChatOnClick) + DatabaseKeyField(dbKey, buttonEnabled, ::saveAndRunChatOnClick) + SaveAndOpenButton(buttonEnabled, ::saveAndRunChatOnClick) } else { - OpenChatButton(buttonEnabled) { runChat(dbKey.value, chatDbStatus, progressIndicator, appPreferences) } + DatabaseKeyField(dbKey, buttonEnabled) { callRunChat() } + OpenChatButton(buttonEnabled) { callRunChat() } } } } - is DBMigrationResult.Error -> { - Text(String.format(generalGetString(R.string.file_with_path), status.dbFile)) - Text(String.format(generalGetString(R.string.error_with_info), status.migrationError)) + is DBMigrationResult.ErrorMigration -> when (val err = status.migrationError) { + is MigrationError.Upgrade -> + DatabaseErrorDetails(R.string.database_upgrade) { + TextButton({ callRunChat(confirmMigrations = MigrationConfirmation.YesUp) }, Modifier.align(Alignment.CenterHorizontally), enabled = !progressIndicator.value) { + Text(generalGetString(R.string.upgrade_and_open_chat)) + } + Spacer(Modifier.height(20.dp)) + FileNameText(status.dbFile) + MigrationsText(err.upMigrations.map { it.upName }) + AppVersionText() + } + is MigrationError.Downgrade -> + DatabaseErrorDetails(R.string.database_downgrade) { + TextButton({ callRunChat(confirmMigrations = MigrationConfirmation.YesUpDown) }, Modifier.align(Alignment.CenterHorizontally), enabled = !progressIndicator.value) { + Text(generalGetString(R.string.downgrade_and_open_chat)) + } + Spacer(Modifier.height(20.dp)) + Text(generalGetString(R.string.database_downgrade_warning), fontWeight = FontWeight.Bold) + FileNameText(status.dbFile) + MigrationsText(err.downMigrations) + AppVersionText() + } + is MigrationError.Error -> + DatabaseErrorDetails(R.string.incompatible_database_version) { + FileNameText(status.dbFile) + Text(String.format(generalGetString(R.string.error_with_info), mtrErrorDescription(err.mtrError))) + } + } + is DBMigrationResult.ErrorSQL -> + DatabaseErrorDetails(R.string.database_error) { + FileNameText(status.dbFile) + 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, + ) } } } @@ -141,7 +187,8 @@ fun DatabaseErrorView( } private fun runChat( - dbKey: String, + dbKey: String? = null, + confirmMigrations: MigrationConfirmation? = null, chatDbStatus: State, progressIndicator: MutableState, prefs: AppPreferences @@ -150,7 +197,7 @@ private fun runChat( if (progressIndicator.value) return@launch progressIndicator.value = true try { - SimplexApp.context.initChatController(dbKey) + SimplexApp.context.initChatController(dbKey, confirmMigrations) } catch (e: Exception) { Log.d(TAG, "initializeChat ${e.stackTraceToString()}") } @@ -163,18 +210,17 @@ private fun runChat( NotificationsMode.PERIODIC.name -> SimplexApp.context.schedulePeriodicWakeUp() } } - is DBMigrationResult.ErrorNotADatabase -> { + 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.ErrorKeychain -> { + is DBMigrationResult.ErrorSQL -> + AlertManager.shared.showAlertMsg(generalGetString(R.string.database_error), status.migrationSQLError) + is DBMigrationResult.ErrorKeychain -> AlertManager.shared.showAlertMsg(generalGetString(R.string.keychain_error)) - } - is DBMigrationResult.Unknown -> { + is DBMigrationResult.Unknown -> AlertManager.shared.showAlertMsg(generalGetString(R.string.unknown_error), status.json) - } + is DBMigrationResult.InvalidConfirmation -> + AlertManager.shared.showAlertMsg(generalGetString(R.string.invalid_migration_confirmation)) + is DBMigrationResult.ErrorMigration -> {} null -> {} } } @@ -204,6 +250,14 @@ private fun restoreDb(restoreDbFromBackup: MutableState, prefs: AppPref } } +private fun mtrErrorDescription(err: MTRError): String = + when (err) { + is MTRError.NoDown -> + String.format(generalGetString(R.string.mtr_error_no_down_migration), err.dbMigrations.joinToString(", ")) + is MTRError.Different -> + String.format(generalGetString(R.string.mtr_error_different), err.appMigration, err.dbMigration) + } + @Composable private fun DatabaseKeyField(text: MutableState, enabled: Boolean, onClick: (() -> Unit)? = null) { PassphraseField( 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..e0fd6d276f --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/DeveloperView.kt @@ -0,0 +1,43 @@ +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()) { + 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) + } + } +} + 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 22774a0ab9..f55eb941ba 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,15 +205,8 @@ 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) + SettingsActionItem(Icons.Outlined.Construction, stringResource(R.string.settings_developer_tools), showSettingsModal { DeveloperView(it, showCustomModal, withAuth) }) 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) }) SectionDivider() AppVersionItem(showVersion) @@ -370,7 +361,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 +373,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), @@ -395,9 +386,11 @@ fun MaintainIncognitoState(chatModel: ChatModel) { } @Composable private fun AppVersionItem(showVersion: () -> Unit) { - SectionItemView(showVersion) { - Text("v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})") - } + SectionItemView(showVersion) { AppVersionText() } +} + +@Composable fun AppVersionText() { + Text("v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})") } @Composable fun ProfilePreview(profileOf: NamedChat, size: Dp = 60.dp, color: Color = MaterialTheme.colors.secondary, stopped: Boolean = false) { @@ -544,7 +537,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 788eccd87f..2f8206c58a 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -824,6 +824,17 @@ 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 + Upgrade and open chat + Downgrade and open chat + database version is newer than the app, but no down migration for: %s + different migration in the app/database: %s / %s + Migrations: %s + Warning: you may lose some data! Chat is stopped