mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
android, desktop: migration enhancements (#3901)
* adapted iOS logic * rename everything * title * title * text * rename * rename * alert on strange error
This commit is contained in:
committed by
GitHub
parent
7fa2f2f72e
commit
e75be71d9a
+3
-2
@@ -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?)
|
||||
|
||||
+8
-4
@@ -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)
|
||||
|
||||
+144
-120
@@ -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,
|
||||
+166
-150
@@ -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,
|
||||
+7
-5
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-3
@@ -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>
|
||||
Reference in New Issue
Block a user