android, desktop: migration enhancements (#3901)

* adapted iOS logic

* rename everything

* title

* title

* text

* rename

* rename

* alert on strange error
This commit is contained in:
Stanislav Dmitrenko
2024-03-13 01:25:02 +07:00
committed by GitHub
parent 7fa2f2f72e
commit e75be71d9a
8 changed files with 392 additions and 339 deletions
@@ -13,7 +13,8 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.*
import chat.simplex.common.views.chat.ComposeState
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.migration.MigrationFromAnotherDeviceState
import chat.simplex.common.views.migration.MigrationToDeviceState
import chat.simplex.common.views.migration.MigrationToState
import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource
@@ -105,7 +106,7 @@ object ChatModel {
// currently showing invitation
val showingInvitation = mutableStateOf(null as ShowingInvitation?)
val migrationState: MutableState<MigrationFromAnotherDeviceState?> by lazy { mutableStateOf(MigrationFromAnotherDeviceState.transform()) }
val migrationState: MutableState<MigrationToState?> by lazy { mutableStateOf(MigrationToDeviceState.makeMigrationState()) }
var draft = mutableStateOf(null as ComposeState?)
var draftChatId = mutableStateOf(null as String?)
@@ -147,7 +147,8 @@ class AppPreferences {
val appLanguage = mkStrPreference(SHARED_PREFS_APP_LANGUAGE, null)
val onboardingStage = mkEnumPreference(SHARED_PREFS_ONBOARDING_STAGE, OnboardingStage.OnboardingComplete) { OnboardingStage.values().firstOrNull { it.name == this } }
val migrationStage = mkStrPreference(SHARED_PREFS_MIGRATION_STAGE, null)
val migrationToStage = mkStrPreference(SHARED_PREFS_MIGRATION_TO_STAGE, null)
val migrationFromStage = mkStrPreference(SHARED_PREFS_MIGRATION_FROM_STAGE, null)
val storeDBPassphrase = mkBoolPreference(SHARED_PREFS_STORE_DB_PASSPHRASE, true)
val initialRandomDBPassphrase = mkBoolPreference(SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE, false)
val encryptedDBPassphrase = mkStrPreference(SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE, null)
@@ -286,7 +287,8 @@ class AppPreferences {
private const val SHARED_PREFS_CHAT_ARCHIVE_TIME = "ChatArchiveTime"
private const val SHARED_PREFS_APP_LANGUAGE = "AppLanguage"
private const val SHARED_PREFS_ONBOARDING_STAGE = "OnboardingStage"
const val SHARED_PREFS_MIGRATION_STAGE = "MigrationStage"
const val SHARED_PREFS_MIGRATION_TO_STAGE = "MigrationToStage"
const val SHARED_PREFS_MIGRATION_FROM_STAGE = "MigrationFromStage"
private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart"
private const val SHARED_PREFS_CHAT_STOPPED = "ChatStopped"
private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools"
@@ -704,9 +706,11 @@ object ChatController {
throw Exception("failed to set storage encryption: ${r.responseType} ${r.details}")
}
suspend fun testStorageEncryption(key: String, ctrl: ChatCtrl? = null): Boolean {
suspend fun testStorageEncryption(key: String, ctrl: ChatCtrl? = null): CR.ChatCmdError? {
val r = sendCmd(null, CC.TestStorageEncryption(key), ctrl)
return r is CR.CmdOk
if (r is CR.CmdOk) return null
else if (r is CR.ChatCmdError) return r
throw Exception("failed to test storage encryption: ${r.responseType} ${r.details}")
}
suspend fun apiGetChats(rh: Long?): List<Chat> {
@@ -152,6 +152,10 @@ fun chatInitTemporaryDatabase(dbPath: String, key: String? = null, confirmation:
fun chatInitControllerRemovingDatabases() {
val dbPath = dbAbsolutePrefixPath
// Remove previous databases, otherwise, can be .errorNotADatabase with null controller
File(dbPath + "_chat.db").delete()
File(dbPath + "_agent.db").delete()
val dbKey = randomDatabasePassword()
Log.d(TAG, "chatInitControllerRemovingDatabases path: $dbPath")
val migrated = chatMigrateInit(dbPath, dbKey, MigrationConfirmation.Error.value)
@@ -79,39 +79,49 @@ data class MigrationFileLinkData(
@Serializable
private sealed class MigrationToState {
@Serializable object ChatStopInProgress: MigrationToState()
@Serializable data class ChatStopFailed(val reason: String): MigrationToState()
@Serializable object PassphraseNotSet: MigrationToState()
@Serializable object PassphraseConfirmation: MigrationToState()
@Serializable object UploadConfirmation: MigrationToState()
@Serializable object Archiving: MigrationToState()
@Serializable data class DatabaseInit(val totalBytes: Long, val archivePath: String): MigrationToState()
@Serializable data class UploadProgress(val uploadedBytes: Long, val totalBytes: Long, val fileId: Long, val archivePath: String, val ctrl: ChatCtrl, val user: User): MigrationToState()
@Serializable data class UploadFailed(val totalBytes: Long, val archivePath: String): MigrationToState()
@Serializable object LinkCreation: MigrationToState()
@Serializable data class LinkShown(val fileId: Long, val link: String, val ctrl: ChatCtrl): MigrationToState()
@Serializable data class Finished(val chatDeletion: Boolean): MigrationToState()
private sealed class MigrationFromState {
@Serializable object ChatStopInProgress: MigrationFromState()
@Serializable data class ChatStopFailed(val reason: String): MigrationFromState()
@Serializable object PassphraseNotSet: MigrationFromState()
@Serializable object PassphraseConfirmation: MigrationFromState()
@Serializable object UploadConfirmation: MigrationFromState()
@Serializable object Archiving: MigrationFromState()
@Serializable data class DatabaseInit(val totalBytes: Long, val archivePath: String): MigrationFromState()
@Serializable data class UploadProgress(val uploadedBytes: Long, val totalBytes: Long, val fileId: Long, val archivePath: String, val ctrl: ChatCtrl, val user: User): MigrationFromState()
@Serializable data class UploadFailed(val totalBytes: Long, val archivePath: String): MigrationFromState()
@Serializable object LinkCreation: MigrationFromState()
@Serializable data class LinkShown(val fileId: Long, val link: String, val ctrl: ChatCtrl): MigrationFromState()
@Serializable data class Finished(val chatDeletion: Boolean): MigrationFromState()
}
private var MutableState<MigrationToState>.state: MigrationToState
private var MutableState<MigrationFromState>.state: MigrationFromState
get() = value
set(v) { value = v }
@Composable
fun MigrateToAnotherDeviceView(close: () -> Unit) {
val migrationState = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf<MigrationToState>(MigrationToState.ChatStopInProgress) }
fun MigrateFromDeviceView(close: () -> Unit) {
val migrationState = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf<MigrationFromState>(MigrationFromState.ChatStopInProgress) }
// Prevent from hiding the view until migration is finished or app deleted
val backDisabled = remember {
derivedStateOf {
migrationState.value is MigrationToState.DatabaseInit ||
migrationState.value is MigrationToState.Archiving ||
migrationState.value is MigrationToState.LinkCreation ||
migrationState.value is MigrationToState.LinkShown ||
migrationState.value is MigrationToState.Finished
when (migrationState.value) {
is MigrationFromState.ChatStopInProgress,
is MigrationFromState.DatabaseInit,
is MigrationFromState.Archiving,
is MigrationFromState.LinkShown,
is MigrationFromState.Finished -> true
is MigrationFromState.ChatStopFailed,
is MigrationFromState.PassphraseNotSet,
is MigrationFromState.PassphraseConfirmation,
is MigrationFromState.UploadConfirmation,
is MigrationFromState.UploadProgress,
is MigrationFromState.UploadFailed,
is MigrationFromState.LinkCreation -> false
}
}
}
val chatReceiver = remember { mutableStateOf(null as MigrationToChatReceiver?) }
val chatReceiver = remember { mutableStateOf(null as MigrationFromChatReceiver?) }
ModalView(
enableClose = !backDisabled.value,
close = {
@@ -121,7 +131,7 @@ fun MigrateToAnotherDeviceView(close: () -> Unit) {
close()
},
) {
MigrateToAnotherDeviceLayout(
MigrateFromDeviceLayout(
migrationState = migrationState,
chatReceiver = chatReceiver
)
@@ -129,16 +139,16 @@ fun MigrateToAnotherDeviceView(close: () -> Unit) {
}
@Composable
private fun MigrateToAnotherDeviceLayout(
migrationState: MutableState<MigrationToState>,
chatReceiver: MutableState<MigrationToChatReceiver?>
private fun MigrateFromDeviceLayout(
migrationState: MutableState<MigrationFromState>,
chatReceiver: MutableState<MigrationFromChatReceiver?>
) {
val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) }
Column(
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).height(IntrinsicSize.Max),
) {
AppBarTitle(stringResource(MR.strings.migrate_to_device))
AppBarTitle(stringResource(MR.strings.migrate_from_device_title))
SectionByState(migrationState, tempDatabaseFile.value, chatReceiver)
SectionBottomSpacer()
}
@@ -147,30 +157,30 @@ private fun MigrateToAnotherDeviceLayout(
@Composable
private fun SectionByState(
migrationState: MutableState<MigrationToState>,
migrationState: MutableState<MigrationFromState>,
tempDatabaseFile: File,
chatReceiver: MutableState<MigrationToChatReceiver?>
chatReceiver: MutableState<MigrationFromChatReceiver?>
) {
when (val s = migrationState.value) {
is MigrationToState.ChatStopInProgress -> migrationState.ChatStopInProgressView()
is MigrationToState.ChatStopFailed -> migrationState.ChatStopFailedView(s.reason)
is MigrationToState.PassphraseNotSet -> migrationState.PassphraseNotSetView()
is MigrationToState.PassphraseConfirmation -> migrationState.PassphraseConfirmationView()
is MigrationToState.UploadConfirmation -> migrationState.UploadConfirmationView()
is MigrationToState.Archiving -> migrationState.ArchivingView()
is MigrationToState.DatabaseInit -> migrationState.DatabaseInitView(tempDatabaseFile, s.totalBytes, s.archivePath)
is MigrationToState.UploadProgress -> migrationState.UploadProgressView(s.uploadedBytes, s.totalBytes, s.ctrl, s.user, tempDatabaseFile, chatReceiver, s.archivePath)
is MigrationToState.UploadFailed -> migrationState.UploadFailedView(s.totalBytes, s.archivePath, chatReceiver.value)
is MigrationToState.LinkCreation -> LinkCreationView()
is MigrationToState.LinkShown -> migrationState.LinkShownView(s.fileId, s.link, s.ctrl)
is MigrationToState.Finished -> migrationState.FinishedView(s.chatDeletion)
is MigrationFromState.ChatStopInProgress -> migrationState.ChatStopInProgressView()
is MigrationFromState.ChatStopFailed -> migrationState.ChatStopFailedView(s.reason)
is MigrationFromState.PassphraseNotSet -> migrationState.PassphraseNotSetView()
is MigrationFromState.PassphraseConfirmation -> migrationState.PassphraseConfirmationView()
is MigrationFromState.UploadConfirmation -> migrationState.UploadConfirmationView()
is MigrationFromState.Archiving -> migrationState.ArchivingView()
is MigrationFromState.DatabaseInit -> migrationState.DatabaseInitView(tempDatabaseFile, s.totalBytes, s.archivePath)
is MigrationFromState.UploadProgress -> migrationState.UploadProgressView(s.uploadedBytes, s.totalBytes, s.ctrl, s.user, tempDatabaseFile, chatReceiver, s.archivePath)
is MigrationFromState.UploadFailed -> migrationState.UploadFailedView(s.totalBytes, s.archivePath, chatReceiver.value)
is MigrationFromState.LinkCreation -> LinkCreationView()
is MigrationFromState.LinkShown -> migrationState.LinkShownView(s.fileId, s.link, s.ctrl)
is MigrationFromState.Finished -> migrationState.FinishedView(s.chatDeletion)
}
}
@Composable
private fun MutableState<MigrationToState>.ChatStopInProgressView() {
private fun MutableState<MigrationFromState>.ChatStopInProgressView() {
Box {
SectionView(stringResource(MR.strings.migration_to_device_stopping_chat).uppercase()) {}
SectionView(stringResource(MR.strings.migrate_from_device_stopping_chat).uppercase()) {}
ProgressView()
}
LaunchedEffect(Unit) {
@@ -179,7 +189,7 @@ private fun MutableState<MigrationToState>.ChatStopInProgressView() {
}
@Composable
private fun MutableState<MigrationToState>.ChatStopFailedView(reason: String) {
private fun MutableState<MigrationFromState>.ChatStopFailedView(reason: String) {
SectionView(stringResource(MR.strings.error_stopping_chat).uppercase()) {
Text(reason)
SectionSpacer()
@@ -189,22 +199,22 @@ private fun MutableState<MigrationToState>.ChatStopFailedView(reason: String) {
textColor = MaterialTheme.colors.error,
click = ::stopChat
){}
SectionTextFooter(stringResource(MR.strings.migration_to_device_chat_should_be_stopped))
SectionTextFooter(stringResource(MR.strings.migrate_from_device_chat_should_be_stopped))
}
}
@Composable
private fun MutableState<MigrationToState>.PassphraseNotSetView() {
private fun MutableState<MigrationFromState>.PassphraseNotSetView() {
DatabaseEncryptionView(chatModel, true)
KeyChangeEffect(appPreferences.initialRandomDBPassphrase.state.value) {
if (!appPreferences.initialRandomDBPassphrase.get()) {
state = MigrationToState.UploadConfirmation
state = MigrationFromState.UploadConfirmation
}
}
}
@Composable
private fun MutableState<MigrationToState>.PassphraseConfirmationView() {
private fun MutableState<MigrationFromState>.PassphraseConfirmationView() {
val useKeychain = remember { appPreferences.storeDBPassphrase.get() }
val currentKey = rememberSaveable { mutableStateOf("") }
val verifyingPassphrase = rememberSaveable { mutableStateOf(false) }
@@ -214,12 +224,12 @@ private fun MutableState<MigrationToState>.PassphraseConfirmationView() {
ChatStoppedView()
SectionSpacer()
SectionView(stringResource(MR.strings.migration_to_device_verify_database_passphrase).uppercase()) {
SectionView(stringResource(MR.strings.migrate_from_device_verify_database_passphrase).uppercase()) {
PassphraseField(currentKey, placeholder = stringResource(MR.strings.current_passphrase), Modifier.padding(horizontal = DEFAULT_PADDING), isValid = ::validKey, requestFocus = true)
SettingsActionItemWithContent(
icon = painterResource(if (useKeychain) MR.images.ic_vpn_key_filled else MR.images.ic_lock),
text = stringResource(MR.strings.migration_to_device_verify_passphrase),
text = stringResource(MR.strings.migrate_from_device_verify_passphrase),
textColor = MaterialTheme.colors.primary,
disabled = verifyingPassphrase.value || currentKey.value.isEmpty(),
click = {
@@ -231,7 +241,7 @@ private fun MutableState<MigrationToState>.PassphraseConfirmationView() {
}
}
) {}
SectionTextFooter(stringResource(MR.strings.migration_to_device_confirm_you_remember_passphrase))
SectionTextFooter(stringResource(MR.strings.migrate_from_device_confirm_you_remember_passphrase))
}
}
if (verifyingPassphrase.value) {
@@ -241,22 +251,22 @@ private fun MutableState<MigrationToState>.PassphraseConfirmationView() {
}
@Composable
private fun MutableState<MigrationToState>.UploadConfirmationView() {
SectionView(stringResource(MR.strings.migration_to_device_confirm_upload).uppercase()) {
private fun MutableState<MigrationFromState>.UploadConfirmationView() {
SectionView(stringResource(MR.strings.migrate_from_device_confirm_upload).uppercase()) {
SettingsActionItemWithContent(
icon = painterResource(MR.images.ic_ios_share),
text = stringResource(MR.strings.migration_to_device_archive_and_upload),
text = stringResource(MR.strings.migrate_from_device_archive_and_upload),
textColor = MaterialTheme.colors.primary,
click = { state = MigrationToState.Archiving }
click = { state = MigrationFromState.Archiving }
){}
SectionTextFooter(stringResource(MR.strings.migration_to_device_all_data_will_be_uploaded))
SectionTextFooter(stringResource(MR.strings.migrate_from_device_all_data_will_be_uploaded))
}
}
@Composable
private fun MutableState<MigrationToState>.ArchivingView() {
private fun MutableState<MigrationFromState>.ArchivingView() {
Box {
SectionView(stringResource(MR.strings.migration_to_device_archiving_database).uppercase()) {}
SectionView(stringResource(MR.strings.migrate_from_device_archiving_database).uppercase()) {}
ProgressView()
}
LaunchedEffect(Unit) {
@@ -265,9 +275,9 @@ private fun MutableState<MigrationToState>.ArchivingView() {
}
@Composable
private fun MutableState<MigrationToState>.DatabaseInitView(tempDatabaseFile: File, totalBytes: Long, archivePath: String) {
private fun MutableState<MigrationFromState>.DatabaseInitView(tempDatabaseFile: File, totalBytes: Long, archivePath: String) {
Box {
SectionView(stringResource(MR.strings.migration_to_device_database_init).uppercase()) {}
SectionView(stringResource(MR.strings.migrate_from_device_database_init).uppercase()) {}
ProgressView()
}
LaunchedEffect(Unit) {
@@ -276,19 +286,19 @@ private fun MutableState<MigrationToState>.DatabaseInitView(tempDatabaseFile: Fi
}
@Composable
private fun MutableState<MigrationToState>.UploadProgressView(
private fun MutableState<MigrationFromState>.UploadProgressView(
uploadedBytes: Long,
totalBytes: Long,
ctrl: ChatCtrl,
user: User,
tempDatabaseFile: File,
chatReceiver: MutableState<MigrationToChatReceiver?>,
chatReceiver: MutableState<MigrationFromChatReceiver?>,
archivePath: String,
) {
Box {
SectionView(stringResource(MR.strings.migration_to_device_uploading_archive).uppercase()) {
SectionView(stringResource(MR.strings.migrate_from_device_uploading_archive).uppercase()) {
val ratio = uploadedBytes.toFloat() / max(totalBytes, 1)
LargeProgressView(ratio, "${(ratio * 100).toInt()}%", stringResource(MR.strings.migration_to_device_bytes_uploaded).format(formatBytes(uploadedBytes)))
LargeProgressView(ratio, "${(ratio * 100).toInt()}%", stringResource(MR.strings.migrate_from_device_bytes_uploaded).format(formatBytes(uploadedBytes)))
}
}
LaunchedEffect(Unit) {
@@ -297,17 +307,17 @@ private fun MutableState<MigrationToState>.UploadProgressView(
}
@Composable
private fun MutableState<MigrationToState>.UploadFailedView(totalBytes: Long, archivePath: String, chatReceiver: MigrationToChatReceiver?) {
SectionView(stringResource(MR.strings.migration_to_device_upload_failed).uppercase()) {
private fun MutableState<MigrationFromState>.UploadFailedView(totalBytes: Long, archivePath: String, chatReceiver: MigrationFromChatReceiver?) {
SectionView(stringResource(MR.strings.migrate_from_device_upload_failed).uppercase()) {
SettingsActionItemWithContent(
icon = painterResource(MR.images.ic_ios_share),
text = stringResource(MR.strings.migration_to_device_repeat_upload),
text = stringResource(MR.strings.migrate_from_device_repeat_upload),
textColor = MaterialTheme.colors.primary,
click = {
state = MigrationToState.DatabaseInit(totalBytes, archivePath)
state = MigrationFromState.DatabaseInit(totalBytes, archivePath)
}
) {}
SectionTextFooter(stringResource(MR.strings.migration_to_device_try_again))
SectionTextFooter(stringResource(MR.strings.migrate_from_device_try_again))
}
LaunchedEffect(Unit) {
chatReceiver?.stopAndCleanUp()
@@ -317,17 +327,17 @@ private fun MutableState<MigrationToState>.UploadFailedView(totalBytes: Long, ar
@Composable
private fun LinkCreationView() {
Box {
SectionView(stringResource(MR.strings.migration_to_device_creating_archive_link).uppercase()) {}
SectionView(stringResource(MR.strings.migrate_from_device_creating_archive_link).uppercase()) {}
ProgressView()
}
}
@Composable
private fun MutableState<MigrationToState>.LinkShownView(fileId: Long, link: String, ctrl: ChatCtrl) {
private fun MutableState<MigrationFromState>.LinkShownView(fileId: Long, link: String, ctrl: ChatCtrl) {
SectionView {
SettingsActionItemWithContent(
icon = painterResource(MR.images.ic_close),
text = stringResource(MR.strings.migration_to_device_cancel_migration),
text = stringResource(MR.strings.migrate_from_device_cancel_migration),
textColor = MaterialTheme.colors.error,
click = {
cancelMigration(fileId, ctrl)
@@ -335,31 +345,32 @@ private fun MutableState<MigrationToState>.LinkShownView(fileId: Long, link: Str
) {}
SettingsActionItemWithContent(
icon = painterResource(MR.images.ic_check),
text = stringResource(MR.strings.migration_to_device_finalize_migration),
text = stringResource(MR.strings.migrate_from_device_finalize_migration),
textColor = MaterialTheme.colors.primary,
click = {
finishMigration(fileId, ctrl)
}
) {}
SectionTextFooter(annotatedStringResource(MR.strings.migration_to_device_choose_migrate_from_another_device))
SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_archive_will_be_deleted))
SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_choose_migrate_from_another_device))
}
SectionSpacer()
SectionView(stringResource(MR.strings.show_QR_code).uppercase()) {
SimpleXLinkQRCode(link, onShare = {})
}
SectionSpacer()
SectionView(stringResource(MR.strings.migration_to_device_or_share_this_file_link).uppercase()) {
SectionView(stringResource(MR.strings.migrate_from_device_or_share_this_file_link).uppercase()) {
LinkTextView(link, true)
}
}
@Composable
private fun MutableState<MigrationToState>.FinishedView(chatDeletion: Boolean) {
private fun MutableState<MigrationFromState>.FinishedView(chatDeletion: Boolean) {
Box {
SectionView(stringResource(MR.strings.migration_to_device_migration_complete).uppercase()) {
SectionView(stringResource(MR.strings.migrate_from_device_migration_complete).uppercase()) {
SettingsActionItemWithContent(
icon = painterResource(MR.images.ic_delete_forever),
text = stringResource(MR.strings.migration_to_device_delete_database_from_device),
text = stringResource(MR.strings.migrate_from_device_delete_database_from_device),
textColor = MaterialTheme.colors.primary,
click = {
AlertManager.shared.showAlertDialog(
@@ -375,21 +386,21 @@ private fun MutableState<MigrationToState>.FinishedView(chatDeletion: Boolean) {
SettingsActionItemWithContent(
icon = painterResource(MR.images.ic_play_arrow_filled),
text = stringResource(MR.strings.migration_to_device_start_chat),
text = stringResource(MR.strings.migrate_from_device_start_chat),
textColor = MaterialTheme.colors.error,
click = {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.start_chat_question),
text = generalGetString(MR.strings.migration_to_device_starting_chat_on_multiple_devices_unsupported),
confirmText = generalGetString(MR.strings.migration_to_device_start_chat),
text = generalGetString(MR.strings.migrate_from_device_starting_chat_on_multiple_devices_unsupported),
confirmText = generalGetString(MR.strings.migrate_from_device_start_chat),
onConfirm = {
withLongRunningApi { startChatAndDismiss() }
}
)
}
) {}
SectionTextFooter(annotatedStringResource(MR.strings.migration_to_device_you_must_not_start_database_on_two_device))
SectionTextFooter(annotatedStringResource(MR.strings.migration_to_device_using_on_two_device_breaks_encryption))
SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_you_must_not_start_database_on_two_device))
SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_using_on_two_device_breaks_encryption))
}
if (chatDeletion) {
ProgressView()
@@ -420,52 +431,58 @@ fun LargeProgressView(value: Float, title: String, description: String) {
}
}
private fun MutableState<MigrationToState>.stopChat() {
private fun MutableState<MigrationFromState>.stopChat() {
withBGApi {
try {
stopChatAsync(chatModel)
try {
controller.apiSaveAppSettings(AppSettings.current.prepareForExport())
state = if (appPreferences.initialRandomDBPassphrase.get()) MigrationToState.PassphraseNotSet else MigrationToState.PassphraseConfirmation
state = if (appPreferences.initialRandomDBPassphrase.get()) MigrationFromState.PassphraseNotSet else MigrationFromState.PassphraseConfirmation
} catch (e: Exception) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.migrate_to_device_error_saving_settings),
title = generalGetString(MR.strings.migrate_from_device_error_saving_settings),
text = e.stackTraceToString()
)
state = MigrationToState.ChatStopFailed(reason = generalGetString(MR.strings.migrate_to_device_error_saving_settings))
state = MigrationFromState.ChatStopFailed(reason = generalGetString(MR.strings.migrate_from_device_error_saving_settings))
}
} catch (e: Exception) {
state = MigrationToState.ChatStopFailed(reason = e.stackTraceToString().take(10))
state = MigrationFromState.ChatStopFailed(reason = e.stackTraceToString().take(10))
}
}
}
private suspend fun MutableState<MigrationToState>.verifyDatabasePassphrase(dbKey: String) {
if (controller.testStorageEncryption(dbKey)) {
state = MigrationToState.UploadConfirmation
} else {
private suspend fun MutableState<MigrationFromState>.verifyDatabasePassphrase(dbKey: String) {
val error = controller.testStorageEncryption(dbKey)
if (error == null) {
state = MigrationFromState.UploadConfirmation
} else if (((error.chatError as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorOpen)?.sqliteError is SQLiteError.ErrorNotADatabase) {
showErrorOnMigrationIfNeeded(DBMigrationResult.ErrorNotADatabase(""))
} else {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.error),
text = generalGetString(MR.strings.migrate_from_device_error_verifying_passphrase) + " " + error.details
)
}
}
private fun MutableState<MigrationToState>.exportArchive() {
private fun MutableState<MigrationFromState>.exportArchive() {
withLongRunningApi {
try {
getMigrationTempFilesDirectory().mkdir()
val archivePath = exportChatArchive(chatModel, getMigrationTempFilesDirectory(), mutableStateOf(""), mutableStateOf(Instant.DISTANT_PAST), mutableStateOf(""))
val totalBytes = File(archivePath).length()
if (totalBytes > 0L) {
state = MigrationToState.DatabaseInit(totalBytes, archivePath)
state = MigrationFromState.DatabaseInit(totalBytes, archivePath)
} else {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.migrate_to_device_exported_file_doesnt_exist))
state = MigrationToState.UploadConfirmation
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.migrate_from_device_exported_file_doesnt_exist))
state = MigrationFromState.UploadConfirmation
}
} catch (e: Exception) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.migrate_to_device_error_exporting_archive),
title = generalGetString(MR.strings.migrate_from_device_error_exporting_archive),
text = e.stackTraceToString()
)
state = MigrationToState.UploadConfirmation
state = MigrationFromState.UploadConfirmation
}
}
}
@@ -484,7 +501,7 @@ suspend fun initTemporaryDatabase(tempDatabaseFile: File, netCfg: NetCfg): Pair<
return null
}
private fun MutableState<MigrationToState>.prepareDatabase(
private fun MutableState<MigrationFromState>.prepareDatabase(
tempDatabaseFile: File,
totalBytes: Long,
archivePath: String,
@@ -492,35 +509,35 @@ private fun MutableState<MigrationToState>.prepareDatabase(
withLongRunningApi {
val ctrlAndUser = initTemporaryDatabase(tempDatabaseFile, getNetCfg())
if (ctrlAndUser == null) {
state = MigrationToState.UploadFailed(totalBytes, archivePath)
state = MigrationFromState.UploadFailed(totalBytes, archivePath)
return@withLongRunningApi
}
val (ctrl, user) = ctrlAndUser
state = MigrationToState.UploadProgress(0L, totalBytes, 0L, archivePath, ctrl, user)
state = MigrationFromState.UploadProgress(0L, totalBytes, 0L, archivePath, ctrl, user)
}
}
private fun MutableState<MigrationToState>.startUploading(
private fun MutableState<MigrationFromState>.startUploading(
totalBytes: Long,
ctrl: ChatCtrl,
user: User,
tempDatabaseFile: File,
chatReceiver: MutableState<MigrationToChatReceiver?>,
chatReceiver: MutableState<MigrationFromChatReceiver?>,
archivePath: String,
) {
withBGApi {
chatReceiver.value = MigrationToChatReceiver(ctrl, tempDatabaseFile) { msg ->
chatReceiver.value = MigrationFromChatReceiver(ctrl, tempDatabaseFile) { msg ->
when (msg) {
is CR.SndFileProgressXFTP -> {
val s = state
if (s is MigrationToState.UploadProgress && s.uploadedBytes != s.totalBytes) {
state = MigrationToState.UploadProgress(msg.sentSize, msg.totalSize, msg.fileTransferMeta.fileId, archivePath, ctrl, user)
if (s is MigrationFromState.UploadProgress && s.uploadedBytes != s.totalBytes) {
state = MigrationFromState.UploadProgress(msg.sentSize, msg.totalSize, msg.fileTransferMeta.fileId, archivePath, ctrl, user)
}
}
is CR.SndFileRedirectStartXFTP -> {
delay(500)
state = MigrationToState.LinkCreation
state = MigrationFromState.LinkCreation
}
is CR.SndStandaloneFileComplete -> {
delay(500)
@@ -532,7 +549,14 @@ private fun MutableState<MigrationToState>.startUploading(
requiredHostMode = cfg.requiredHostMode
)
)
state = MigrationToState.LinkShown(msg.fileTransferMeta.fileId, data.addToLink(msg.rcvURIs[0]), ctrl)
state = MigrationFromState.LinkShown(msg.fileTransferMeta.fileId, data.addToLink(msg.rcvURIs[0]), ctrl)
}
is CR.SndFileError -> {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.migrate_from_device_upload_failed),
generalGetString(MR.strings.migrate_from_device_check_connection_and_try_again)
)
state = MigrationFromState.UploadFailed(totalBytes, archivePath)
}
else -> {
Log.d(TAG, "unsupported event: ${msg.responseType}")
@@ -544,13 +568,13 @@ private fun MutableState<MigrationToState>.startUploading(
val (res, error) = controller.uploadStandaloneFile(user, CryptoFile.plain(File(archivePath).name), ctrl)
if (res == null) {
state = MigrationToState.UploadFailed(totalBytes, archivePath)
state = MigrationFromState.UploadFailed(totalBytes, archivePath)
return@withBGApi AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.migration_to_device_error_uploading_archive),
generalGetString(MR.strings.migrate_from_device_error_uploading_archive),
error
)
}
state = MigrationToState.UploadProgress(0, res.fileSize, res.fileId, archivePath, ctrl, user)
state = MigrationFromState.UploadProgress(0, res.fileSize, res.fileId, archivePath, ctrl, user)
}
}
@@ -565,19 +589,19 @@ private fun cancelMigration(fileId: Long, ctrl: ChatCtrl) {
}
}
private fun MutableState<MigrationToState>.finishMigration(fileId: Long, ctrl: ChatCtrl) {
private fun MutableState<MigrationFromState>.finishMigration(fileId: Long, ctrl: ChatCtrl) {
withBGApi {
cancelUploadedArchive(fileId, ctrl)
state = MigrationToState.Finished(false)
state = MigrationFromState.Finished(false)
}
}
private fun MutableState<MigrationToState>.deleteChatAndDismiss() {
private fun MutableState<MigrationFromState>.deleteChatAndDismiss() {
withBGApi {
try {
deleteChatAsync(chatModel)
chatModel.chatDbChanged.value = true
state = MigrationToState.Finished(true)
state = MigrationFromState.Finished(true)
try {
initChatController(startChat = { CompletableDeferred(false) })
chatModel.chatDbChanged.value = false
@@ -587,7 +611,7 @@ private fun MutableState<MigrationToState>.deleteChatAndDismiss() {
}
} catch (e: Exception) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.migration_to_device_error_deleting_database),
title = generalGetString(MR.strings.migrate_from_device_error_deleting_database),
text = e.stackTraceToString()
)
}
@@ -615,14 +639,14 @@ private suspend fun startChatAndDismiss(dismiss: Boolean = true) {
}
}
private suspend fun MutableState<MigrationToState>.cleanUpOnBack(chatReceiver: MigrationToChatReceiver?) {
private suspend fun MutableState<MigrationFromState>.cleanUpOnBack(chatReceiver: MigrationFromChatReceiver?) {
val s = state
if (s !is MigrationToState.LinkShown && s !is MigrationToState.Finished) {
if (s !is MigrationFromState.LinkShown && s !is MigrationFromState.Finished) {
chatModel.switchingUsersAndHosts.value = true
startChatAndDismiss(false)
chatModel.switchingUsersAndHosts.value = false
}
if (s is MigrationToState.UploadProgress) {
if (s is MigrationFromState.UploadProgress) {
cancelUploadedArchive(s.fileId, s.ctrl)
}
chatReceiver?.stopAndCleanUp()
@@ -632,7 +656,7 @@ private suspend fun MutableState<MigrationToState>.cleanUpOnBack(chatReceiver: M
private fun fileForTemporaryDatabase(): File =
File(getMigrationTempFilesDirectory(), generateNewFileName("migration", "db", getMigrationTempFilesDirectory()))
private class MigrationToChatReceiver(
private class MigrationFromChatReceiver(
val ctrl: ChatCtrl,
val databaseUrl: File,
var receiveMessages: Boolean = true,
@@ -13,7 +13,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import chat.simplex.common.model.*
import chat.simplex.common.model.AppPreferences.Companion.SHARED_PREFS_MIGRATION_STAGE
import chat.simplex.common.model.AppPreferences.Companion.SHARED_PREFS_MIGRATION_TO_STAGE
import chat.simplex.common.model.ChatController.getNetCfg
import chat.simplex.common.model.ChatController.startChat
import chat.simplex.common.model.ChatCtrl
@@ -39,100 +39,97 @@ import java.util.*
import kotlin.math.max
@Serializable
sealed class MigrationFromAnotherDeviceState {
@Serializable @SerialName("onion") data class Onion(val link: String, val socksProxy: String?, val hostMode: HostMode, val requiredHostMode: Boolean): MigrationFromAnotherDeviceState()
@Serializable @SerialName("downloadProgress") data class DownloadProgress(val link: String, val archiveName: String, val netCfg: NetCfg): MigrationFromAnotherDeviceState()
@Serializable @SerialName("archiveImport") data class ArchiveImport(val archiveName: String, val netCfg: NetCfg): MigrationFromAnotherDeviceState()
@Serializable @SerialName("passphrase") data class Passphrase(val netCfg: NetCfg): MigrationFromAnotherDeviceState()
sealed class MigrationToDeviceState {
@Serializable @SerialName("onion") data class Onion(val link: String, val socksProxy: String?, val hostMode: HostMode, val requiredHostMode: Boolean): MigrationToDeviceState()
@Serializable @SerialName("downloadProgress") data class DownloadProgress(val link: String, val archiveName: String, val netCfg: NetCfg): MigrationToDeviceState()
@Serializable @SerialName("archiveImport") data class ArchiveImport(val archiveName: String, val netCfg: NetCfg): MigrationToDeviceState()
@Serializable @SerialName("passphrase") data class Passphrase(val netCfg: NetCfg): MigrationToDeviceState()
companion object {
// Here we check whether it's needed to show migration process after app restart or not
// It's important to NOT show the process when archive was corrupted/not fully downloaded
fun transform(): MigrationFromAnotherDeviceState? {
val stage = settings.getStringOrNull(SHARED_PREFS_MIGRATION_STAGE)
var state: MigrationFromAnotherDeviceState? = if (stage != null) json.decodeFromString(stage) else null
if (state is DownloadProgress) {
// No migration happens at the moment actually since archive were not downloaded fully
Log.e(TAG, "MigrateFromDevice: archive wasn't fully downloaded, removed broken file")
state = null
} else if (state is Onion) {
state = null
} else if (state is ArchiveImport && !File(getMigrationTempFilesDirectory(), state.archiveName).exists()) {
Log.e(TAG, "MigrateFromDevice: archive was removed unintentionally or state is broken, dropping migration")
state = null
fun makeMigrationState(): MigrationToState? {
val stage = settings.getStringOrNull(SHARED_PREFS_MIGRATION_TO_STAGE)
val state: MigrationToDeviceState? = if (stage != null) json.decodeFromString(stage) else null
val initial: MigrationToState? = when(state) {
null -> null
is DownloadProgress -> {
// No migration happens at the moment actually since archive were not downloaded fully
Log.e(TAG, "MigrateToDevice: archive wasn't fully downloaded, removed broken file")
null
}
is Onion -> null
is ArchiveImport -> {
if (!File(getMigrationTempFilesDirectory(), state.archiveName).exists()) {
Log.e(TAG, "MigrateToDevice: archive was removed unintentionally or state is broken, dropping migration")
null
} else {
val archivePath = File(getMigrationTempFilesDirectory(), state.archiveName)
MigrationToState.ArchiveImportFailed(archivePath.absolutePath, state.netCfg)
}
}
is Passphrase -> MigrationToState.Passphrase("", state.netCfg)
}
if (state == null) {
settings.remove(SHARED_PREFS_MIGRATION_STAGE)
if (initial == null) {
settings.remove(SHARED_PREFS_MIGRATION_TO_STAGE)
getMigrationTempFilesDirectory().deleteRecursively()
}
return state
return initial
}
fun save(state: MigrationFromAnotherDeviceState?) {
fun save(state: MigrationToDeviceState?) {
if (state != null) {
appPreferences.migrationStage.set(json.encodeToString(state))
appPreferences.migrationToStage.set(json.encodeToString(state))
} else {
appPreferences.migrationStage.set(null)
appPreferences.migrationToStage.set(null)
}
chatModel.migrationState.value = state
}
}
}
@Serializable
private sealed class MigrationState {
@Serializable object PasteOrScanLink: MigrationState()
@Serializable data class Onion(val link: String, val socksProxy: String?, val hostMode: HostMode, val requiredHostMode: Boolean): MigrationState()
@Serializable data class DatabaseInit(val link: String, val netCfg: NetCfg): MigrationState()
@Serializable data class LinkDownloading(val link: String, val ctrl: ChatCtrl, val user: User, val archivePath: String, val netCfg: NetCfg): MigrationState()
@Serializable data class DownloadProgress(val downloadedBytes: Long, val totalBytes: Long, val fileId: Long, val link: String, val archivePath: String, val netCfg: NetCfg, val ctrl: ChatCtrl?): MigrationState()
@Serializable data class DownloadFailed(val totalBytes: Long, val link: String, val archivePath: String, val netCfg: NetCfg): MigrationState()
@Serializable data class ArchiveImport(val archivePath: String, val netCfg: NetCfg): MigrationState()
@Serializable data class ArchiveImportFailed(val archivePath: String, val netCfg: NetCfg): MigrationState()
@Serializable data class Passphrase(val passphrase: String, val netCfg: NetCfg): MigrationState()
@Serializable data class MigrationConfirmation(val status: DBMigrationResult, val passphrase: String, val useKeychain: Boolean, val netCfg: NetCfg): MigrationState()
@Serializable data class Migration(val passphrase: String, val confirmation: chat.simplex.common.views.helpers.MigrationConfirmation, val useKeychain: Boolean, val netCfg: NetCfg): MigrationState()
sealed class MigrationToState {
@Serializable object PasteOrScanLink: MigrationToState()
@Serializable data class Onion(val link: String, val socksProxy: String?, val hostMode: HostMode, val requiredHostMode: Boolean): MigrationToState()
@Serializable data class DatabaseInit(val link: String, val netCfg: NetCfg): MigrationToState()
@Serializable data class LinkDownloading(val link: String, val ctrl: ChatCtrl, val user: User, val archivePath: String, val netCfg: NetCfg): MigrationToState()
@Serializable data class DownloadProgress(val downloadedBytes: Long, val totalBytes: Long, val fileId: Long, val link: String, val archivePath: String, val netCfg: NetCfg, val ctrl: ChatCtrl?): MigrationToState()
@Serializable data class DownloadFailed(val totalBytes: Long, val link: String, val archivePath: String, val netCfg: NetCfg): MigrationToState()
@Serializable data class ArchiveImport(val archivePath: String, val netCfg: NetCfg): MigrationToState()
@Serializable data class ArchiveImportFailed(val archivePath: String, val netCfg: NetCfg): MigrationToState()
@Serializable data class Passphrase(val passphrase: String, val netCfg: NetCfg): MigrationToState()
@Serializable data class MigrationConfirmation(val status: DBMigrationResult, val passphrase: String, val useKeychain: Boolean, val netCfg: NetCfg): MigrationToState()
@Serializable data class Migration(val passphrase: String, val confirmation: chat.simplex.common.views.helpers.MigrationConfirmation, val useKeychain: Boolean, val netCfg: NetCfg): MigrationToState()
}
private var MutableState<MigrationState>.state: MigrationState
private var MutableState<MigrationToState?>.state: MigrationToState?
get() = value
set(v) { value = v }
@Composable
fun ModalData.MigrateFromAnotherDeviceView(state: MigrationFromAnotherDeviceState? = null, close: () -> Unit) {
val migrationState = rememberSaveable(stateSaver = serializableSaver()) {
mutableStateOf(
when (state) {
null -> MigrationState.PasteOrScanLink
is MigrationFromAnotherDeviceState.Onion -> {
MigrationState.Onion(state.link, state.socksProxy, state.hostMode, state.requiredHostMode)
}
is MigrationFromAnotherDeviceState.DownloadProgress -> {
val archivePath = File(getMigrationTempFilesDirectory(), state.archiveName)
// SHOULDN'T BE HERE because the app checks this before opening migration screen and will not open it in this case.
// See analyzeMigrationState()
MigrationState.DownloadFailed(totalBytes = 0, link = state.link, archivePath = archivePath.absolutePath, state.netCfg)
}
is MigrationFromAnotherDeviceState.ArchiveImport -> {
val archivePath = File(getMigrationTempFilesDirectory(), state.archiveName)
MigrationState.ArchiveImportFailed(archivePath.absolutePath, state.netCfg)
}
is MigrationFromAnotherDeviceState.Passphrase -> {
MigrationState.Passphrase("", state.netCfg)
}
}
)
}
fun ModalData.MigrateToDeviceView(close: () -> Unit) {
val migrationState = remember { chatModel.migrationState }
// Prevent from hiding the view until migration is finished or app deleted
val backDisabled = remember {
derivedStateOf {
val s = chatModel.migrationState.value
s is MigrationFromAnotherDeviceState.ArchiveImport ||
s is MigrationFromAnotherDeviceState.Passphrase ||
migrationState.value is MigrationState.DatabaseInit
when (chatModel.migrationState.value) {
null,
is MigrationToState.PasteOrScanLink,
is MigrationToState.Onion,
is MigrationToState.LinkDownloading,
is MigrationToState.DownloadProgress,
is MigrationToState.DownloadFailed,
is MigrationToState.ArchiveImportFailed -> false
is MigrationToState.ArchiveImport,
is MigrationToState.DatabaseInit,
is MigrationToState.Migration,
is MigrationToState.MigrationConfirmation,
is MigrationToState.Passphrase -> true
}
}
}
val chatReceiver = remember { mutableStateOf(null as MigrationFromChatReceiver?) }
val chatReceiver = remember { mutableStateOf(null as MigrationToChatReceiver?) }
ModalView(
enableClose = !backDisabled.value,
close = {
@@ -142,7 +139,7 @@ fun ModalData.MigrateFromAnotherDeviceView(state: MigrationFromAnotherDeviceStat
}
},
) {
MigrateFromAnotherDeviceLayout(
MigrateToDeviceLayout(
migrationState = migrationState,
chatReceiver = chatReceiver,
close = close,
@@ -151,9 +148,9 @@ fun ModalData.MigrateFromAnotherDeviceView(state: MigrationFromAnotherDeviceStat
}
@Composable
private fun ModalData.MigrateFromAnotherDeviceLayout(
migrationState: MutableState<MigrationState>,
chatReceiver: MutableState<MigrationFromChatReceiver?>,
private fun ModalData.MigrateToDeviceLayout(
migrationState: MutableState<MigrationToState?>,
chatReceiver: MutableState<MigrationToChatReceiver?>,
close: () -> Unit,
) {
val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) }
@@ -161,7 +158,7 @@ private fun ModalData.MigrateFromAnotherDeviceLayout(
Column(
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).height(IntrinsicSize.Max),
) {
AppBarTitle(stringResource(MR.strings.migrate_here))
AppBarTitle(stringResource(MR.strings.migrate_to_device_title))
SectionByState(migrationState, tempDatabaseFile.value, chatReceiver, close)
SectionBottomSpacer()
}
@@ -170,28 +167,29 @@ private fun ModalData.MigrateFromAnotherDeviceLayout(
@Composable
private fun ModalData.SectionByState(
migrationState: MutableState<MigrationState>,
migrationState: MutableState<MigrationToState?>,
tempDatabaseFile: File,
chatReceiver: MutableState<MigrationFromChatReceiver?>,
chatReceiver: MutableState<MigrationToChatReceiver?>,
close: () -> Unit
) {
when (val s = migrationState.value) {
is MigrationState.PasteOrScanLink -> migrationState.PasteOrScanLinkView()
is MigrationState.Onion -> OnionView(s.link, s.socksProxy, s.hostMode, s.requiredHostMode, migrationState)
is MigrationState.DatabaseInit -> migrationState.DatabaseInitView(s.link, tempDatabaseFile, s.netCfg)
is MigrationState.LinkDownloading -> migrationState.LinkDownloadingView(s.link, s.ctrl, s.user, s.archivePath, tempDatabaseFile, chatReceiver, s.netCfg)
is MigrationState.DownloadProgress -> DownloadProgressView(s.downloadedBytes, totalBytes = s.totalBytes)
is MigrationState.DownloadFailed -> migrationState.DownloadFailedView(s.link, chatReceiver.value, s.archivePath, s.netCfg)
is MigrationState.ArchiveImport -> migrationState.ArchiveImportView(s.archivePath, s.netCfg)
is MigrationState.ArchiveImportFailed -> migrationState.ArchiveImportFailedView(s.archivePath, s.netCfg)
is MigrationState.Passphrase -> migrationState.PassphraseEnteringView(currentKey = s.passphrase, s.netCfg)
is MigrationState.MigrationConfirmation -> migrationState.MigrationConfirmationView(s.status, s.passphrase, s.useKeychain, s.netCfg)
is MigrationState.Migration -> MigrationView(s.passphrase, s.confirmation, s.useKeychain, s.netCfg, close)
null -> {}
is MigrationToState.PasteOrScanLink -> migrationState.PasteOrScanLinkView()
is MigrationToState.Onion -> OnionView(s.link, s.socksProxy, s.hostMode, s.requiredHostMode, migrationState)
is MigrationToState.DatabaseInit -> migrationState.DatabaseInitView(s.link, tempDatabaseFile, s.netCfg)
is MigrationToState.LinkDownloading -> migrationState.LinkDownloadingView(s.link, s.ctrl, s.user, s.archivePath, tempDatabaseFile, chatReceiver, s.netCfg)
is MigrationToState.DownloadProgress -> DownloadProgressView(s.downloadedBytes, totalBytes = s.totalBytes)
is MigrationToState.DownloadFailed -> migrationState.DownloadFailedView(s.link, chatReceiver.value, s.archivePath, s.netCfg)
is MigrationToState.ArchiveImport -> migrationState.ArchiveImportView(s.archivePath, s.netCfg)
is MigrationToState.ArchiveImportFailed -> migrationState.ArchiveImportFailedView(s.archivePath, s.netCfg)
is MigrationToState.Passphrase -> migrationState.PassphraseEnteringView(currentKey = s.passphrase, s.netCfg)
is MigrationToState.MigrationConfirmation -> migrationState.MigrationConfirmationView(s.status, s.passphrase, s.useKeychain, s.netCfg)
is MigrationToState.Migration -> MigrationView(s.passphrase, s.confirmation, s.useKeychain, s.netCfg, close)
}
}
@Composable
private fun MutableState<MigrationState>.PasteOrScanLinkView() {
private fun MutableState<MigrationToState?>.PasteOrScanLinkView() {
if (appPlatform.isAndroid) {
SectionView(stringResource(MR.strings.scan_QR_code).replace('\n', ' ').uppercase()) {
QRCodeScanner(showQRCodeScanner = remember { mutableStateOf(true) }) { text ->
@@ -209,7 +207,7 @@ private fun MutableState<MigrationState>.PasteOrScanLinkView() {
}
@Composable
private fun MutableState<MigrationState>.PasteLinkView() {
private fun MutableState<MigrationToState?>.PasteLinkView() {
val clipboard = LocalClipboardManager.current
SectionItemView({
val str = clipboard.getText()?.text ?: return@SectionItemView
@@ -220,7 +218,7 @@ private fun MutableState<MigrationState>.PasteLinkView() {
}
@Composable
private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: HostMode, requiredHostMode: Boolean, state: MutableState<MigrationState>) {
private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: HostMode, requiredHostMode: Boolean, state: MutableState<MigrationToState?>) {
val onionHosts = remember { stateGetOrPut("onionHosts") {
getNetCfg().copy(socksProxy = socksProxy, hostMode = hostMode, requiredHostMode = requiredHostMode).onionHosts
} }
@@ -238,10 +236,10 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos
mutableStateOf(getNetCfg().withOnionHosts(onionHosts.value).copy(socksProxy = socksProxy, sessionMode = sessionMode.value))
}
SectionView(stringResource(MR.strings.migration_from_device_confirm_network_settings).uppercase()) {
SectionView(stringResource(MR.strings.migrate_to_device_confirm_network_settings).uppercase()) {
SettingsActionItemWithContent(
icon = painterResource(MR.images.ic_check),
text = stringResource(MR.strings.migration_from_device_apply_onion),
text = stringResource(MR.strings.migrate_to_device_apply_onion),
textColor = MaterialTheme.colors.primary,
click = {
val updated = netCfg.value
@@ -251,11 +249,11 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos
sessionMode = sessionMode.value
)
withBGApi {
state.value = MigrationState.DatabaseInit(link, updated)
state.value = MigrationToState.DatabaseInit(link, updated)
}
}
){}
SectionTextFooter(stringResource(MR.strings.migration_from_device_confirm_network_settings_footer))
SectionTextFooter(stringResource(MR.strings.migrate_to_device_confirm_network_settings_footer))
}
SectionSpacer()
@@ -285,9 +283,9 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos
}
@Composable
private fun MutableState<MigrationState>.DatabaseInitView(link: String, tempDatabaseFile: File, netCfg: NetCfg) {
private fun MutableState<MigrationToState?>.DatabaseInitView(link: String, tempDatabaseFile: File, netCfg: NetCfg) {
Box {
SectionView(stringResource(MR.strings.migration_from_device_database_init).uppercase()) {}
SectionView(stringResource(MR.strings.migrate_to_device_database_init).uppercase()) {}
ProgressView()
}
LaunchedEffect(Unit) {
@@ -296,17 +294,17 @@ private fun MutableState<MigrationState>.DatabaseInitView(link: String, tempData
}
@Composable
private fun MutableState<MigrationState>.LinkDownloadingView(
private fun MutableState<MigrationToState?>.LinkDownloadingView(
link: String,
ctrl: ChatCtrl,
user: User,
archivePath: String,
tempDatabaseFile: File,
chatReceiver: MutableState<MigrationFromChatReceiver?>,
chatReceiver: MutableState<MigrationToChatReceiver?>,
netCfg: NetCfg
) {
Box {
SectionView(stringResource(MR.strings.migration_from_device_downloading_details).uppercase()) {}
SectionView(stringResource(MR.strings.migrate_to_device_downloading_details).uppercase()) {}
ProgressView()
}
LaunchedEffect(Unit) {
@@ -317,37 +315,37 @@ private fun MutableState<MigrationState>.LinkDownloadingView(
@Composable
private fun DownloadProgressView(downloadedBytes: Long, totalBytes: Long) {
Box {
SectionView(stringResource(MR.strings.migration_from_device_downloading_archive).uppercase()) {
SectionView(stringResource(MR.strings.migrate_to_device_downloading_archive).uppercase()) {
val ratio = downloadedBytes.toFloat() / max(totalBytes, 1)
LargeProgressView(ratio, "${(ratio * 100).toInt()}%", stringResource(MR.strings.migration_from_device_bytes_downloaded).format(formatBytes(downloadedBytes)))
LargeProgressView(ratio, "${(ratio * 100).toInt()}%", stringResource(MR.strings.migrate_to_device_bytes_downloaded).format(formatBytes(downloadedBytes)))
}
}
}
@Composable
private fun MutableState<MigrationState>.DownloadFailedView(link: String, chatReceiver: MigrationFromChatReceiver?, archivePath: String, netCfg: NetCfg) {
SectionView(stringResource(MR.strings.migration_from_device_download_failed).uppercase()) {
private fun MutableState<MigrationToState?>.DownloadFailedView(link: String, chatReceiver: MigrationToChatReceiver?, archivePath: String, netCfg: NetCfg) {
SectionView(stringResource(MR.strings.migrate_to_device_download_failed).uppercase()) {
SettingsActionItemWithContent(
icon = painterResource(MR.images.ic_download),
text = stringResource(MR.strings.migration_from_device_repeat_download),
text = stringResource(MR.strings.migrate_to_device_repeat_download),
textColor = MaterialTheme.colors.primary,
click = {
state = MigrationState.DatabaseInit(link, netCfg)
state = MigrationToState.DatabaseInit(link, netCfg)
}
) {}
SectionTextFooter(stringResource(MR.strings.migration_from_device_try_again))
SectionTextFooter(stringResource(MR.strings.migrate_to_device_try_again))
}
LaunchedEffect(Unit) {
chatReceiver?.stopAndCleanUp()
File(archivePath).delete()
MigrationFromAnotherDeviceState.save(null)
MigrationToDeviceState.save(null)
}
}
@Composable
private fun MutableState<MigrationState>.ArchiveImportView(archivePath: String, netCfg: NetCfg) {
private fun MutableState<MigrationToState?>.ArchiveImportView(archivePath: String, netCfg: NetCfg) {
Box {
SectionView(stringResource(MR.strings.migration_from_device_importing_archive).uppercase()) {}
SectionView(stringResource(MR.strings.migrate_to_device_importing_archive).uppercase()) {}
ProgressView()
}
LaunchedEffect(Unit) {
@@ -356,29 +354,29 @@ private fun MutableState<MigrationState>.ArchiveImportView(archivePath: String,
}
@Composable
private fun MutableState<MigrationState>.ArchiveImportFailedView(archivePath: String, netCfg: NetCfg) {
SectionView(stringResource(MR.strings.migration_from_device_import_failed).uppercase()) {
private fun MutableState<MigrationToState?>.ArchiveImportFailedView(archivePath: String, netCfg: NetCfg) {
SectionView(stringResource(MR.strings.migrate_to_device_import_failed).uppercase()) {
SettingsActionItemWithContent(
icon = painterResource(MR.images.ic_download),
text = stringResource(MR.strings.migration_from_device_repeat_import),
text = stringResource(MR.strings.migrate_to_device_repeat_import),
textColor = MaterialTheme.colors.primary,
click = {
state = MigrationState.ArchiveImport(archivePath, netCfg)
state = MigrationToState.ArchiveImport(archivePath, netCfg)
}
) {}
SectionTextFooter(stringResource(MR.strings.migration_from_device_try_again))
SectionTextFooter(stringResource(MR.strings.migrate_to_device_try_again))
}
}
@Composable
private fun MutableState<MigrationState>.PassphraseEnteringView(currentKey: String, netCfg: NetCfg) {
private fun MutableState<MigrationToState?>.PassphraseEnteringView(currentKey: String, netCfg: NetCfg) {
val currentKey = rememberSaveable { mutableStateOf(currentKey) }
val verifyingPassphrase = rememberSaveable { mutableStateOf(false) }
val useKeychain = rememberSaveable { mutableStateOf(appPreferences.storeDBPassphrase.get()) }
Box {
val view = LocalMultiplatformView()
SectionView(stringResource(MR.strings.migration_from_device_enter_passphrase).uppercase()) {
SectionView(stringResource(MR.strings.migrate_to_device_enter_passphrase).uppercase()) {
SavePassphraseSetting(
useKeychain.value,
false,
@@ -401,9 +399,9 @@ private fun MutableState<MigrationState>.PassphraseEnteringView(currentKey: Stri
val (status, _) = chatInitTemporaryDatabase(dbAbsolutePrefixPath, key = currentKey.value, confirmation = MigrationConfirmation.YesUp)
val success = status == DBMigrationResult.OK || status == DBMigrationResult.InvalidConfirmation
if (success) {
state = MigrationState.Migration(currentKey.value, MigrationConfirmation.YesUp, useKeychain.value, netCfg)
state = MigrationToState.Migration(currentKey.value, MigrationConfirmation.YesUp, useKeychain.value, netCfg)
} else if (status is DBMigrationResult.ErrorMigration) {
state = MigrationState.MigrationConfirmation(status, currentKey.value, useKeychain.value, netCfg)
state = MigrationToState.MigrationConfirmation(status, currentKey.value, useKeychain.value, netCfg)
} else {
showErrorOnMigrationIfNeeded(status)
}
@@ -420,7 +418,7 @@ private fun MutableState<MigrationState>.PassphraseEnteringView(currentKey: Stri
}
@Composable
private fun MutableState<MigrationState>.MigrationConfirmationView(status: DBMigrationResult, passphrase: String, useKeychain: Boolean, netCfg: NetCfg) {
private fun MutableState<MigrationToState?>.MigrationConfirmationView(status: DBMigrationResult, passphrase: String, useKeychain: Boolean, netCfg: NetCfg) {
data class Tuple4<A,B,C,D>(val a: A, val b: B, val c: C, val d: D)
val (header: String, button: String?, footer: String, confirmation: MigrationConfirmation?) = when (status) {
is DBMigrationResult.ErrorMigration -> when (val err = status.migrationError) {
@@ -455,7 +453,7 @@ private fun MutableState<MigrationState>.MigrationConfirmationView(status: DBMig
text = button,
textColor = MaterialTheme.colors.primary,
click = {
state = MigrationState.Migration(passphrase, confirmation, useKeychain, netCfg)
state = MigrationToState.Migration(passphrase, confirmation, useKeychain, netCfg)
}
) {}
}
@@ -466,7 +464,7 @@ private fun MutableState<MigrationState>.MigrationConfirmationView(status: DBMig
@Composable
private fun MigrationView(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Boolean, netCfg: NetCfg, close: () -> Unit) {
Box {
SectionView(stringResource(MR.strings.migration_from_device_migrating).uppercase()) {}
SectionView(stringResource(MR.strings.migrate_to_device_migrating).uppercase()) {}
ProgressView()
}
LaunchedEffect(Unit) {
@@ -479,18 +477,18 @@ private fun ProgressView() {
DefaultProgressView(null)
}
private suspend fun MutableState<MigrationState>.checkUserLink(link: String) {
private suspend fun MutableState<MigrationToState?>.checkUserLink(link: String) {
if (strHasSimplexFileLink(link.trim())) {
val data = MigrationFileLinkData.readFromLink(link)
val hasOnionConfigured = data?.networkConfig?.hasOnionConfigured() ?: false
val networkConfig = data?.networkConfig?.transformToPlatformSupported()
// If any of iOS or Android had onion enabled, show onion screen
if (hasOnionConfigured && networkConfig?.hostMode != null && networkConfig.requiredHostMode != null) {
state = MigrationState.Onion(link.trim(), networkConfig.socksProxy, networkConfig.hostMode, networkConfig.requiredHostMode)
MigrationFromAnotherDeviceState.save(MigrationFromAnotherDeviceState.Onion(link.trim(), networkConfig.socksProxy, networkConfig.hostMode, networkConfig.requiredHostMode))
state = MigrationToState.Onion(link.trim(), networkConfig.socksProxy, networkConfig.hostMode, networkConfig.requiredHostMode)
MigrationToDeviceState.save(MigrationToDeviceState.Onion(link.trim(), networkConfig.socksProxy, networkConfig.hostMode, networkConfig.requiredHostMode))
} else {
val current = getNetCfg()
state = MigrationState.DatabaseInit(link.trim(), current.copy(
state = MigrationToState.DatabaseInit(link.trim(), current.copy(
socksProxy = networkConfig?.socksProxy,
hostMode = networkConfig?.hostMode ?: current.hostMode,
requiredHostMode = networkConfig?.requiredHostMode ?: current.requiredHostMode
@@ -504,7 +502,7 @@ private suspend fun MutableState<MigrationState>.checkUserLink(link: String) {
}
}
private fun MutableState<MigrationState>.prepareDatabase(
private fun MutableState<MigrationToState?>.prepareDatabase(
link: String,
tempDatabaseFile: File,
netCfg: NetCfg,
@@ -512,43 +510,59 @@ private fun MutableState<MigrationState>.prepareDatabase(
withLongRunningApi {
val ctrlAndUser = initTemporaryDatabase(tempDatabaseFile, netCfg)
if (ctrlAndUser == null) {
state = MigrationState.DownloadFailed(0, link, archivePath(), netCfg)
state = MigrationToState.DownloadFailed(0, link, archivePath(), netCfg)
return@withLongRunningApi
}
val (ctrl, user) = ctrlAndUser
state = MigrationState.LinkDownloading(link, ctrl, user, archivePath(), netCfg)
state = MigrationToState.LinkDownloading(link, ctrl, user, archivePath(), netCfg)
}
}
private fun MutableState<MigrationState>.startDownloading(
private fun MutableState<MigrationToState?>.startDownloading(
totalBytes: Long,
ctrl: ChatCtrl,
user: User,
tempDatabaseFile: File,
chatReceiver: MutableState<MigrationFromChatReceiver?>,
chatReceiver: MutableState<MigrationToChatReceiver?>,
link: String,
archivePath: String,
netCfg: NetCfg,
) {
withBGApi {
chatReceiver.value = MigrationFromChatReceiver(ctrl, tempDatabaseFile) { msg ->
chatReceiver.value = MigrationToChatReceiver(ctrl, tempDatabaseFile) { msg ->
when (msg) {
is CR.RcvFileProgressXFTP -> {
state = MigrationState.DownloadProgress(msg.receivedSize, msg.totalSize, msg.rcvFileTransfer.fileId, link, archivePath, netCfg, ctrl)
MigrationFromAnotherDeviceState.save(MigrationFromAnotherDeviceState.DownloadProgress(link, File(archivePath).name, netCfg))
state = MigrationToState.DownloadProgress(msg.receivedSize, msg.totalSize, msg.rcvFileTransfer.fileId, link, archivePath, netCfg, ctrl)
MigrationToDeviceState.save(MigrationToDeviceState.DownloadProgress(link, File(archivePath).name, netCfg))
}
is CR.RcvStandaloneFileComplete -> {
delay(500)
state = MigrationState.ArchiveImport(archivePath, netCfg)
MigrationFromAnotherDeviceState.save(MigrationFromAnotherDeviceState.ArchiveImport(File(archivePath).name, netCfg))
// User closed the whole screen before new state was saved
if (state == null) {
MigrationToDeviceState.save(null)
} else {
state = MigrationToState.ArchiveImport(archivePath, netCfg)
MigrationToDeviceState.save(MigrationToDeviceState.ArchiveImport(File(archivePath).name, netCfg))
}
}
is CR.RcvFileError -> {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.migration_from_device_download_failed),
generalGetString(MR.strings.migration_from_device_file_delete_or_link_invalid)
generalGetString(MR.strings.migrate_to_device_download_failed),
generalGetString(MR.strings.migrate_to_device_file_delete_or_link_invalid)
)
state = MigrationState.DownloadFailed(totalBytes, link, archivePath, netCfg)
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg)
}
is CR.ChatRespError -> {
if (msg.chatError is ChatError.ChatErrorChat && msg.chatError.errorType is ChatErrorType.NoRcvFileUser) {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.migrate_to_device_download_failed),
generalGetString(MR.strings.migrate_to_device_file_delete_or_link_invalid)
)
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg)
} else {
Log.d(TAG, "unsupported error: ${msg.responseType}")
}
}
else -> Log.d(TAG, "unsupported event: ${msg.responseType}")
}
@@ -557,16 +571,16 @@ private fun MutableState<MigrationState>.startDownloading(
val (res, error) = controller.downloadStandaloneFile(user, link, CryptoFile.plain(File(archivePath).path), ctrl)
if (res == null) {
state = MigrationState.DownloadFailed(totalBytes, link, archivePath, netCfg)
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg)
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.migration_from_device_error_downloading_archive),
generalGetString(MR.strings.migrate_to_device_error_downloading_archive),
error
)
}
}
}
private fun MutableState<MigrationState>.importArchive(archivePath: String, netCfg: NetCfg) {
private fun MutableState<MigrationToState?>.importArchive(archivePath: String, netCfg: NetCfg) {
withLongRunningApi {
try {
if (ChatController.ctrl == null || ChatController.ctrl == -1L) {
@@ -582,14 +596,14 @@ private fun MutableState<MigrationState>.importArchive(archivePath: String, netC
generalGetString(MR.strings.non_fatal_errors_occured_during_import)
)
}
state = MigrationState.Passphrase("", netCfg)
MigrationFromAnotherDeviceState.save(MigrationFromAnotherDeviceState.Passphrase(netCfg))
state = MigrationToState.Passphrase("", netCfg)
MigrationToDeviceState.save(MigrationToDeviceState.Passphrase(netCfg))
} catch (e: Exception) {
state = MigrationState.ArchiveImportFailed(archivePath, netCfg)
state = MigrationToState.ArchiveImportFailed(archivePath, netCfg)
AlertManager.shared.showAlertMsg (generalGetString(MR.strings.error_importing_database), e.stackTraceToString())
}
} catch (e: Exception) {
state = MigrationState.ArchiveImportFailed(archivePath, netCfg)
state = MigrationToState.ArchiveImportFailed(archivePath, netCfg)
AlertManager.shared.showAlertMsg (generalGetString(MR.strings.error_deleting_database), e.stackTraceToString())
}
}
@@ -630,30 +644,32 @@ private suspend fun finishMigration(appSettings: AppSettings, close: () -> Unit)
startChat(user)
}
hideView(close)
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.migration_from_device_chat_migrated), generalGetString(MR.strings.migration_from_device_finalize_migration))
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.migrate_to_device_chat_migrated), generalGetString(MR.strings.migrate_to_device_finalize_migration))
} catch (e: Exception) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_starting_chat), e.stackTraceToString())
}
MigrationFromAnotherDeviceState.save(null)
MigrationToDeviceState.save(null)
}
private fun hideView(close: () -> Unit) {
appPreferences.onboardingStage.set(OnboardingStage.OnboardingComplete)
chatModel.migrationState.value = null
close()
}
private suspend fun MutableState<MigrationState>.cleanUpOnBack(chatReceiver: MigrationFromChatReceiver?) {
private suspend fun MutableState<MigrationToState?>.cleanUpOnBack(chatReceiver: MigrationToChatReceiver?) {
val state = state
if (state is MigrationState.ArchiveImportFailed) {
if (state is MigrationToState.ArchiveImportFailed) {
// Original database is not exist, nothing is set up correctly for showing to a user yet. Return to clean state
deleteChatDatabaseFilesAndState()
initChatControllerAndRunMigrations()
} else if (state is MigrationState.DownloadProgress && state.ctrl != null) {
} else if (state is MigrationToState.DownloadProgress && state.ctrl != null) {
stopArchiveDownloading(state.fileId, state.ctrl)
}
chatReceiver?.stopAndCleanUp()
getMigrationTempFilesDirectory().deleteRecursively()
MigrationFromAnotherDeviceState.save(null)
MigrationToDeviceState.save(null)
chatModel.migrationState.value = null
}
private fun strHasSimplexFileLink(text: String): Boolean =
@@ -670,7 +686,7 @@ private fun archivePath(): String {
return archivePath.absolutePath
}
private class MigrationFromChatReceiver(
private class MigrationToChatReceiver(
val ctrl: ChatCtrl,
val databaseUrl: File,
var receiveMessages: Boolean = true,
@@ -19,7 +19,8 @@ import chat.simplex.common.model.*
import chat.simplex.common.platform.chatModel
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.migration.MigrateFromAnotherDeviceView
import chat.simplex.common.views.migration.MigrateToDeviceView
import chat.simplex.common.views.migration.MigrationToState
import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource
@@ -71,7 +72,9 @@ fun SimpleXInfoLayout(
.padding(top = DEFAULT_PADDING), contentAlignment = Alignment.Center
) {
SimpleButtonDecorated(text = stringResource(MR.strings.migrate_from_another_device), icon = painterResource(MR.images.ic_download),
click = { ModalManager.fullscreen.showCustomModal { close -> MigrateFromAnotherDeviceView(chatModel.migrationState.value, close) } })
click = {
chatModel.migrationState.value = MigrationToState.PasteOrScanLink
ModalManager.fullscreen.showCustomModal { close -> MigrateToDeviceView(close) } })
}
}
@@ -85,9 +88,8 @@ fun SimpleXInfoLayout(
}
}
LaunchedEffect(Unit) {
val state = chatModel.migrationState.value
if (state != null && !ModalManager.fullscreen.hasModalsOpen()) {
ModalManager.fullscreen.showCustomModal(animated = false) { close -> MigrateFromAnotherDeviceView(state, close) }
if (chatModel.migrationState.value != null && !ModalManager.fullscreen.hasModalsOpen()) {
ModalManager.fullscreen.showCustomModal(animated = false) { close -> MigrateToDeviceView(close) }
}
}
}
@@ -28,8 +28,7 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.CreateProfile
import chat.simplex.common.views.database.DatabaseView
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.migration.MigrateFromAnotherDeviceView
import chat.simplex.common.views.migration.MigrateToAnotherDeviceView
import chat.simplex.common.views.migration.MigrateFromDeviceView
import chat.simplex.common.views.onboarding.SimpleXInfo
import chat.simplex.common.views.onboarding.WhatsNewView
import chat.simplex.common.views.remote.ConnectDesktopView
@@ -137,7 +136,7 @@ fun SettingsLayout(
} else {
SettingsActionItem(painterResource(MR.images.ic_desktop), stringResource(MR.strings.settings_section_title_use_from_desktop), showCustomModal{ it, close -> ConnectDesktopView(close) }, disabled = stopped, extraPadding = true)
}
SettingsActionItem(painterResource(MR.images.ic_ios_share), stringResource(MR.strings.migrate_to_device), { withAuth(generalGetString(MR.strings.auth_open_migration_to_another_device), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.fullscreen.showCustomModal { close -> MigrateToAnotherDeviceView(close) } }}, disabled = stopped, extraPadding = true)
SettingsActionItem(painterResource(MR.images.ic_ios_share), stringResource(MR.strings.migrate_from_device_to_another_device), { withAuth(generalGetString(MR.strings.auth_open_migration_to_another_device), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.fullscreen.showCustomModal { close -> MigrateFromDeviceView(close) } }}, disabled = stopped, extraPadding = true)
}
SectionDividerSpaced()
@@ -1846,63 +1846,66 @@
<string name="agent_internal_error_desc">Please report it to the developers: \n%s</string>
<string name="restart_chat_button">Restart chat</string>
<!-- MigrateFromAnotherDevice.kt -->
<string name="migrate_here">Migrate here</string>
<!-- MigrateToDevice.kt -->
<string name="migrate_to_device_title">Migrate here</string>
<string name="or_paste_archive_link">Or paste archive link</string>
<string name="paste_archive_link">Paste archive link</string>
<string name="invalid_file_link">Invalid link</string>
<string name="migration_from_device_migrating">Migrating</string>
<string name="migration_from_device_database_init">Preparing download</string>
<string name="migration_from_device_downloading_details">Downloading link details</string>
<string name="migration_from_device_downloading_archive">Downloading archive</string>
<string name="migration_from_device_bytes_downloaded">%s downloaded</string>
<string name="migration_from_device_download_failed">Download failed</string>
<string name="migration_from_device_repeat_download">Repeat download</string>
<string name="migration_from_device_try_again">You can give another try.</string>
<string name="migration_from_device_importing_archive">Importing archive</string>
<string name="migration_from_device_import_failed">Import failed</string>
<string name="migration_from_device_repeat_import">Repeat import</string>
<string name="migration_from_device_enter_passphrase">Enter passphrase</string>
<string name="migration_from_device_file_delete_or_link_invalid">File was deleted or link is invalid</string>
<string name="migration_from_device_error_downloading_archive">Error downloading the archive</string>
<string name="migration_from_device_chat_migrated">Chat migrated!</string>
<string name="migration_from_device_finalize_migration">Finalize migration on another device.</string>
<string name="migration_from_device_confirm_network_settings">Confirm network settings</string>
<string name="migration_from_device_confirm_network_settings_footer">Please confirm that network settings are correct for this device.</string>
<string name="migration_from_device_apply_onion">Apply</string>
<string name="migrate_to_device_migrating">Migrating</string>
<string name="migrate_to_device_database_init">Preparing download</string>
<string name="migrate_to_device_downloading_details">Downloading link details</string>
<string name="migrate_to_device_downloading_archive">Downloading archive</string>
<string name="migrate_to_device_bytes_downloaded">%s downloaded</string>
<string name="migrate_to_device_download_failed">Download failed</string>
<string name="migrate_to_device_repeat_download">Repeat download</string>
<string name="migrate_to_device_try_again">You can give another try.</string>
<string name="migrate_to_device_importing_archive">Importing archive</string>
<string name="migrate_to_device_import_failed">Import failed</string>
<string name="migrate_to_device_repeat_import">Repeat import</string>
<string name="migrate_to_device_enter_passphrase">Enter passphrase</string>
<string name="migrate_to_device_file_delete_or_link_invalid">File was deleted or link is invalid</string>
<string name="migrate_to_device_error_downloading_archive">Error downloading the archive</string>
<string name="migrate_to_device_chat_migrated">Chat migrated!</string>
<string name="migrate_to_device_finalize_migration">Finalize migration on another device.</string>
<string name="migrate_to_device_confirm_network_settings">Confirm network settings</string>
<string name="migrate_to_device_confirm_network_settings_footer">Please confirm that network settings are correct for this device.</string>
<string name="migrate_to_device_apply_onion">Apply</string>
<!-- MigrateToAnotherDevice.kt -->
<string name="migrate_to_device">Migrate to another device</string>
<string name="migrate_to_device_error_saving_settings">Error saving settings</string>
<string name="migrate_to_device_exported_file_doesnt_exist">Exported file doesn\'t exist</string>
<string name="migrate_to_device_error_exporting_archive">Error exporting chat database</string>
<string name="migration_to_device_database_init">Preparing upload</string>
<string name="migration_to_device_error_uploading_archive">Error uploading the archive</string>
<string name="migration_to_device_error_deleting_database">Error deleting database</string>
<string name="migration_to_device_stopping_chat">Stopping chat</string>
<string name="migration_to_device_chat_should_be_stopped">In order to continue, chat should be stopped.</string>
<string name="migration_to_device_archive_and_upload">Archive and upload</string>
<string name="migration_to_device_confirm_upload">Confirm upload</string>
<string name="migration_to_device_all_data_will_be_uploaded">All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</string>
<string name="migration_to_device_archiving_database">Archiving database</string>
<string name="migration_to_device_bytes_uploaded">%s uploaded</string>
<string name="migration_to_device_uploading_archive">Uploading archive</string>
<string name="migration_to_device_upload_failed">Upload failed</string>
<string name="migration_to_device_repeat_upload">Repeat upload</string>
<string name="migration_to_device_try_again">You can give another try.</string>
<string name="migration_to_device_creating_archive_link">Creating archive link</string>
<string name="migration_to_device_cancel_migration">Cancel migration</string>
<string name="migration_to_device_finalize_migration">Finalize migration</string>
<string name="migration_to_device_choose_migrate_from_another_device"><![CDATA[Choose <i>Migrate from another device</i> on the new device and scan QR code.]]></string>
<string name="migration_to_device_or_share_this_file_link">Or securely share this file link</string>
<string name="migration_to_device_delete_database_from_device">Delete database from this device</string>
<string name="migration_to_device_starting_chat_on_multiple_devices_unsupported">Warning: starting chat on multiple devices is not supported and will cause message delivery failures</string>
<string name="migration_to_device_start_chat">Start chat</string>
<string name="migration_to_device_migration_complete">Migration complete</string>
<string name="migration_to_device_you_must_not_start_database_on_two_device"><![CDATA[You <b>must not</b> use the same database on two devices.]]></string>
<string name="migration_to_device_using_on_two_device_breaks_encryption"><![CDATA[<b>Please note</b>: using the same database on two devices will break the decryption of messages from your connections, as a security protection.]]></string>
<string name="migration_to_device_verify_database_passphrase">Verify database passphrase</string>
<string name="migration_to_device_verify_passphrase">Verify passphrase</string>
<string name="migration_to_device_confirm_you_remember_passphrase">Confirm that you remember database passphrase to migrate it.</string>
<!-- MigrateFromDevice.kt -->
<string name="migrate_from_device_title">Migrate device</string>
<string name="migrate_from_device_to_another_device">Migrate to another device</string>
<string name="migrate_from_device_error_saving_settings">Error saving settings</string>
<string name="migrate_from_device_exported_file_doesnt_exist">Exported file doesn\'t exist</string>
<string name="migrate_from_device_error_exporting_archive">Error exporting chat database</string>
<string name="migrate_from_device_database_init">Preparing upload</string>
<string name="migrate_from_device_error_uploading_archive">Error uploading the archive</string>
<string name="migrate_from_device_error_deleting_database">Error deleting database</string>
<string name="migrate_from_device_stopping_chat">Stopping chat</string>
<string name="migrate_from_device_chat_should_be_stopped">In order to continue, chat should be stopped.</string>
<string name="migrate_from_device_archive_and_upload">Archive and upload</string>
<string name="migrate_from_device_confirm_upload">Confirm upload</string>
<string name="migrate_from_device_all_data_will_be_uploaded">All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</string>
<string name="migrate_from_device_archiving_database">Archiving database</string>
<string name="migrate_from_device_bytes_uploaded">%s uploaded</string>
<string name="migrate_from_device_uploading_archive">Uploading archive</string>
<string name="migrate_from_device_upload_failed">Upload failed</string>
<string name="migrate_from_device_repeat_upload">Repeat upload</string>
<string name="migrate_from_device_try_again">You can give another try.</string>
<string name="migrate_from_device_creating_archive_link">Creating archive link</string>
<string name="migrate_from_device_cancel_migration">Cancel migration</string>
<string name="migrate_from_device_finalize_migration">Finalize migration</string>
<string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[Choose <i>Migrate from another device</i> on the new device and scan QR code.]]></string>
<string name="migrate_from_device_or_share_this_file_link">Or securely share this file link</string>
<string name="migrate_from_device_delete_database_from_device">Delete database from this device</string>
<string name="migrate_from_device_starting_chat_on_multiple_devices_unsupported">Warning: starting chat on multiple devices is not supported and will cause message delivery failures</string>
<string name="migrate_from_device_start_chat">Start chat</string>
<string name="migrate_from_device_migration_complete">Migration complete</string>
<string name="migrate_from_device_you_must_not_start_database_on_two_device"><![CDATA[You <b>must not</b> use the same database on two devices.]]></string>
<string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>Please note</b>: using the same database on two devices will break the decryption of messages from your connections, as a security protection.]]></string>
<string name="migrate_from_device_verify_database_passphrase">Verify database passphrase</string>
<string name="migrate_from_device_verify_passphrase">Verify passphrase</string>
<string name="migrate_from_device_confirm_you_remember_passphrase">Confirm that you remember database passphrase to migrate it.</string>
<string name="migrate_from_device_check_connection_and_try_again">Check your internet connection and try again</string>
<string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Warning</b>: the archive will be deleted.]]></string>
<string name="migrate_from_device_error_verifying_passphrase">Error verifying passphrase:</string>
</resources>