mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-29 03:18:37 +00:00
multiplatform: migration via link (#3854)
* multiplatform: migration via link * onion screen * unused code * changes * migrate from device * changes * don't allow going back on Archiving step * changes * correction * correction * change * font * changes * changes * changes * show NEVER text for onion when socks is disabled * onion setup * no check --------- Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
This commit is contained in:
co-authored by
spaced4ndy
parent
a56bc6760b
commit
c9df591e52
@@ -1,10 +1,16 @@
|
||||
package chat.simplex.app
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.*
|
||||
import android.content.Context
|
||||
import chat.simplex.common.platform.Log
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.media.AudioManager
|
||||
import android.os.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.lifecycle.*
|
||||
import androidx.work.*
|
||||
import chat.simplex.app.model.NtfManager
|
||||
@@ -18,8 +24,7 @@ import chat.simplex.common.model.ChatModel.updatingChatsMutex
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.CurrentColors
|
||||
import chat.simplex.common.ui.theme.DefaultTheme
|
||||
import chat.simplex.common.views.call.RcvCallInvitation
|
||||
import chat.simplex.common.views.call.activeCallDestroyWebView
|
||||
import chat.simplex.common.views.call.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.onboarding.OnboardingStage
|
||||
import com.jakewharton.processphoenix.ProcessPhoenix
|
||||
@@ -65,7 +70,11 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
tmpDir.deleteRecursively()
|
||||
tmpDir.mkdir()
|
||||
|
||||
if (DatabaseUtils.ksSelfDestructPassword.get() == null) {
|
||||
// Present screen for continue migration if it wasn't finished yet
|
||||
if (chatModel.migrationState.value != null) {
|
||||
// It's important, otherwise, user may be locked in undefined state
|
||||
appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
|
||||
} else if (DatabaseUtils.ksAppPassword.get() == null || DatabaseUtils.ksSelfDestructPassword.get() == null) {
|
||||
initChatControllerAndRunMigrations()
|
||||
}
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this@SimplexApp)
|
||||
@@ -282,6 +291,21 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
activeCallDestroyWebView()
|
||||
}
|
||||
|
||||
@SuppressLint("SourceLockedOrientationActivity")
|
||||
@Composable
|
||||
override fun androidLockPortraitOrientation() {
|
||||
val context = LocalContext.current
|
||||
DisposableEffect(Unit) {
|
||||
val activity = context as? Activity ?: return@DisposableEffect onDispose {}
|
||||
// Lock orientation to portrait in order to have good experience with calls
|
||||
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
onDispose {
|
||||
// Unlock orientation
|
||||
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun androidAskToAllowBackgroundCalls(): Boolean {
|
||||
if (SimplexService.isBackgroundRestricted()) {
|
||||
val userChoice: CompletableDeferred<Boolean> = CompletableDeferred()
|
||||
|
||||
+11
-4
@@ -2,6 +2,7 @@ package chat.simplex.common.views.database
|
||||
|
||||
import SectionItemView
|
||||
import SectionTextFooter
|
||||
import TextIconSpaced
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -22,8 +23,9 @@ actual fun SavePassphraseSetting(
|
||||
useKeychain: Boolean,
|
||||
initialRandomDBPassphrase: Boolean,
|
||||
storedKey: Boolean,
|
||||
progressIndicator: Boolean,
|
||||
minHeight: Dp,
|
||||
enabled: Boolean,
|
||||
smallPadding: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
) {
|
||||
SectionItemView(minHeight = minHeight) {
|
||||
@@ -33,7 +35,11 @@ actual fun SavePassphraseSetting(
|
||||
stringResource(MR.strings.save_passphrase_in_keychain),
|
||||
tint = if (storedKey) SimplexGreen else MaterialTheme.colors.secondary
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
if (smallPadding) {
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
} else {
|
||||
TextIconSpaced(false)
|
||||
}
|
||||
Text(
|
||||
stringResource(MR.strings.save_passphrase_in_keychain),
|
||||
Modifier.padding(end = 24.dp),
|
||||
@@ -43,7 +49,7 @@ actual fun SavePassphraseSetting(
|
||||
DefaultSwitch(
|
||||
checked = useKeychain,
|
||||
onCheckedChange = onCheckedChange,
|
||||
enabled = !initialRandomDBPassphrase && !progressIndicator
|
||||
enabled = enabled
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -55,13 +61,14 @@ actual fun DatabaseEncryptionFooter(
|
||||
chatDbEncrypted: Boolean?,
|
||||
storedKey: MutableState<Boolean>,
|
||||
initialRandomDBPassphrase: MutableState<Boolean>,
|
||||
migration: Boolean,
|
||||
) {
|
||||
if (chatDbEncrypted == false) {
|
||||
SectionTextFooter(generalGetString(MR.strings.database_is_not_encrypted))
|
||||
} else if (useKeychain.value) {
|
||||
if (storedKey.value) {
|
||||
SectionTextFooter(generalGetString(MR.strings.keychain_is_storing_securely))
|
||||
if (initialRandomDBPassphrase.value) {
|
||||
if (initialRandomDBPassphrase.value && !migration) {
|
||||
SectionTextFooter(generalGetString(MR.strings.encrypted_with_random_passphrase))
|
||||
} else {
|
||||
SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase))
|
||||
|
||||
@@ -110,6 +110,13 @@ fun MainScreen() {
|
||||
val localUserCreated = chatModel.localUserCreated.value
|
||||
var showInitializationView by remember { mutableStateOf(false) }
|
||||
when {
|
||||
onboarding == OnboardingStage.Step1_SimpleXInfo && chatModel.migrationState.value != null -> {
|
||||
// In migration process. Nothing should interrupt it, that's why it's the first branch in when()
|
||||
SimpleXInfo(chatModel, onboarding = true)
|
||||
if (appPlatform.isDesktop) {
|
||||
ModalManager.fullscreen.showInView()
|
||||
}
|
||||
}
|
||||
chatModel.dbMigrationInProgress.value -> DefaultProgressView(stringResource(MR.strings.database_migration_in_progress))
|
||||
chatModel.chatDbStatus.value == null && showInitializationView -> DefaultProgressView(stringResource(MR.strings.opening_database))
|
||||
showChatDatabaseError -> {
|
||||
|
||||
+12
-2
@@ -13,6 +13,7 @@ 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.res.MR
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
@@ -104,6 +105,8 @@ object ChatModel {
|
||||
// currently showing invitation
|
||||
val showingInvitation = mutableStateOf(null as ShowingInvitation?)
|
||||
|
||||
val migrationState: MutableState<MigrationFromAnotherDeviceState?> by lazy { mutableStateOf(MigrationFromAnotherDeviceState.transform()) }
|
||||
|
||||
var draft = mutableStateOf(null as ComposeState?)
|
||||
var draftChatId = mutableStateOf(null as String?)
|
||||
|
||||
@@ -2973,10 +2976,17 @@ enum class FormatColor(val color: String) {
|
||||
class SndFileTransfer() {}
|
||||
|
||||
@Serializable
|
||||
class RcvFileTransfer() {}
|
||||
data class RcvFileTransfer(
|
||||
val fileId: Long,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class FileTransferMeta() {}
|
||||
data class FileTransferMeta(
|
||||
val fileId: Long,
|
||||
val fileName: String,
|
||||
val filePath: String,
|
||||
val fileSize: Long,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
enum class CICallStatus {
|
||||
|
||||
+391
-60
@@ -4,12 +4,15 @@ import chat.simplex.common.views.helpers.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import chat.simplex.common.model.ChatController.getNetCfg
|
||||
import chat.simplex.common.model.ChatController.setNetCfg
|
||||
import chat.simplex.common.model.ChatModel.updatingChatsMutex
|
||||
import chat.simplex.common.model.ChatModel.changingActiveUserMutex
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.call.*
|
||||
import chat.simplex.common.views.migration.MigrationFileLinkData
|
||||
import chat.simplex.common.views.onboarding.OnboardingStage
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
import com.charleskorn.kaml.Yaml
|
||||
@@ -144,6 +147,7 @@ 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 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)
|
||||
@@ -177,6 +181,11 @@ class AppPreferences {
|
||||
val offerRemoteMulticast = mkBoolPreference(SHARED_PREFS_OFFER_REMOTE_MULTICAST, true)
|
||||
|
||||
val desktopWindowState = mkStrPreference(SHARED_PREFS_DESKTOP_WINDOW_STATE, null)
|
||||
|
||||
|
||||
val iosCallKitEnabled = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_ENABLED, true)
|
||||
val iosCallKitCallsInRecents = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS, false)
|
||||
|
||||
|
||||
private fun mkIntPreference(prefName: String, default: Int) =
|
||||
SharedPreference(
|
||||
@@ -277,6 +286,7 @@ 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"
|
||||
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"
|
||||
@@ -326,6 +336,9 @@ class AppPreferences {
|
||||
private const val SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "ConnectRemoteViaMulticastAuto"
|
||||
private const val SHARED_PREFS_OFFER_REMOTE_MULTICAST = "OfferRemoteMulticast"
|
||||
private const val SHARED_PREFS_DESKTOP_WINDOW_STATE = "DesktopWindowState"
|
||||
|
||||
private const val SHARED_PREFS_IOS_CALL_KIT_ENABLED = "iOSCallKitEnabled"
|
||||
private const val SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS = "iOSCallKitCallsInRecents"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,6 +415,16 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun startChatWithTemporaryDatabase(ctrl: ChatCtrl, netCfg: NetCfg): User? {
|
||||
Log.d(TAG, "startChatWithTemporaryDatabase")
|
||||
val migrationActiveUser = apiGetActiveUser(null, ctrl) ?: apiCreateActiveUser(null, Profile(displayName = "Temp", fullName = ""), ctrl = ctrl)
|
||||
apiSetNetworkConfig(netCfg, ctrl)
|
||||
apiSetTempFolder(getMigrationTempFilesDirectory().absolutePath, ctrl)
|
||||
apiSetFilesFolder(getMigrationTempFilesDirectory().absolutePath, ctrl)
|
||||
apiStartChat(ctrl)
|
||||
return migrationActiveUser
|
||||
}
|
||||
|
||||
suspend fun changeActiveUser(rhId: Long?, toUserId: Long, viewPwd: String?) {
|
||||
try {
|
||||
changeActiveUser_(rhId, toUserId, viewPwd)
|
||||
@@ -478,8 +501,8 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendCmd(rhId: Long?, cmd: CC): CR {
|
||||
val ctrl = ctrl ?: throw Exception("Controller is not initialized")
|
||||
suspend fun sendCmd(rhId: Long?, cmd: CC, otherCtrl: ChatCtrl? = null): CR {
|
||||
val ctrl = otherCtrl ?: ctrl ?: throw Exception("Controller is not initialized")
|
||||
|
||||
return withContext(Dispatchers.IO) {
|
||||
val c = cmd.cmdString
|
||||
@@ -496,7 +519,7 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
private fun recvMsg(ctrl: ChatCtrl): APIResponse? {
|
||||
fun recvMsg(ctrl: ChatCtrl): APIResponse? {
|
||||
val json = chatRecvMsgWait(ctrl, MESSAGE_TIMEOUT)
|
||||
return if (json == "") {
|
||||
null
|
||||
@@ -509,8 +532,8 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiGetActiveUser(rh: Long?): User? {
|
||||
val r = sendCmd(rh, CC.ShowActiveUser())
|
||||
suspend fun apiGetActiveUser(rh: Long?, ctrl: ChatCtrl? = null): User? {
|
||||
val r = sendCmd(rh, CC.ShowActiveUser(), ctrl)
|
||||
if (r is CR.ActiveUser) return r.user.updateRemoteHostId(rh)
|
||||
Log.d(TAG, "apiGetActiveUser: ${r.responseType} ${r.details}")
|
||||
if (rh == null) {
|
||||
@@ -519,8 +542,8 @@ object ChatController {
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiCreateActiveUser(rh: Long?, p: Profile?, sameServers: Boolean = false, pastTimestamp: Boolean = false): User? {
|
||||
val r = sendCmd(rh, CC.CreateActiveUser(p, sameServers = sameServers, pastTimestamp = pastTimestamp))
|
||||
suspend fun apiCreateActiveUser(rh: Long?, p: Profile?, sameServers: Boolean = false, pastTimestamp: Boolean = false, ctrl: ChatCtrl? = null): User? {
|
||||
val r = sendCmd(rh, CC.CreateActiveUser(p, sameServers = sameServers, pastTimestamp = pastTimestamp), ctrl)
|
||||
if (r is CR.ActiveUser) return r.user.updateRemoteHostId(rh)
|
||||
else if (
|
||||
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore && r.chatError.storeError is StoreError.DuplicateName ||
|
||||
@@ -598,8 +621,8 @@ object ChatController {
|
||||
throw Exception("failed to delete the user ${r.responseType} ${r.details}")
|
||||
}
|
||||
|
||||
suspend fun apiStartChat(): Boolean {
|
||||
val r = sendCmd(null, CC.StartChat(mainApp = true))
|
||||
suspend fun apiStartChat(ctrl: ChatCtrl? = null): Boolean {
|
||||
val r = sendCmd(null, CC.StartChat(mainApp = true), ctrl)
|
||||
when (r) {
|
||||
is CR.ChatStarted -> return true
|
||||
is CR.ChatRunning -> return false
|
||||
@@ -615,14 +638,14 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiSetTempFolder(tempFolder: String) {
|
||||
val r = sendCmd(null, CC.SetTempFolder(tempFolder))
|
||||
suspend fun apiSetTempFolder(tempFolder: String, ctrl: ChatCtrl? = null) {
|
||||
val r = sendCmd(null, CC.SetTempFolder(tempFolder), ctrl)
|
||||
if (r is CR.CmdOk) return
|
||||
throw Error("failed to set temp folder: ${r.responseType} ${r.details}")
|
||||
}
|
||||
|
||||
suspend fun apiSetFilesFolder(filesFolder: String) {
|
||||
val r = sendCmd(null, CC.SetFilesFolder(filesFolder))
|
||||
suspend fun apiSetFilesFolder(filesFolder: String, ctrl: ChatCtrl? = null) {
|
||||
val r = sendCmd(null, CC.SetFilesFolder(filesFolder), ctrl)
|
||||
if (r is CR.CmdOk) return
|
||||
throw Error("failed to set files folder: ${r.responseType} ${r.details}")
|
||||
}
|
||||
@@ -635,6 +658,18 @@ object ChatController {
|
||||
|
||||
suspend fun apiSetEncryptLocalFiles(enable: Boolean) = sendCommandOkResp(null, CC.ApiSetEncryptLocalFiles(enable))
|
||||
|
||||
suspend fun apiSaveAppSettings(settings: AppSettings) {
|
||||
val r = sendCmd(null, CC.ApiSaveSettings(settings))
|
||||
if (r is CR.CmdOk) return
|
||||
throw Error("failed to set app settings: ${r.responseType} ${r.details}")
|
||||
}
|
||||
|
||||
suspend fun apiGetAppSettings(settings: AppSettings): AppSettings {
|
||||
val r = sendCmd(null, CC.ApiGetSettings(settings))
|
||||
if (r is CR.AppSettingsR) return r.appSettings
|
||||
throw Error("failed to get app settings: ${r.responseType} ${r.details}")
|
||||
}
|
||||
|
||||
suspend fun apiSetPQEncryption(enable: Boolean) = sendCommandOkResp(null, CC.ApiSetPQEncryption(enable))
|
||||
|
||||
suspend fun apiSetContactPQ(rh: Long?, contactId: Long, enable: Boolean): Contact? {
|
||||
@@ -669,6 +704,11 @@ object ChatController {
|
||||
throw Exception("failed to set storage encryption: ${r.responseType} ${r.details}")
|
||||
}
|
||||
|
||||
suspend fun testStorageEncryption(key: String, ctrl: ChatCtrl? = null): Boolean {
|
||||
val r = sendCmd(null, CC.TestStorageEncryption(key), ctrl)
|
||||
return r is CR.CmdOk
|
||||
}
|
||||
|
||||
suspend fun apiGetChats(rh: Long?): List<Chat> {
|
||||
val userId = kotlin.runCatching { currentUserId("apiGetChats") }.getOrElse { return emptyList() }
|
||||
val r = sendCmd(rh, CC.ApiGetChats(userId))
|
||||
@@ -805,8 +845,8 @@ object ChatController {
|
||||
throw Exception("failed to set chat item TTL: ${r.responseType} ${r.details}")
|
||||
}
|
||||
|
||||
suspend fun apiSetNetworkConfig(cfg: NetCfg): Boolean {
|
||||
val r = sendCmd(null, CC.APISetNetworkConfig(cfg))
|
||||
suspend fun apiSetNetworkConfig(cfg: NetCfg, ctrl: ChatCtrl? = null): Boolean {
|
||||
val r = sendCmd(null, CC.APISetNetworkConfig(cfg), ctrl)
|
||||
return when (r) {
|
||||
is CR.CmdOk -> true
|
||||
else -> {
|
||||
@@ -1236,6 +1276,36 @@ object ChatController {
|
||||
return false
|
||||
}
|
||||
|
||||
suspend fun uploadStandaloneFile(user: UserLike, file: CryptoFile, ctrl: ChatCtrl? = null): Pair<FileTransferMeta?, String?> {
|
||||
val r = sendCmd(null, CC.ApiUploadStandaloneFile(user.userId, file), ctrl)
|
||||
return if (r is CR.SndStandaloneFileCreated) {
|
||||
r.fileTransferMeta to null
|
||||
} else {
|
||||
Log.e(TAG, "uploadStandaloneFile error: $r")
|
||||
null to r.toString()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun downloadStandaloneFile(user: UserLike, url: String, file: CryptoFile, ctrl: ChatCtrl? = null): Pair<RcvFileTransfer?, String?> {
|
||||
val r = sendCmd(null, CC.ApiDownloadStandaloneFile(user.userId, url, file), ctrl)
|
||||
return if (r is CR.RcvStandaloneFileCreated) {
|
||||
r.rcvFileTransfer to null
|
||||
} else {
|
||||
Log.e(TAG, "downloadStandaloneFile error: $r")
|
||||
null to r.toString()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun standaloneFileInfo(url: String, ctrl: ChatCtrl? = null): MigrationFileLinkData? {
|
||||
val r = sendCmd(null, CC.ApiStandaloneFileInfo(url), ctrl)
|
||||
return if (r is CR.StandaloneFileInfo) {
|
||||
r.fileMeta
|
||||
} else {
|
||||
Log.e(TAG, "standaloneFileInfo error: $r")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiReceiveFile(rh: Long?, fileId: Long, encrypted: Boolean, inline: Boolean? = null, auto: Boolean = false): AChatItem? {
|
||||
// -1 here is to override default behavior of providing current remote host id because file can be asked by local device while remote is connected
|
||||
val r = sendCmd(rh, CC.ReceiveFile(fileId, encrypted, inline))
|
||||
@@ -1274,11 +1344,11 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiCancelFile(rh: Long?, fileId: Long): AChatItem? {
|
||||
val r = sendCmd(rh, CC.CancelFile(fileId))
|
||||
suspend fun apiCancelFile(rh: Long?, fileId: Long, ctrl: ChatCtrl? = null): AChatItem? {
|
||||
val r = sendCmd(rh, CC.CancelFile(fileId), ctrl)
|
||||
return when (r) {
|
||||
is CR.SndFileCancelled -> r.chatItem
|
||||
is CR.RcvFileCancelled -> r.chatItem
|
||||
is CR.SndFileCancelled -> r.chatItem_
|
||||
is CR.RcvFileCancelled -> r.chatItem_
|
||||
else -> {
|
||||
Log.d(TAG, "apiCancelFile bad response: ${r.responseType} ${r.details}")
|
||||
null
|
||||
@@ -1565,8 +1635,8 @@ object ChatController {
|
||||
|
||||
suspend fun deleteRemoteCtrl(rcId: Long): Boolean = sendCommandOkResp(null, CC.DeleteRemoteCtrl(rcId))
|
||||
|
||||
private suspend fun sendCommandOkResp(rh: Long?, cmd: CC): Boolean {
|
||||
val r = sendCmd(rh, cmd)
|
||||
private suspend fun sendCommandOkResp(rh: Long?, cmd: CC, ctrl: ChatCtrl? = null): Boolean {
|
||||
val r = sendCmd(rh, cmd, ctrl)
|
||||
val ok = r is CR.CmdOk
|
||||
if (!ok) apiErrorAlert(cmd.cmdType, generalGetString(MR.strings.error_alert_title), r)
|
||||
return ok
|
||||
@@ -1856,11 +1926,16 @@ object ChatController {
|
||||
chatItemSimpleUpdate(rhId, r.user, r.chatItem)
|
||||
cleanupFile(r.chatItem)
|
||||
}
|
||||
is CR.RcvFileProgressXFTP ->
|
||||
chatItemSimpleUpdate(rhId, r.user, r.chatItem)
|
||||
is CR.RcvFileProgressXFTP -> {
|
||||
if (r.chatItem_ != null) {
|
||||
chatItemSimpleUpdate(rhId, r.user, r.chatItem_)
|
||||
}
|
||||
}
|
||||
is CR.RcvFileError -> {
|
||||
chatItemSimpleUpdate(rhId, r.user, r.chatItem)
|
||||
cleanupFile(r.chatItem)
|
||||
if (r.chatItem_ != null) {
|
||||
chatItemSimpleUpdate(rhId, r.user, r.chatItem_)
|
||||
cleanupFile(r.chatItem_)
|
||||
}
|
||||
}
|
||||
is CR.SndFileStart ->
|
||||
chatItemSimpleUpdate(rhId, r.user, r.chatItem)
|
||||
@@ -1869,18 +1944,25 @@ object ChatController {
|
||||
cleanupDirectFile(r.chatItem)
|
||||
}
|
||||
is CR.SndFileRcvCancelled -> {
|
||||
chatItemSimpleUpdate(rhId, r.user, r.chatItem)
|
||||
cleanupDirectFile(r.chatItem)
|
||||
if (r.chatItem_ != null) {
|
||||
chatItemSimpleUpdate(rhId, r.user, r.chatItem_)
|
||||
cleanupDirectFile(r.chatItem_)
|
||||
}
|
||||
}
|
||||
is CR.SndFileProgressXFTP -> {
|
||||
if (r.chatItem_ != null) {
|
||||
chatItemSimpleUpdate(rhId, r.user, r.chatItem_)
|
||||
}
|
||||
}
|
||||
is CR.SndFileProgressXFTP ->
|
||||
chatItemSimpleUpdate(rhId, r.user, r.chatItem)
|
||||
is CR.SndFileCompleteXFTP -> {
|
||||
chatItemSimpleUpdate(rhId, r.user, r.chatItem)
|
||||
cleanupFile(r.chatItem)
|
||||
}
|
||||
is CR.SndFileError -> {
|
||||
chatItemSimpleUpdate(rhId, r.user, r.chatItem)
|
||||
cleanupFile(r.chatItem)
|
||||
if (r.chatItem_ != null) {
|
||||
chatItemSimpleUpdate(rhId, r.user, r.chatItem_)
|
||||
cleanupFile(r.chatItem_)
|
||||
}
|
||||
}
|
||||
is CR.CallInvitation -> {
|
||||
chatModel.callManager.reportNewIncomingCall(r.callInvitation.copy(remoteHostId = rhId))
|
||||
@@ -2249,21 +2331,13 @@ object ChatController {
|
||||
|
||||
class SharedPreference<T>(val get: () -> T, set: (T) -> Unit) {
|
||||
val set: (T) -> Unit
|
||||
private val _state: MutableState<T> by lazy { mutableStateOf(get()) }
|
||||
val state: State<T> by lazy { _state }
|
||||
private val _state: MutableState<T> = mutableStateOf(get())
|
||||
val state: State<T> = _state
|
||||
|
||||
init {
|
||||
this.set = { value ->
|
||||
set(value)
|
||||
try {
|
||||
_state.value = value
|
||||
} catch (e: IllegalStateException) {
|
||||
// Can be `Reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied`
|
||||
Log.i(TAG, e.stackTraceToString())
|
||||
withApi {
|
||||
_state.value = value
|
||||
}
|
||||
}
|
||||
_state.value = value
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2295,6 +2369,9 @@ sealed class CC {
|
||||
class ApiImportArchive(val config: ArchiveConfig): CC()
|
||||
class ApiDeleteStorage: CC()
|
||||
class ApiStorageEncryption(val config: DBEncryptionConfig): CC()
|
||||
class TestStorageEncryption(val key: String): CC()
|
||||
class ApiSaveSettings(val settings: AppSettings): CC()
|
||||
class ApiGetSettings(val settings: AppSettings): CC()
|
||||
class ApiGetChats(val userId: Long): CC()
|
||||
class ApiGetChat(val type: ChatType, val id: Long, val pagination: ChatPagination, val search: String = ""): CC()
|
||||
class ApiGetChatItemInfo(val type: ChatType, val id: Long, val itemId: Long): CC()
|
||||
@@ -2388,6 +2465,9 @@ sealed class CC {
|
||||
class ListRemoteCtrls(): CC()
|
||||
class StopRemoteCtrl(): CC()
|
||||
class DeleteRemoteCtrl(val remoteCtrlId: Long): CC()
|
||||
class ApiUploadStandaloneFile(val userId: Long, val file: CryptoFile): CC()
|
||||
class ApiDownloadStandaloneFile(val userId: Long, val url: String, val file: CryptoFile): CC()
|
||||
class ApiStandaloneFileInfo(val url: String): CC()
|
||||
// misc
|
||||
class ShowVersion(): CC()
|
||||
|
||||
@@ -2426,6 +2506,9 @@ sealed class CC {
|
||||
is ApiImportArchive -> "/_db import ${json.encodeToString(config)}"
|
||||
is ApiDeleteStorage -> "/_db delete"
|
||||
is ApiStorageEncryption -> "/_db encryption ${json.encodeToString(config)}"
|
||||
is TestStorageEncryption -> "/db test key $key"
|
||||
is ApiSaveSettings -> "/_save app settings ${json.encodeToString(settings)}"
|
||||
is ApiGetSettings -> "/_get app settings ${json.encodeToString(settings)}"
|
||||
is ApiGetChats -> "/_get chats $userId pcc=on"
|
||||
is ApiGetChat -> "/_get chat ${chatRef(type, id)} ${pagination.cmdString}" + (if (search == "") "" else " search=$search")
|
||||
is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id)} $itemId"
|
||||
@@ -2533,6 +2616,9 @@ sealed class CC {
|
||||
is ListRemoteCtrls -> "/list remote ctrls"
|
||||
is StopRemoteCtrl -> "/stop remote ctrl"
|
||||
is DeleteRemoteCtrl -> "/delete remote ctrl $remoteCtrlId"
|
||||
is ApiUploadStandaloneFile -> "/_upload $userId ${file.filePath}"
|
||||
is ApiDownloadStandaloneFile -> "/_download $userId $url ${file.filePath}"
|
||||
is ApiStandaloneFileInfo -> "/_download info $url"
|
||||
is ShowVersion -> "/version"
|
||||
}
|
||||
|
||||
@@ -2562,6 +2648,9 @@ sealed class CC {
|
||||
is ApiImportArchive -> "apiImportArchive"
|
||||
is ApiDeleteStorage -> "apiDeleteStorage"
|
||||
is ApiStorageEncryption -> "apiStorageEncryption"
|
||||
is TestStorageEncryption -> "testStorageEncryption"
|
||||
is ApiSaveSettings -> "apiSaveSettings"
|
||||
is ApiGetSettings -> "apiGetSettings"
|
||||
is ApiGetChats -> "apiGetChats"
|
||||
is ApiGetChat -> "apiGetChat"
|
||||
is ApiGetChatItemInfo -> "apiGetChatItemInfo"
|
||||
@@ -2654,6 +2743,9 @@ sealed class CC {
|
||||
is ListRemoteCtrls -> "listRemoteCtrls"
|
||||
is StopRemoteCtrl -> "stopRemoteCtrl"
|
||||
is DeleteRemoteCtrl -> "deleteRemoteCtrl"
|
||||
is ApiUploadStandaloneFile -> "apiUploadStandaloneFile"
|
||||
is ApiDownloadStandaloneFile -> "apiDownloadStandaloneFile"
|
||||
is ApiStandaloneFileInfo -> "apiStandaloneFileInfo"
|
||||
is ShowVersion -> "showVersion"
|
||||
}
|
||||
|
||||
@@ -2671,6 +2763,7 @@ sealed class CC {
|
||||
is ApiHideUser -> ApiHideUser(userId, obfuscate(viewPwd))
|
||||
is ApiUnhideUser -> ApiUnhideUser(userId, obfuscate(viewPwd))
|
||||
is ApiDeleteUser -> ApiDeleteUser(userId, delSMPQueues, obfuscateOrNull(viewPwd))
|
||||
is TestStorageEncryption -> TestStorageEncryption(obfuscate(key))
|
||||
else -> this
|
||||
}
|
||||
|
||||
@@ -3796,6 +3889,13 @@ val json = Json {
|
||||
explicitNulls = false
|
||||
}
|
||||
|
||||
val jsonShort = Json {
|
||||
prettyPrint = false
|
||||
ignoreUnknownKeys = true
|
||||
encodeDefaults = true
|
||||
explicitNulls = false
|
||||
}
|
||||
|
||||
val yaml = Yaml(configuration = YamlConfiguration(
|
||||
strictMode = false,
|
||||
encodeDefaults = false,
|
||||
@@ -3985,20 +4085,28 @@ sealed class CR {
|
||||
// receiving file events
|
||||
@Serializable @SerialName("rcvFileAccepted") class RcvFileAccepted(val user: UserRef, val chatItem: AChatItem): CR()
|
||||
@Serializable @SerialName("rcvFileAcceptedSndCancelled") class RcvFileAcceptedSndCancelled(val user: UserRef, val rcvFileTransfer: RcvFileTransfer): CR()
|
||||
@Serializable @SerialName("rcvFileStart") class RcvFileStart(val user: UserRef, val chatItem: AChatItem): CR()
|
||||
@Serializable @SerialName("standaloneFileInfo") class StandaloneFileInfo(val fileMeta: MigrationFileLinkData?): CR()
|
||||
@Serializable @SerialName("rcvStandaloneFileCreated") class RcvStandaloneFileCreated(val user: UserRef, val rcvFileTransfer: RcvFileTransfer): CR()
|
||||
@Serializable @SerialName("rcvFileStart") class RcvFileStart(val user: UserRef, val chatItem: AChatItem): CR() // send by chats
|
||||
@Serializable @SerialName("rcvFileProgressXFTP") class RcvFileProgressXFTP(val user: UserRef, val chatItem_: AChatItem?, val receivedSize: Long, val totalSize: Long, val rcvFileTransfer: RcvFileTransfer): CR()
|
||||
@Serializable @SerialName("rcvFileComplete") class RcvFileComplete(val user: UserRef, val chatItem: AChatItem): CR()
|
||||
@Serializable @SerialName("rcvFileCancelled") class RcvFileCancelled(val user: UserRef, val chatItem: AChatItem, val rcvFileTransfer: RcvFileTransfer): CR()
|
||||
@Serializable @SerialName("rcvStandaloneFileComplete") class RcvStandaloneFileComplete(val user: UserRef, val targetPath: String, val rcvFileTransfer: RcvFileTransfer): CR()
|
||||
@Serializable @SerialName("rcvFileCancelled") class RcvFileCancelled(val user: UserRef, val chatItem_: AChatItem?, val rcvFileTransfer: RcvFileTransfer): CR()
|
||||
@Serializable @SerialName("rcvFileSndCancelled") class RcvFileSndCancelled(val user: UserRef, val chatItem: AChatItem, val rcvFileTransfer: RcvFileTransfer): CR()
|
||||
@Serializable @SerialName("rcvFileProgressXFTP") class RcvFileProgressXFTP(val user: UserRef, val chatItem: AChatItem, val receivedSize: Long, val totalSize: Long): CR()
|
||||
@Serializable @SerialName("rcvFileError") class RcvFileError(val user: UserRef, val chatItem: AChatItem): CR()
|
||||
@Serializable @SerialName("rcvFileError") class RcvFileError(val user: UserRef, val chatItem_: AChatItem?, val rcvFileTransfer: RcvFileTransfer): CR()
|
||||
// sending file events
|
||||
@Serializable @SerialName("sndFileStart") class SndFileStart(val user: UserRef, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
|
||||
@Serializable @SerialName("sndFileComplete") class SndFileComplete(val user: UserRef, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
|
||||
@Serializable @SerialName("sndFileCancelled") class SndFileCancelled(val user: UserRef, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta, val sndFileTransfers: List<SndFileTransfer>): CR()
|
||||
@Serializable @SerialName("sndFileRcvCancelled") class SndFileRcvCancelled(val user: UserRef, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
|
||||
@Serializable @SerialName("sndFileProgressXFTP") class SndFileProgressXFTP(val user: UserRef, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta, val sentSize: Long, val totalSize: Long): CR()
|
||||
@Serializable @SerialName("sndFileRcvCancelled") class SndFileRcvCancelled(val user: UserRef, val chatItem_: AChatItem?, val sndFileTransfer: SndFileTransfer): CR()
|
||||
@Serializable @SerialName("sndFileCancelled") class SndFileCancelled(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta, val sndFileTransfers: List<SndFileTransfer>): CR()
|
||||
@Serializable @SerialName("sndStandaloneFileCreated") class SndStandaloneFileCreated(val user: UserRef, val fileTransferMeta: FileTransferMeta): CR() // returned by _upload
|
||||
@Serializable @SerialName("sndFileStartXFTP") class SndFileStartXFTP(val user: UserRef, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta): CR() // not used
|
||||
@Serializable @SerialName("sndFileProgressXFTP") class SndFileProgressXFTP(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta, val sentSize: Long, val totalSize: Long): CR()
|
||||
@Serializable @SerialName("sndFileRedirectStartXFTP") class SndFileRedirectStartXFTP(val user: UserRef, val fileTransferMeta: FileTransferMeta, val redirectMeta: FileTransferMeta): CR()
|
||||
@Serializable @SerialName("sndFileCompleteXFTP") class SndFileCompleteXFTP(val user: UserRef, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta): CR()
|
||||
@Serializable @SerialName("sndFileError") class SndFileError(val user: UserRef, val chatItem: AChatItem): CR()
|
||||
@Serializable @SerialName("sndStandaloneFileComplete") class SndStandaloneFileComplete(val user: UserRef, val fileTransferMeta: FileTransferMeta, val rcvURIs: List<String>): CR()
|
||||
@Serializable @SerialName("sndFileCancelledXFTP") class SndFileCancelledXFTP(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta): CR()
|
||||
@Serializable @SerialName("sndFileError") class SndFileError(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta): CR()
|
||||
// call events
|
||||
@Serializable @SerialName("callInvitation") class CallInvitation(val callInvitation: RcvCallInvitation): CR()
|
||||
@Serializable @SerialName("callInvitations") class CallInvitations(val callInvitations: List<RcvCallInvitation>): CR()
|
||||
@@ -4032,6 +4140,7 @@ sealed class CR {
|
||||
@Serializable @SerialName("chatCmdError") class ChatCmdError(val user_: UserRef?, val chatError: ChatError): CR()
|
||||
@Serializable @SerialName("chatError") class ChatRespError(val user_: UserRef?, val chatError: ChatError): CR()
|
||||
@Serializable @SerialName("archiveImported") class ArchiveImported(val archiveErrors: List<ArchiveError>): CR()
|
||||
@Serializable @SerialName("appSettings") class AppSettingsR(val appSettings: AppSettings): CR()
|
||||
// general
|
||||
@Serializable class Response(val type: String, val json: String): CR()
|
||||
@Serializable class Invalid(val str: String): CR()
|
||||
@@ -4141,19 +4250,27 @@ sealed class CR {
|
||||
is NewMemberContactSentInv -> "newMemberContactSentInv"
|
||||
is NewMemberContactReceivedInv -> "newMemberContactReceivedInv"
|
||||
is RcvFileAcceptedSndCancelled -> "rcvFileAcceptedSndCancelled"
|
||||
is StandaloneFileInfo -> "standaloneFileInfo"
|
||||
is RcvStandaloneFileCreated -> "rcvStandaloneFileCreated"
|
||||
is RcvFileAccepted -> "rcvFileAccepted"
|
||||
is RcvFileStart -> "rcvFileStart"
|
||||
is RcvFileComplete -> "rcvFileComplete"
|
||||
is RcvStandaloneFileComplete -> "rcvStandaloneFileComplete"
|
||||
is RcvFileCancelled -> "rcvFileCancelled"
|
||||
is SndStandaloneFileCreated -> "sndStandaloneFileCreated"
|
||||
is SndFileStartXFTP -> "sndFileStartXFTP"
|
||||
is RcvFileSndCancelled -> "rcvFileSndCancelled"
|
||||
is RcvFileProgressXFTP -> "rcvFileProgressXFTP"
|
||||
is SndFileRedirectStartXFTP -> "sndFileRedirectStartXFTP"
|
||||
is RcvFileError -> "rcvFileError"
|
||||
is SndFileCancelled -> "sndFileCancelled"
|
||||
is SndFileStart -> "sndFileStart"
|
||||
is SndFileComplete -> "sndFileComplete"
|
||||
is SndFileRcvCancelled -> "sndFileRcvCancelled"
|
||||
is SndFileStart -> "sndFileStart"
|
||||
is SndFileCancelled -> "sndFileCancelled"
|
||||
is SndFileProgressXFTP -> "sndFileProgressXFTP"
|
||||
is SndFileCompleteXFTP -> "sndFileCompleteXFTP"
|
||||
is SndStandaloneFileComplete -> "sndStandaloneFileComplete"
|
||||
is SndFileCancelledXFTP -> "sndFileCancelledXFTP"
|
||||
is SndFileError -> "sndFileError"
|
||||
is CallInvitations -> "callInvitations"
|
||||
is CallInvitation -> "callInvitation"
|
||||
@@ -4183,6 +4300,7 @@ sealed class CR {
|
||||
is ChatCmdError -> "chatCmdError"
|
||||
is ChatRespError -> "chatError"
|
||||
is ArchiveImported -> "archiveImported"
|
||||
is AppSettingsR -> "appSettings"
|
||||
is Response -> "* $type"
|
||||
is Invalid -> "* invalid json"
|
||||
}
|
||||
@@ -4195,7 +4313,7 @@ sealed class CR {
|
||||
is ChatStopped -> noDetails()
|
||||
is ApiChats -> withUser(user, json.encodeToString(chats))
|
||||
is ApiChat -> withUser(user, json.encodeToString(chat))
|
||||
is ApiChatItemInfo -> withUser(user, "chatItem: ${json.encodeToString(AChatItem)}\n${json.encodeToString(chatItemInfo)}")
|
||||
is ApiChatItemInfo -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\n${json.encodeToString(chatItemInfo)}")
|
||||
is UserProtoServers -> withUser(user, "servers: ${json.encodeToString(servers)}")
|
||||
is ServerTestResult -> withUser(user, "server: $testServer\nresult: ${json.encodeToString(testFailure)}")
|
||||
is ChatItemTTL -> withUser(user, json.encodeToString(chatItemTTL))
|
||||
@@ -4292,20 +4410,28 @@ sealed class CR {
|
||||
is NewMemberContactSentInv -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member")
|
||||
is NewMemberContactReceivedInv -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member")
|
||||
is RcvFileAcceptedSndCancelled -> withUser(user, noDetails())
|
||||
is StandaloneFileInfo -> json.encodeToString(fileMeta)
|
||||
is RcvStandaloneFileCreated -> noDetails()
|
||||
is RcvFileAccepted -> withUser(user, json.encodeToString(chatItem))
|
||||
is RcvFileStart -> withUser(user, json.encodeToString(chatItem))
|
||||
is RcvFileComplete -> withUser(user, json.encodeToString(chatItem))
|
||||
is RcvFileCancelled -> withUser(user, json.encodeToString(chatItem))
|
||||
is RcvFileCancelled -> withUser(user, json.encodeToString(chatItem_))
|
||||
is RcvFileSndCancelled -> withUser(user, json.encodeToString(chatItem))
|
||||
is RcvFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\nreceivedSize: $receivedSize\ntotalSize: $totalSize")
|
||||
is RcvFileError -> withUser(user, json.encodeToString(chatItem))
|
||||
is SndFileCancelled -> json.encodeToString(chatItem)
|
||||
is RcvFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem_)}\nreceivedSize: $receivedSize\ntotalSize: $totalSize")
|
||||
is RcvStandaloneFileComplete -> withUser(user, targetPath)
|
||||
is RcvFileError -> withUser(user, json.encodeToString(chatItem_))
|
||||
is SndFileCancelled -> json.encodeToString(chatItem_)
|
||||
is SndStandaloneFileCreated -> noDetails()
|
||||
is SndFileStartXFTP -> withUser(user, json.encodeToString(chatItem))
|
||||
is SndFileComplete -> withUser(user, json.encodeToString(chatItem))
|
||||
is SndFileRcvCancelled -> withUser(user, json.encodeToString(chatItem))
|
||||
is SndFileRcvCancelled -> withUser(user, json.encodeToString(chatItem_))
|
||||
is SndFileStart -> withUser(user, json.encodeToString(chatItem))
|
||||
is SndFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\nsentSize: $sentSize\ntotalSize: $totalSize")
|
||||
is SndFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem_)}\nsentSize: $sentSize\ntotalSize: $totalSize")
|
||||
is SndFileRedirectStartXFTP -> withUser(user, json.encodeToString(redirectMeta))
|
||||
is SndFileCompleteXFTP -> withUser(user, json.encodeToString(chatItem))
|
||||
is SndFileError -> withUser(user, json.encodeToString(chatItem))
|
||||
is SndStandaloneFileComplete -> withUser(user, rcvURIs.size.toString())
|
||||
is SndFileCancelledXFTP -> withUser(user, json.encodeToString(chatItem_))
|
||||
is SndFileError -> withUser(user, json.encodeToString(chatItem_))
|
||||
is CallInvitations -> "callInvitations: ${json.encodeToString(callInvitations)}"
|
||||
is CallInvitation -> "contact: ${callInvitation.contact.id}\ncallType: $callInvitation.callType\nsharedKey: ${callInvitation.sharedKey ?: ""}"
|
||||
is CallOffer -> withUser(user, "contact: ${contact.id}\ncallType: $callType\nsharedKey: ${sharedKey ?: ""}\naskConfirmation: $askConfirmation\noffer: ${json.encodeToString(offer)}")
|
||||
@@ -4351,6 +4477,7 @@ sealed class CR {
|
||||
is ChatCmdError -> withUser(user_, chatError.string)
|
||||
is ChatRespError -> withUser(user_, chatError.string)
|
||||
is ArchiveImported -> "${archiveErrors.map { it.string } }"
|
||||
is AppSettingsR -> json.encodeToString(appSettings)
|
||||
is Response -> json
|
||||
is Invalid -> str
|
||||
}
|
||||
@@ -4764,6 +4891,7 @@ sealed class StoreError {
|
||||
is FileIdNotFoundBySharedMsgId -> "fileIdNotFoundBySharedMsgId"
|
||||
is SndFileNotFoundXFTP -> "sndFileNotFoundXFTP"
|
||||
is RcvFileNotFoundXFTP -> "rcvFileNotFoundXFTP"
|
||||
is ExtraFileDescrNotFoundXFTP -> "extraFileDescrNotFoundXFTP"
|
||||
is ConnectionNotFound -> "connectionNotFound"
|
||||
is ConnectionNotFoundById -> "connectionNotFoundById"
|
||||
is ConnectionNotFoundByMemberId -> "connectionNotFoundByMemberId"
|
||||
@@ -4822,6 +4950,7 @@ sealed class StoreError {
|
||||
@Serializable @SerialName("fileIdNotFoundBySharedMsgId") class FileIdNotFoundBySharedMsgId(val sharedMsgId: String): StoreError()
|
||||
@Serializable @SerialName("sndFileNotFoundXFTP") class SndFileNotFoundXFTP(val agentSndFileId: String): StoreError()
|
||||
@Serializable @SerialName("rcvFileNotFoundXFTP") class RcvFileNotFoundXFTP(val agentRcvFileId: String): StoreError()
|
||||
@Serializable @SerialName("extraFileDescrNotFoundXFTP") class ExtraFileDescrNotFoundXFTP(val fileId: Long): StoreError()
|
||||
@Serializable @SerialName("connectionNotFound") class ConnectionNotFound(val agentConnId: String): StoreError()
|
||||
@Serializable @SerialName("connectionNotFoundById") class ConnectionNotFoundById(val connId: Long): StoreError()
|
||||
@Serializable @SerialName("connectionNotFoundByMemberId") class ConnectionNotFoundByMemberId(val groupMemberId: Long): StoreError()
|
||||
@@ -5167,3 +5296,205 @@ enum class NotificationsMode() {
|
||||
val default: NotificationsMode = SERVICE
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class AppSettings(
|
||||
var networkConfig: NetCfg? = null,
|
||||
var privacyEncryptLocalFiles: Boolean? = null,
|
||||
var privacyAcceptImages: Boolean? = null,
|
||||
var privacyLinkPreviews: Boolean? = null,
|
||||
var privacyShowChatPreviews: Boolean? = null,
|
||||
var privacySaveLastDraft: Boolean? = null,
|
||||
var privacyProtectScreen: Boolean? = null,
|
||||
var notificationMode: AppSettingsNotificationMode? = null,
|
||||
var notificationPreviewMode: AppSettingsNotificationPreviewMode? = null,
|
||||
var webrtcPolicyRelay: Boolean? = null,
|
||||
var webrtcICEServers: List<String>? = null,
|
||||
var confirmRemoteSessions: Boolean? = null,
|
||||
var connectRemoteViaMulticast: Boolean? = null,
|
||||
var connectRemoteViaMulticastAuto: Boolean? = null,
|
||||
var developerTools: Boolean? = null,
|
||||
var confirmDBUpgrades: Boolean? = null,
|
||||
var androidCallOnLockScreen: AppSettingsLockScreenCalls? = null,
|
||||
var iosCallKitEnabled: Boolean? = null,
|
||||
var iosCallKitCallsInRecents: Boolean? = null,
|
||||
) {
|
||||
fun prepareForExport(): AppSettings {
|
||||
val empty = AppSettings()
|
||||
val def = defaults
|
||||
if (networkConfig != def.networkConfig) { empty.networkConfig = networkConfig }
|
||||
if (privacyEncryptLocalFiles != def.privacyEncryptLocalFiles) { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles }
|
||||
if (privacyAcceptImages != def.privacyAcceptImages) { empty.privacyAcceptImages = privacyAcceptImages }
|
||||
if (privacyLinkPreviews != def.privacyLinkPreviews) { empty.privacyLinkPreviews = privacyLinkPreviews }
|
||||
if (privacyShowChatPreviews != def.privacyShowChatPreviews) { empty.privacyShowChatPreviews = privacyShowChatPreviews }
|
||||
if (privacySaveLastDraft != def.privacySaveLastDraft) { empty.privacySaveLastDraft = privacySaveLastDraft }
|
||||
if (privacyProtectScreen != def.privacyProtectScreen) { empty.privacyProtectScreen = privacyProtectScreen }
|
||||
if (notificationMode != def.notificationMode) { empty.notificationMode = notificationMode }
|
||||
if (notificationPreviewMode != def.notificationPreviewMode) { empty.notificationPreviewMode = notificationPreviewMode }
|
||||
if (webrtcPolicyRelay != def.webrtcPolicyRelay) { empty.webrtcPolicyRelay = webrtcPolicyRelay }
|
||||
if (webrtcICEServers != def.webrtcICEServers) { empty.webrtcICEServers = webrtcICEServers }
|
||||
if (confirmRemoteSessions != def.confirmRemoteSessions) { empty.confirmRemoteSessions = confirmRemoteSessions }
|
||||
if (connectRemoteViaMulticast != def.connectRemoteViaMulticast) { empty.connectRemoteViaMulticast = connectRemoteViaMulticast }
|
||||
if (connectRemoteViaMulticastAuto != def.connectRemoteViaMulticastAuto) { empty.connectRemoteViaMulticastAuto = connectRemoteViaMulticastAuto }
|
||||
if (developerTools != def.developerTools) { empty.developerTools = developerTools }
|
||||
if (confirmDBUpgrades != def.confirmDBUpgrades) { empty.confirmDBUpgrades = confirmDBUpgrades }
|
||||
if (androidCallOnLockScreen != def.androidCallOnLockScreen) { empty.androidCallOnLockScreen = androidCallOnLockScreen }
|
||||
if (iosCallKitEnabled != def.iosCallKitEnabled) { empty.iosCallKitEnabled = iosCallKitEnabled }
|
||||
if (iosCallKitCallsInRecents != def.iosCallKitCallsInRecents) { empty.iosCallKitCallsInRecents = iosCallKitCallsInRecents }
|
||||
return empty
|
||||
}
|
||||
|
||||
fun importIntoApp() {
|
||||
val def = appPreferences
|
||||
var net = networkConfig?.copy()
|
||||
if (net != null) {
|
||||
// migrating from iOS BUT shouldn't be here ever because it should be changed on migration stage
|
||||
if (net.hostMode == HostMode.Onion) {
|
||||
net = net.copy(hostMode = HostMode.Public, requiredHostMode = true)
|
||||
}
|
||||
setNetCfg(net)
|
||||
}
|
||||
privacyEncryptLocalFiles?.let { def.privacyEncryptLocalFiles.set(it) }
|
||||
privacyAcceptImages?.let { def.privacyAcceptImages.set(it) }
|
||||
privacyLinkPreviews?.let { def.privacyLinkPreviews.set(it) }
|
||||
privacyShowChatPreviews?.let { def.privacyShowChatPreviews.set(it) }
|
||||
privacySaveLastDraft?.let { def.privacySaveLastDraft.set(it) }
|
||||
privacyProtectScreen?.let { def.privacyProtectScreen.set(it) }
|
||||
notificationMode?.let { def.notificationsMode.set(it.toNotificationsMode()) }
|
||||
notificationPreviewMode?.let { def.notificationPreviewMode.set(it.toNotificationPreviewMode().name) }
|
||||
webrtcPolicyRelay?.let { def.webrtcPolicyRelay.set(it) }
|
||||
webrtcICEServers?.let { def.webrtcIceServers.set(it.joinToString(separator = "\n")) }
|
||||
confirmRemoteSessions?.let { def.confirmRemoteSessions.set(it) }
|
||||
connectRemoteViaMulticast?.let { def.connectRemoteViaMulticast.set(it) }
|
||||
connectRemoteViaMulticastAuto?.let { def.connectRemoteViaMulticastAuto.set(it) }
|
||||
developerTools?.let { def.developerTools.set(it) }
|
||||
confirmDBUpgrades?.let { def.confirmDBUpgrades.set(it) }
|
||||
androidCallOnLockScreen?.let { def.callOnLockScreen.set(it.toCallOnLockScreen()) }
|
||||
iosCallKitEnabled?.let { def.iosCallKitEnabled.set(it) }
|
||||
iosCallKitCallsInRecents?.let { def.iosCallKitCallsInRecents.set(it) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
val defaults: AppSettings
|
||||
get() = AppSettings(
|
||||
networkConfig = NetCfg.defaults,
|
||||
privacyEncryptLocalFiles = true,
|
||||
privacyAcceptImages = true,
|
||||
privacyLinkPreviews = true,
|
||||
privacyShowChatPreviews = true,
|
||||
privacySaveLastDraft = true,
|
||||
privacyProtectScreen = false,
|
||||
notificationMode = AppSettingsNotificationMode.INSTANT,
|
||||
notificationPreviewMode = AppSettingsNotificationPreviewMode.MESSAGE,
|
||||
webrtcPolicyRelay = true,
|
||||
webrtcICEServers = emptyList(),
|
||||
confirmRemoteSessions = false,
|
||||
connectRemoteViaMulticast = true,
|
||||
connectRemoteViaMulticastAuto = true,
|
||||
developerTools = false,
|
||||
confirmDBUpgrades = false,
|
||||
androidCallOnLockScreen = AppSettingsLockScreenCalls.SHOW,
|
||||
iosCallKitEnabled = true,
|
||||
iosCallKitCallsInRecents = false
|
||||
)
|
||||
|
||||
val current: AppSettings
|
||||
get() {
|
||||
val def = appPreferences
|
||||
return defaults.copy(
|
||||
networkConfig = getNetCfg(),
|
||||
privacyEncryptLocalFiles = def.privacyEncryptLocalFiles.get(),
|
||||
privacyAcceptImages = def.privacyAcceptImages.get(),
|
||||
privacyLinkPreviews = def.privacyLinkPreviews.get(),
|
||||
privacyShowChatPreviews = def.privacyShowChatPreviews.get(),
|
||||
privacySaveLastDraft = def.privacySaveLastDraft.get(),
|
||||
privacyProtectScreen = def.privacyProtectScreen.get(),
|
||||
notificationMode = AppSettingsNotificationMode.from(def.notificationsMode.get()),
|
||||
notificationPreviewMode = AppSettingsNotificationPreviewMode.from(NotificationPreviewMode.valueOf(def.notificationPreviewMode.get()!!)),
|
||||
webrtcPolicyRelay = def.webrtcPolicyRelay.get(),
|
||||
webrtcICEServers = def.webrtcIceServers.get()?.lines(),
|
||||
confirmRemoteSessions = def.confirmRemoteSessions.get(),
|
||||
connectRemoteViaMulticast = def.connectRemoteViaMulticast.get(),
|
||||
connectRemoteViaMulticastAuto = def.connectRemoteViaMulticastAuto.get(),
|
||||
developerTools = def.developerTools.get(),
|
||||
confirmDBUpgrades = def.confirmDBUpgrades.get(),
|
||||
androidCallOnLockScreen = AppSettingsLockScreenCalls.from(def.callOnLockScreen.get()),
|
||||
iosCallKitEnabled = def.iosCallKitEnabled.get(),
|
||||
iosCallKitCallsInRecents = def.iosCallKitCallsInRecents.get(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class AppSettingsNotificationMode {
|
||||
@SerialName("off") OFF,
|
||||
@SerialName("periodic") PERIODIC,
|
||||
@SerialName("instant") INSTANT;
|
||||
|
||||
fun toNotificationsMode(): NotificationsMode =
|
||||
when (this) {
|
||||
INSTANT -> NotificationsMode.SERVICE
|
||||
PERIODIC -> NotificationsMode.PERIODIC
|
||||
OFF -> NotificationsMode.OFF
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(mode: NotificationsMode): AppSettingsNotificationMode =
|
||||
when (mode) {
|
||||
NotificationsMode.SERVICE -> INSTANT
|
||||
NotificationsMode.PERIODIC -> PERIODIC
|
||||
NotificationsMode.OFF -> OFF
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class AppSettingsNotificationPreviewMode {
|
||||
@SerialName("message") MESSAGE,
|
||||
@SerialName("contact") CONTACT,
|
||||
@SerialName("hidden") HIDDEN;
|
||||
|
||||
fun toNotificationPreviewMode(): NotificationPreviewMode =
|
||||
when (this) {
|
||||
MESSAGE -> NotificationPreviewMode.MESSAGE
|
||||
CONTACT -> NotificationPreviewMode.CONTACT
|
||||
HIDDEN -> NotificationPreviewMode.HIDDEN
|
||||
}
|
||||
|
||||
companion object {
|
||||
val default: AppSettingsNotificationPreviewMode = MESSAGE
|
||||
|
||||
fun from(mode: NotificationPreviewMode): AppSettingsNotificationPreviewMode =
|
||||
when (mode) {
|
||||
NotificationPreviewMode.MESSAGE -> MESSAGE
|
||||
NotificationPreviewMode.CONTACT -> CONTACT
|
||||
NotificationPreviewMode.HIDDEN -> HIDDEN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class AppSettingsLockScreenCalls {
|
||||
@SerialName("disable") DISABLE,
|
||||
@SerialName("show") SHOW,
|
||||
@SerialName("accept") ACCEPT;
|
||||
|
||||
fun toCallOnLockScreen(): CallOnLockScreen =
|
||||
when (this) {
|
||||
DISABLE -> CallOnLockScreen.DISABLE
|
||||
SHOW -> CallOnLockScreen.SHOW
|
||||
ACCEPT -> CallOnLockScreen.ACCEPT
|
||||
}
|
||||
|
||||
companion object {
|
||||
val default = SHOW
|
||||
|
||||
fun from(mode: CallOnLockScreen): AppSettingsLockScreenCalls =
|
||||
when (mode) {
|
||||
CallOnLockScreen.DISABLE -> DISABLE
|
||||
CallOnLockScreen.SHOW -> SHOW
|
||||
CallOnLockScreen.ACCEPT -> ACCEPT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@ import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.model.ChatModel.currentUser
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.helpers.DatabaseUtils.ksDatabasePassword
|
||||
import chat.simplex.common.views.helpers.DatabaseUtils.randomDatabasePassword
|
||||
import chat.simplex.common.views.onboarding.OnboardingStage
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import java.io.File
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
// ghc's rts
|
||||
@@ -137,6 +139,33 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
|
||||
}
|
||||
}
|
||||
|
||||
fun chatInitTemporaryDatabase(dbPath: String, key: String? = null, confirmation: MigrationConfirmation = MigrationConfirmation.Error): Pair<DBMigrationResult, ChatCtrl?> {
|
||||
val dbKey = key ?: randomDatabasePassword()
|
||||
Log.d(TAG, "chatInitTemporaryDatabase path: $dbPath")
|
||||
val migrated = chatMigrateInit(dbPath, dbKey, confirmation.value)
|
||||
val res = runCatching {
|
||||
json.decodeFromString<DBMigrationResult>(migrated[0] as String)
|
||||
}.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) }
|
||||
|
||||
return res to migrated[1] as ChatCtrl
|
||||
}
|
||||
|
||||
fun chatInitControllerRemovingDatabases() {
|
||||
val dbPath = dbAbsolutePrefixPath
|
||||
val dbKey = randomDatabasePassword()
|
||||
Log.d(TAG, "chatInitControllerRemovingDatabases path: $dbPath")
|
||||
val migrated = chatMigrateInit(dbPath, dbKey, MigrationConfirmation.Error.value)
|
||||
val res = runCatching {
|
||||
json.decodeFromString<DBMigrationResult>(migrated[0] as String)
|
||||
}.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) }
|
||||
|
||||
val ctrl = migrated[1] as Long
|
||||
chatController.ctrl = ctrl
|
||||
// We need only controller, not databases
|
||||
File(dbPath + "_chat.db").delete()
|
||||
File(dbPath + "_agent.db").delete()
|
||||
}
|
||||
|
||||
fun showStartChatAfterRestartAlert(): CompletableDeferred<Boolean> {
|
||||
val deferred = CompletableDeferred<Boolean>()
|
||||
AlertManager.shared.showAlertDialog(
|
||||
|
||||
@@ -66,6 +66,8 @@ fun copyBytesToFile(bytes: ByteArrayInputStream, to: URI, finally: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
fun getMigrationTempFilesDirectory(): File = File(dataDir, "migration_temp_files")
|
||||
|
||||
fun getAppFilePath(fileName: String): String {
|
||||
val rh = chatModel.currentRemoteHost.value
|
||||
val s = File.separator
|
||||
|
||||
+2
@@ -1,5 +1,6 @@
|
||||
package chat.simplex.common.platform
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import chat.simplex.common.model.ChatId
|
||||
import chat.simplex.common.model.NotificationsMode
|
||||
|
||||
@@ -16,6 +17,7 @@ interface PlatformInterface {
|
||||
fun androidStartCallActivity(acceptCall: Boolean, remoteHostId: Long? = null, chatId: ChatId? = null) {}
|
||||
fun androidPictureInPictureAllowed(): Boolean = true
|
||||
fun androidCallEnded() {}
|
||||
@Composable fun androidLockPortraitOrientation() {}
|
||||
suspend fun androidAskToAllowBackgroundCalls(): Boolean = true
|
||||
}
|
||||
/**
|
||||
|
||||
+75
-24
@@ -1,9 +1,8 @@
|
||||
package chat.simplex.common.views.database
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionItemView
|
||||
import SectionItemViewSpaceBetween
|
||||
import SectionTextFooter
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
@@ -24,20 +23,22 @@ import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.*
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.appPreferences
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.appPlatform
|
||||
import chat.simplex.common.views.usersettings.SettingsActionItem
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlin.math.log2
|
||||
|
||||
@Composable
|
||||
fun DatabaseEncryptionView(m: ChatModel) {
|
||||
fun DatabaseEncryptionView(m: ChatModel, migration: Boolean) {
|
||||
val progressIndicator = remember { mutableStateOf(false) }
|
||||
val prefs = m.controller.appPrefs
|
||||
val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) }
|
||||
@@ -61,9 +62,10 @@ fun DatabaseEncryptionView(m: ChatModel) {
|
||||
storedKey,
|
||||
initialRandomDBPassphrase,
|
||||
progressIndicator,
|
||||
migration,
|
||||
onConfirmEncrypt = {
|
||||
withLongRunningApi {
|
||||
encryptDatabase(currentKey, newKey, confirmNewKey, initialRandomDBPassphrase, useKeychain, storedKey, progressIndicator)
|
||||
encryptDatabase(currentKey, newKey, confirmNewKey, initialRandomDBPassphrase, useKeychain, storedKey, progressIndicator, migration)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -95,24 +97,34 @@ fun DatabaseEncryptionLayout(
|
||||
storedKey: MutableState<Boolean>,
|
||||
initialRandomDBPassphrase: MutableState<Boolean>,
|
||||
progressIndicator: MutableState<Boolean>,
|
||||
migration: Boolean,
|
||||
onConfirmEncrypt: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
|
||||
if (!migration) Modifier.fillMaxWidth().verticalScroll(rememberScrollState()) else Modifier.fillMaxWidth(),
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.database_passphrase))
|
||||
SectionView(null) {
|
||||
SavePassphraseSetting(useKeychain.value, initialRandomDBPassphrase.value, storedKey.value, progressIndicator.value) { checked ->
|
||||
if (!migration) {
|
||||
AppBarTitle(stringResource(MR.strings.database_passphrase))
|
||||
} else {
|
||||
ChatStoppedView()
|
||||
SectionSpacer()
|
||||
}
|
||||
SectionView(if (migration) generalGetString(MR.strings.database_passphrase).uppercase() else null) {
|
||||
SavePassphraseSetting(
|
||||
useKeychain.value,
|
||||
initialRandomDBPassphrase.value,
|
||||
storedKey.value,
|
||||
enabled = (!initialRandomDBPassphrase.value && !progressIndicator.value) || migration
|
||||
) { checked ->
|
||||
if (checked) {
|
||||
setUseKeychain(true, useKeychain, prefs)
|
||||
} else if (storedKey.value) {
|
||||
setUseKeychain(true, useKeychain, prefs, migration)
|
||||
} else if (storedKey.value && !migration) {
|
||||
// Don't show in migration process since it will remove the key after successful encryption
|
||||
removePassphraseAlert {
|
||||
DatabaseUtils.ksDatabasePassword.remove()
|
||||
setUseKeychain(false, useKeychain, prefs)
|
||||
storedKey.value = false
|
||||
removePassphraseFromKeyChain(useKeychain, prefs, storedKey, false)
|
||||
}
|
||||
} else {
|
||||
setUseKeychain(false, useKeychain, prefs)
|
||||
setUseKeychain(false, useKeychain, prefs, migration)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,12 +181,12 @@ fun DatabaseEncryptionLayout(
|
||||
)
|
||||
|
||||
SectionItemViewSpaceBetween(onClickUpdate, disabled = disabled, minHeight = TextFieldDefaults.MinHeight) {
|
||||
Text(generalGetString(MR.strings.update_database_passphrase), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
|
||||
Text(generalGetString(if (migration) MR.strings.set_passphrase else MR.strings.update_database_passphrase), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
DatabaseEncryptionFooter(useKeychain, chatDbEncrypted, storedKey, initialRandomDBPassphrase)
|
||||
DatabaseEncryptionFooter(useKeychain, chatDbEncrypted, storedKey, initialRandomDBPassphrase, migration)
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
@@ -211,8 +223,9 @@ expect fun SavePassphraseSetting(
|
||||
useKeychain: Boolean,
|
||||
initialRandomDBPassphrase: Boolean,
|
||||
storedKey: Boolean,
|
||||
progressIndicator: Boolean,
|
||||
minHeight: Dp = TextFieldDefaults.MinHeight,
|
||||
enabled: Boolean,
|
||||
smallPadding: Boolean = true,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
)
|
||||
|
||||
@@ -222,8 +235,18 @@ expect fun DatabaseEncryptionFooter(
|
||||
chatDbEncrypted: Boolean?,
|
||||
storedKey: MutableState<Boolean>,
|
||||
initialRandomDBPassphrase: MutableState<Boolean>,
|
||||
migration: Boolean,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun ChatStoppedView() {
|
||||
SettingsActionItem(
|
||||
icon = painterResource(MR.images.ic_report_filled),
|
||||
text = stringResource(MR.strings.chat_is_stopped),
|
||||
iconColor = Color.Red,
|
||||
)
|
||||
}
|
||||
|
||||
fun resetFormAfterEncryption(
|
||||
m: ChatModel,
|
||||
initialRandomDBPassphrase: MutableState<Boolean>,
|
||||
@@ -242,9 +265,18 @@ fun resetFormAfterEncryption(
|
||||
m.controller.appPrefs.initialRandomDBPassphrase.set(false)
|
||||
}
|
||||
|
||||
fun setUseKeychain(value: Boolean, useKeychain: MutableState<Boolean>, prefs: AppPreferences) {
|
||||
fun setUseKeychain(value: Boolean, useKeychain: MutableState<Boolean>, prefs: AppPreferences, migration: Boolean) {
|
||||
useKeychain.value = value
|
||||
prefs.storeDBPassphrase.set(value)
|
||||
// Postpone it when migrating to the end of encryption process
|
||||
if (!migration) {
|
||||
prefs.storeDBPassphrase.set(value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removePassphraseFromKeyChain(useKeychain: MutableState<Boolean>, prefs: AppPreferences, storedKey: MutableState<Boolean>, migration: Boolean) {
|
||||
DatabaseUtils.ksDatabasePassword.remove()
|
||||
setUseKeychain(false, useKeychain, prefs, migration)
|
||||
storedKey.value = false
|
||||
}
|
||||
|
||||
fun storeSecurelySaved() = generalGetString(MR.strings.store_passphrase_securely)
|
||||
@@ -267,6 +299,7 @@ fun PassphraseField(
|
||||
isValid: (String) -> Boolean,
|
||||
keyboardActions: KeyboardActions = KeyboardActions(),
|
||||
dependsOn: State<Any?>? = null,
|
||||
requestFocus: Boolean = false,
|
||||
) {
|
||||
var valid by remember { mutableStateOf(validKey(key.value)) }
|
||||
var showKey by remember { mutableStateOf(false) }
|
||||
@@ -295,6 +328,7 @@ fun PassphraseField(
|
||||
val color = MaterialTheme.colors.onBackground
|
||||
val shape = MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize)
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
BasicTextField(
|
||||
value = state.value,
|
||||
modifier = modifier
|
||||
@@ -304,7 +338,8 @@ fun PassphraseField(
|
||||
.defaultMinSize(
|
||||
minWidth = TextFieldDefaults.MinWidth,
|
||||
minHeight = TextFieldDefaults.MinHeight
|
||||
),
|
||||
)
|
||||
.focusRequester(focusRequester),
|
||||
onValueChange = {
|
||||
state.value = it
|
||||
key.value = it.text
|
||||
@@ -347,6 +382,12 @@ fun PassphraseField(
|
||||
)
|
||||
}
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
if (requestFocus) {
|
||||
delay(200)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { dependsOn?.value }
|
||||
.distinctUntilChanged()
|
||||
@@ -363,13 +404,17 @@ suspend fun encryptDatabase(
|
||||
initialRandomDBPassphrase: MutableState<Boolean>,
|
||||
useKeychain: MutableState<Boolean>,
|
||||
storedKey: MutableState<Boolean>,
|
||||
progressIndicator: MutableState<Boolean>
|
||||
progressIndicator: MutableState<Boolean>,
|
||||
migration: Boolean,
|
||||
): Boolean {
|
||||
val m = ChatModel
|
||||
val prefs = ChatController.appPrefs
|
||||
progressIndicator.value = true
|
||||
return try {
|
||||
prefs.encryptionStartedAt.set(Clock.System.now())
|
||||
if (!m.chatDbChanged.value) {
|
||||
m.controller.apiSaveAppSettings(AppSettings.current.prepareForExport())
|
||||
}
|
||||
val error = m.controller.apiStorageEncryption(currentKey.value, newKey.value)
|
||||
prefs.encryptionStartedAt.set(null)
|
||||
val sqliteError = ((error?.chatError as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorExport)?.sqliteError
|
||||
@@ -393,9 +438,14 @@ suspend fun encryptDatabase(
|
||||
}
|
||||
else -> {
|
||||
val new = newKey.value
|
||||
if (migration) {
|
||||
appPreferences.storeDBPassphrase.set(useKeychain.value)
|
||||
}
|
||||
resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value)
|
||||
if (useKeychain.value) {
|
||||
DatabaseUtils.ksDatabasePassword.set(new)
|
||||
} else if (migration) {
|
||||
removePassphraseFromKeyChain(useKeychain, prefs, storedKey, true)
|
||||
}
|
||||
operationEnded(m, progressIndicator) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.database_encrypted))
|
||||
@@ -474,6 +524,7 @@ fun PreviewDatabaseEncryptionLayout() {
|
||||
storedKey = remember { mutableStateOf(true) },
|
||||
initialRandomDBPassphrase = remember { mutableStateOf(true) },
|
||||
progressIndicator = remember { mutableStateOf(false) },
|
||||
migration = false,
|
||||
onConfirmEncrypt = {},
|
||||
)
|
||||
}
|
||||
|
||||
+9
-3
@@ -206,6 +206,14 @@ private fun runChat(
|
||||
is DBMigrationResult.OK -> {
|
||||
platform.androidChatStartedAfterBeingOff()
|
||||
}
|
||||
null -> {}
|
||||
else -> showErrorOnMigrationIfNeeded(status)
|
||||
}
|
||||
}
|
||||
|
||||
fun showErrorOnMigrationIfNeeded(status: DBMigrationResult) =
|
||||
when (status) {
|
||||
is DBMigrationResult.OK -> {}
|
||||
is DBMigrationResult.ErrorNotADatabase ->
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.wrong_passphrase_title), generalGetString(MR.strings.enter_correct_passphrase))
|
||||
is DBMigrationResult.ErrorSQL ->
|
||||
@@ -217,9 +225,7 @@ private fun runChat(
|
||||
is DBMigrationResult.InvalidConfirmation ->
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.invalid_migration_confirmation))
|
||||
is DBMigrationResult.ErrorMigration -> {}
|
||||
null -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldShowRestoreDbButton(prefs: AppPreferences): Boolean {
|
||||
val startedAt = prefs.encryptionStartedAt.get() ?: return false
|
||||
@@ -246,7 +252,7 @@ private fun restoreDb(restoreDbFromBackup: MutableState<Boolean>, prefs: AppPref
|
||||
}
|
||||
}
|
||||
|
||||
private fun mtrErrorDescription(err: MTRError): String =
|
||||
fun mtrErrorDescription(err: MTRError): String =
|
||||
when (err) {
|
||||
is MTRError.NoDown ->
|
||||
String.format(generalGetString(MR.strings.mtr_error_no_down_migration), err.dbMigrations.joinToString(", "))
|
||||
|
||||
+15
-7
@@ -211,7 +211,7 @@ fun DatabaseLayout(
|
||||
if (unencrypted) painterResource(MR.images.ic_lock_open_right) else if (useKeyChain) painterResource(MR.images.ic_vpn_key_filled)
|
||||
else painterResource(MR.images.ic_lock),
|
||||
stringResource(MR.strings.database_passphrase),
|
||||
click = showSettingsModal() { DatabaseEncryptionView(it) },
|
||||
click = showSettingsModal() { DatabaseEncryptionView(it, false) },
|
||||
iconColor = if (unencrypted || (appPlatform.isDesktop && passphraseSaved)) WarningOrange else MaterialTheme.colors.secondary,
|
||||
disabled = operationsDisabled
|
||||
)
|
||||
@@ -486,6 +486,7 @@ fun deleteChatDatabaseFilesAndState() {
|
||||
filesDir.mkdir()
|
||||
remoteHostsDir.deleteRecursively()
|
||||
tmpDir.deleteRecursively()
|
||||
getMigrationTempFilesDirectory().deleteRecursively()
|
||||
tmpDir.mkdir()
|
||||
DatabaseUtils.ksDatabasePassword.remove()
|
||||
controller.appPrefs.storeDBPassphrase.set(true)
|
||||
@@ -509,7 +510,7 @@ private fun exportArchive(
|
||||
progressIndicator.value = true
|
||||
withLongRunningApi {
|
||||
try {
|
||||
val archiveFile = exportChatArchive(m, chatArchiveName, chatArchiveTime, chatArchiveFile)
|
||||
val archiveFile = exportChatArchive(m, null, chatArchiveName, chatArchiveTime, chatArchiveFile)
|
||||
chatArchiveFile.value = archiveFile
|
||||
saveArchiveLauncher.launch(archiveFile.substringAfterLast(File.separator))
|
||||
progressIndicator.value = false
|
||||
@@ -520,8 +521,9 @@ private fun exportArchive(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun exportChatArchive(
|
||||
suspend fun exportChatArchive(
|
||||
m: ChatModel,
|
||||
storagePath: File?,
|
||||
chatArchiveName: MutableState<String?>,
|
||||
chatArchiveTime: MutableState<Instant?>,
|
||||
chatArchiveFile: MutableState<String?>
|
||||
@@ -529,13 +531,19 @@ private suspend fun exportChatArchive(
|
||||
val archiveTime = Clock.System.now()
|
||||
val ts = SimpleDateFormat("yyyy-MM-dd'T'HHmmss", Locale.US).format(Date.from(archiveTime.toJavaInstant()))
|
||||
val archiveName = "simplex-chat.$ts.zip"
|
||||
val archivePath = "${filesDir.absolutePath}${File.separator}$archiveName"
|
||||
val archivePath = "${(storagePath ?: filesDir).absolutePath}${File.separator}$archiveName"
|
||||
val config = ArchiveConfig(archivePath, parentTempDirectory = databaseExportDir.toString())
|
||||
// Settings should be saved before changing a passphrase, otherwise the database needs to be migrated first
|
||||
if (!m.chatDbChanged.value) {
|
||||
controller.apiSaveAppSettings(AppSettings.current.prepareForExport())
|
||||
}
|
||||
m.controller.apiExportArchive(config)
|
||||
deleteOldArchive(m)
|
||||
m.controller.appPrefs.chatArchiveName.set(archiveName)
|
||||
if (storagePath == null) {
|
||||
deleteOldArchive(m)
|
||||
m.controller.appPrefs.chatArchiveName.set(archiveName)
|
||||
m.controller.appPrefs.chatArchiveTime.set(archiveTime)
|
||||
}
|
||||
chatArchiveName.value = archiveName
|
||||
m.controller.appPrefs.chatArchiveTime.set(archiveTime)
|
||||
chatArchiveTime.value = archiveTime
|
||||
chatArchiveFile.value = archivePath
|
||||
return archivePath
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ object DatabaseUtils {
|
||||
return dbKey
|
||||
}
|
||||
|
||||
private fun randomDatabasePassword(): String {
|
||||
fun randomDatabasePassword(): String {
|
||||
val s = ByteArray(32)
|
||||
SecureRandom().nextBytes(s)
|
||||
return s.toBase64StringForPassphrase().replace("\n", "")
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ fun DefaultProgressView(description: String?) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
CircularProgressIndicator(
|
||||
Modifier
|
||||
.padding(bottom = DEFAULT_PADDING)
|
||||
.padding(bottom = if (description != null) DEFAULT_PADDING else 0.dp)
|
||||
.size(30.dp),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
strokeWidth = 2.5.dp
|
||||
|
||||
+3
-2
@@ -19,17 +19,18 @@ import kotlin.math.min
|
||||
fun ModalView(
|
||||
close: () -> Unit,
|
||||
showClose: Boolean = true,
|
||||
enableClose: Boolean = true,
|
||||
background: Color = MaterialTheme.colors.background,
|
||||
modifier: Modifier = Modifier,
|
||||
endButtons: @Composable RowScope.() -> Unit = {},
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
if (showClose) {
|
||||
BackHandler(onBack = close)
|
||||
BackHandler(enabled = enableClose, onBack = close)
|
||||
}
|
||||
Surface(Modifier.fillMaxSize(), contentColor = LocalContentColor.current) {
|
||||
Column(if (background != MaterialTheme.colors.background) Modifier.background(background) else Modifier.themedBackground()) {
|
||||
CloseSheetBar(close, showClose, endButtons = endButtons)
|
||||
CloseSheetBar(if (enableClose) close else null, showClose, endButtons = endButtons)
|
||||
Box(modifier) { content() }
|
||||
}
|
||||
}
|
||||
|
||||
+721
@@ -0,0 +1,721 @@
|
||||
package chat.simplex.common.views.migration
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionItemView
|
||||
import SectionSpacer
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
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.ChatController.getNetCfg
|
||||
import chat.simplex.common.model.ChatController.startChat
|
||||
import chat.simplex.common.model.ChatCtrl
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.database.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.helpers.DatabaseUtils.ksDatabasePassword
|
||||
import chat.simplex.common.views.newchat.QRCodeScanner
|
||||
import chat.simplex.common.views.onboarding.OnboardingStage
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.toJavaInstant
|
||||
import kotlinx.serialization.*
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
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()
|
||||
|
||||
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
|
||||
}
|
||||
if (state == null) {
|
||||
settings.remove(SHARED_PREFS_MIGRATION_STAGE)
|
||||
getMigrationTempFilesDirectory().deleteRecursively()
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
fun save(state: MigrationFromAnotherDeviceState?) {
|
||||
if (state != null) {
|
||||
appPreferences.migrationStage.set(json.encodeToString(state))
|
||||
} else {
|
||||
appPreferences.migrationStage.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()
|
||||
}
|
||||
|
||||
private var MutableState<MigrationState>.state: MigrationState
|
||||
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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
// 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
|
||||
}
|
||||
}
|
||||
val chatReceiver = remember { mutableStateOf(null as MigrationFromChatReceiver?) }
|
||||
ModalView(
|
||||
enableClose = !backDisabled.value,
|
||||
close = {
|
||||
withBGApi {
|
||||
migrationState.cleanUpOnBack(chatReceiver.value)
|
||||
close()
|
||||
}
|
||||
},
|
||||
) {
|
||||
MigrateFromAnotherDeviceLayout(
|
||||
migrationState = migrationState,
|
||||
chatReceiver = chatReceiver,
|
||||
close = close,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ModalData.MigrateFromAnotherDeviceLayout(
|
||||
migrationState: MutableState<MigrationState>,
|
||||
chatReceiver: MutableState<MigrationFromChatReceiver?>,
|
||||
close: () -> Unit,
|
||||
) {
|
||||
val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) }
|
||||
|
||||
Column(
|
||||
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).height(IntrinsicSize.Max),
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.migrate_here))
|
||||
SectionByState(migrationState, tempDatabaseFile.value, chatReceiver, close)
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
platform.androidLockPortraitOrientation()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ModalData.SectionByState(
|
||||
migrationState: MutableState<MigrationState>,
|
||||
tempDatabaseFile: File,
|
||||
chatReceiver: MutableState<MigrationFromChatReceiver?>,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationState>.PasteOrScanLinkView() {
|
||||
if (appPlatform.isAndroid) {
|
||||
SectionView(stringResource(MR.strings.scan_QR_code).replace('\n', ' ').uppercase()) {
|
||||
QRCodeScanner(showQRCodeScanner = remember { mutableStateOf(true) }) { text ->
|
||||
withBGApi { checkUserLink(text) }
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
}
|
||||
|
||||
if (appPlatform.isDesktop || appPreferences.developerTools.get()) {
|
||||
SectionView(stringResource(if (appPlatform.isAndroid) MR.strings.or_paste_archive_link else MR.strings.paste_archive_link).uppercase()) {
|
||||
PasteLinkView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationState>.PasteLinkView() {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
SectionItemView({
|
||||
val str = clipboard.getText()?.text ?: return@SectionItemView
|
||||
withBGApi { checkUserLink(str) }
|
||||
}) {
|
||||
Text(stringResource(MR.strings.tap_to_paste_link))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: HostMode, requiredHostMode: Boolean, state: MutableState<MigrationState>) {
|
||||
val onionHosts = remember { stateGetOrPut("onionHosts") {
|
||||
getNetCfg().copy(socksProxy = socksProxy, hostMode = hostMode, requiredHostMode = requiredHostMode).onionHosts
|
||||
} }
|
||||
val networkUseSocksProxy = remember { stateGetOrPut("networkUseSocksProxy") { socksProxy != null } }
|
||||
val sessionMode = remember { stateGetOrPut("sessionMode") { TransportSessionMode.User} }
|
||||
val networkProxyHostPort = remember { stateGetOrPut("networkHostProxyPort") {
|
||||
var proxy = (socksProxy ?: chatModel.controller.appPrefs.networkProxyHostPort.get())
|
||||
if (proxy?.startsWith(":") == true) proxy = "localhost$proxy"
|
||||
proxy
|
||||
}
|
||||
}
|
||||
val proxyPort = remember { derivedStateOf { networkProxyHostPort.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } }
|
||||
|
||||
val netCfg = rememberSaveable(stateSaver = serializableSaver()) {
|
||||
mutableStateOf(getNetCfg().withOnionHosts(onionHosts.value).copy(socksProxy = socksProxy, sessionMode = sessionMode.value))
|
||||
}
|
||||
|
||||
SectionView(stringResource(MR.strings.migration_from_device_confirm_network_settings).uppercase()) {
|
||||
SettingsActionItemWithContent(
|
||||
icon = painterResource(MR.images.ic_check),
|
||||
text = stringResource(MR.strings.migration_from_device_apply_onion),
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
click = {
|
||||
val updated = netCfg.value
|
||||
.withOnionHosts(onionHosts.value)
|
||||
.withHostPort(if (networkUseSocksProxy.value) networkProxyHostPort.value else null, null)
|
||||
.copy(
|
||||
sessionMode = sessionMode.value
|
||||
)
|
||||
withBGApi {
|
||||
state.value = MigrationState.DatabaseInit(link, updated)
|
||||
}
|
||||
}
|
||||
){}
|
||||
SectionTextFooter(stringResource(MR.strings.migration_from_device_confirm_network_settings_footer))
|
||||
}
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
val networkProxyHostPortPref = SharedPreference(get = { networkProxyHostPort.value }, set = {
|
||||
networkProxyHostPort.value = it
|
||||
})
|
||||
SectionView(stringResource(MR.strings.network_settings_title).uppercase()) {
|
||||
OnionRelatedLayout(
|
||||
appPreferences.developerTools.get(),
|
||||
networkUseSocksProxy,
|
||||
onionHosts,
|
||||
sessionMode,
|
||||
networkProxyHostPortPref,
|
||||
proxyPort,
|
||||
toggleSocksProxy = { enable ->
|
||||
networkUseSocksProxy.value = enable
|
||||
},
|
||||
useOnion = {
|
||||
onionHosts.value = it
|
||||
},
|
||||
updateSessionMode = {
|
||||
sessionMode.value = it
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationState>.DatabaseInitView(link: String, tempDatabaseFile: File, netCfg: NetCfg) {
|
||||
Box {
|
||||
SectionView(stringResource(MR.strings.migration_from_device_database_init).uppercase()) {}
|
||||
ProgressView()
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
prepareDatabase(link, tempDatabaseFile, netCfg)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationState>.LinkDownloadingView(
|
||||
link: String,
|
||||
ctrl: ChatCtrl,
|
||||
user: User,
|
||||
archivePath: String,
|
||||
tempDatabaseFile: File,
|
||||
chatReceiver: MutableState<MigrationFromChatReceiver?>,
|
||||
netCfg: NetCfg
|
||||
) {
|
||||
Box {
|
||||
SectionView(stringResource(MR.strings.migration_from_device_downloading_details).uppercase()) {}
|
||||
ProgressView()
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
startDownloading(0, ctrl, user, tempDatabaseFile, chatReceiver, link, archivePath, netCfg)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DownloadProgressView(downloadedBytes: Long, totalBytes: Long) {
|
||||
Box {
|
||||
SectionView(stringResource(MR.strings.migration_from_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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationState>.DownloadFailedView(link: String, chatReceiver: MigrationFromChatReceiver?, archivePath: String, netCfg: NetCfg) {
|
||||
SectionView(stringResource(MR.strings.migration_from_device_download_failed).uppercase()) {
|
||||
SettingsActionItemWithContent(
|
||||
icon = painterResource(MR.images.ic_download),
|
||||
text = stringResource(MR.strings.migration_from_device_repeat_download),
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
click = {
|
||||
state = MigrationState.DatabaseInit(link, netCfg)
|
||||
}
|
||||
) {}
|
||||
SectionTextFooter(stringResource(MR.strings.migration_from_device_try_again))
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
chatReceiver?.stopAndCleanUp()
|
||||
File(archivePath).delete()
|
||||
MigrationFromAnotherDeviceState.save(null)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationState>.ArchiveImportView(archivePath: String, netCfg: NetCfg) {
|
||||
Box {
|
||||
SectionView(stringResource(MR.strings.migration_from_device_importing_archive).uppercase()) {}
|
||||
ProgressView()
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
importArchive(archivePath, netCfg)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationState>.ArchiveImportFailedView(archivePath: String, netCfg: NetCfg) {
|
||||
SectionView(stringResource(MR.strings.migration_from_device_import_failed).uppercase()) {
|
||||
SettingsActionItemWithContent(
|
||||
icon = painterResource(MR.images.ic_download),
|
||||
text = stringResource(MR.strings.migration_from_device_repeat_import),
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
click = {
|
||||
state = MigrationState.ArchiveImport(archivePath, netCfg)
|
||||
}
|
||||
) {}
|
||||
SectionTextFooter(stringResource(MR.strings.migration_from_device_try_again))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationState>.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()) {
|
||||
SavePassphraseSetting(
|
||||
useKeychain.value,
|
||||
false,
|
||||
false,
|
||||
enabled = !verifyingPassphrase.value,
|
||||
smallPadding = false
|
||||
) { checked -> useKeychain.value = checked }
|
||||
|
||||
PassphraseField(currentKey, placeholder = stringResource(MR.strings.current_passphrase), Modifier.padding(horizontal = DEFAULT_PADDING), isValid = ::validKey, requestFocus = true)
|
||||
|
||||
SettingsActionItemWithContent(
|
||||
icon = painterResource(MR.images.ic_vpn_key_filled),
|
||||
text = stringResource(MR.strings.open_chat),
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
disabled = verifyingPassphrase.value || currentKey.value.isEmpty(),
|
||||
click = {
|
||||
verifyingPassphrase.value = true
|
||||
hideKeyboard(view)
|
||||
withBGApi {
|
||||
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)
|
||||
} else if (status is DBMigrationResult.ErrorMigration) {
|
||||
state = MigrationState.MigrationConfirmation(status, currentKey.value, useKeychain.value, netCfg)
|
||||
} else {
|
||||
showErrorOnMigrationIfNeeded(status)
|
||||
}
|
||||
verifyingPassphrase.value = false
|
||||
}
|
||||
}
|
||||
) {}
|
||||
DatabaseEncryptionFooter(useKeychain, chatDbEncrypted = true, remember { mutableStateOf(false) }, remember { mutableStateOf(false) }, true)
|
||||
}
|
||||
if (verifyingPassphrase.value) {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationState>.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) {
|
||||
is MigrationError.Upgrade ->
|
||||
Tuple4(
|
||||
generalGetString(MR.strings.database_upgrade),
|
||||
generalGetString(MR.strings.upgrade_and_open_chat),
|
||||
"",
|
||||
MigrationConfirmation.YesUp
|
||||
)
|
||||
is MigrationError.Downgrade ->
|
||||
Tuple4(
|
||||
generalGetString(MR.strings.database_downgrade),
|
||||
generalGetString(MR.strings.downgrade_and_open_chat),
|
||||
generalGetString(MR.strings.database_downgrade_warning),
|
||||
MigrationConfirmation.YesUpDown
|
||||
)
|
||||
is MigrationError.Error ->
|
||||
Tuple4(
|
||||
generalGetString(MR.strings.incompatible_database_version),
|
||||
null,
|
||||
mtrErrorDescription(err.mtrError),
|
||||
null
|
||||
)
|
||||
}
|
||||
else -> Tuple4(generalGetString(MR.strings.error), null, generalGetString(MR.strings.unknown_error), null)
|
||||
}
|
||||
SectionView(header.uppercase()) {
|
||||
if (button != null && confirmation != null) {
|
||||
SettingsActionItemWithContent(
|
||||
icon = painterResource(MR.images.ic_download),
|
||||
text = button,
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
click = {
|
||||
state = MigrationState.Migration(passphrase, confirmation, useKeychain, netCfg)
|
||||
}
|
||||
) {}
|
||||
}
|
||||
SectionTextFooter(footer)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MigrationView(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Boolean, netCfg: NetCfg, close: () -> Unit) {
|
||||
Box {
|
||||
SectionView(stringResource(MR.strings.migration_from_device_migrating).uppercase()) {}
|
||||
ProgressView()
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
startChat(passphrase, confirmation, useKeychain, netCfg, close)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProgressView() {
|
||||
DefaultProgressView(null)
|
||||
}
|
||||
|
||||
private suspend fun MutableState<MigrationState>.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))
|
||||
} else {
|
||||
val current = getNetCfg()
|
||||
state = MigrationState.DatabaseInit(link.trim(), current.copy(
|
||||
socksProxy = networkConfig?.socksProxy,
|
||||
hostMode = networkConfig?.hostMode ?: current.hostMode,
|
||||
requiredHostMode = networkConfig?.requiredHostMode ?: current.requiredHostMode
|
||||
))
|
||||
}
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.invalid_file_link),
|
||||
text = generalGetString(MR.strings.the_text_you_pasted_is_not_a_link)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableState<MigrationState>.prepareDatabase(
|
||||
link: String,
|
||||
tempDatabaseFile: File,
|
||||
netCfg: NetCfg,
|
||||
) {
|
||||
withLongRunningApi {
|
||||
val ctrlAndUser = initTemporaryDatabase(tempDatabaseFile, netCfg)
|
||||
if (ctrlAndUser == null) {
|
||||
state = MigrationState.DownloadFailed(0, link, archivePath(), netCfg)
|
||||
return@withLongRunningApi
|
||||
}
|
||||
|
||||
val (ctrl, user) = ctrlAndUser
|
||||
state = MigrationState.LinkDownloading(link, ctrl, user, archivePath(), netCfg)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableState<MigrationState>.startDownloading(
|
||||
totalBytes: Long,
|
||||
ctrl: ChatCtrl,
|
||||
user: User,
|
||||
tempDatabaseFile: File,
|
||||
chatReceiver: MutableState<MigrationFromChatReceiver?>,
|
||||
link: String,
|
||||
archivePath: String,
|
||||
netCfg: NetCfg,
|
||||
) {
|
||||
withBGApi {
|
||||
chatReceiver.value = MigrationFromChatReceiver(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))
|
||||
}
|
||||
is CR.RcvStandaloneFileComplete -> {
|
||||
delay(500)
|
||||
state = MigrationState.ArchiveImport(archivePath, netCfg)
|
||||
MigrationFromAnotherDeviceState.save(MigrationFromAnotherDeviceState.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)
|
||||
)
|
||||
state = MigrationState.DownloadFailed(totalBytes, link, archivePath, netCfg)
|
||||
}
|
||||
else -> Log.d(TAG, "unsupported event: ${msg.responseType}")
|
||||
}
|
||||
}
|
||||
chatReceiver.value?.start()
|
||||
|
||||
val (res, error) = controller.downloadStandaloneFile(user, link, CryptoFile.plain(File(archivePath).path), ctrl)
|
||||
if (res == null) {
|
||||
state = MigrationState.DownloadFailed(totalBytes, link, archivePath, netCfg)
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.migration_from_device_error_downloading_archive),
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableState<MigrationState>.importArchive(archivePath: String, netCfg: NetCfg) {
|
||||
withLongRunningApi {
|
||||
try {
|
||||
if (ChatController.ctrl == null || ChatController.ctrl == -1L) {
|
||||
chatInitControllerRemovingDatabases()
|
||||
}
|
||||
controller.apiDeleteStorage()
|
||||
try {
|
||||
val config = ArchiveConfig(archivePath, parentTempDirectory = databaseExportDir.toString())
|
||||
val archiveErrors = controller.apiImportArchive(config)
|
||||
if (archiveErrors.isNotEmpty()) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.chat_database_imported),
|
||||
generalGetString(MR.strings.non_fatal_errors_occured_during_import)
|
||||
)
|
||||
}
|
||||
state = MigrationState.Passphrase("", netCfg)
|
||||
MigrationFromAnotherDeviceState.save(MigrationFromAnotherDeviceState.Passphrase(netCfg))
|
||||
} catch (e: Exception) {
|
||||
state = MigrationState.ArchiveImportFailed(archivePath, netCfg)
|
||||
AlertManager.shared.showAlertMsg (generalGetString(MR.strings.error_importing_database), e.stackTraceToString())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
state = MigrationState.ArchiveImportFailed(archivePath, netCfg)
|
||||
AlertManager.shared.showAlertMsg (generalGetString(MR.strings.error_deleting_database), e.stackTraceToString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun stopArchiveDownloading(fileId: Long, ctrl: ChatCtrl) {
|
||||
controller.apiCancelFile(null, fileId, ctrl)
|
||||
}
|
||||
|
||||
private fun startChat(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Boolean, netCfg: NetCfg, close: () -> Unit) {
|
||||
if (useKeychain) {
|
||||
ksDatabasePassword.set(passphrase)
|
||||
} else {
|
||||
ksDatabasePassword.remove()
|
||||
}
|
||||
appPreferences.storeDBPassphrase.set(useKeychain)
|
||||
appPreferences.initialRandomDBPassphrase.set(false)
|
||||
withBGApi {
|
||||
try {
|
||||
initChatController(useKey = passphrase, confirmMigrations = confirmation) { CompletableDeferred(false) }
|
||||
val appSettings = controller.apiGetAppSettings(AppSettings.current.prepareForExport()).copy(
|
||||
networkConfig = netCfg
|
||||
)
|
||||
finishMigration(appSettings, close)
|
||||
} catch (e: Exception) {
|
||||
hideView(close)
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_starting_chat), e.stackTraceToString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun finishMigration(appSettings: AppSettings, close: () -> Unit) {
|
||||
try {
|
||||
getMigrationTempFilesDirectory().deleteRecursively()
|
||||
appSettings.importIntoApp()
|
||||
val user = chatModel.currentUser.value
|
||||
if (user != null) {
|
||||
startChat(user)
|
||||
}
|
||||
hideView(close)
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.migration_from_device_chat_migrated), generalGetString(MR.strings.migration_from_device_finalize_migration))
|
||||
} catch (e: Exception) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_starting_chat), e.stackTraceToString())
|
||||
}
|
||||
MigrationFromAnotherDeviceState.save(null)
|
||||
}
|
||||
|
||||
private fun hideView(close: () -> Unit) {
|
||||
appPreferences.onboardingStage.set(OnboardingStage.OnboardingComplete)
|
||||
close()
|
||||
}
|
||||
|
||||
private suspend fun MutableState<MigrationState>.cleanUpOnBack(chatReceiver: MigrationFromChatReceiver?) {
|
||||
val state = state
|
||||
if (state is MigrationState.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) {
|
||||
stopArchiveDownloading(state.fileId, state.ctrl)
|
||||
}
|
||||
chatReceiver?.stopAndCleanUp()
|
||||
getMigrationTempFilesDirectory().deleteRecursively()
|
||||
MigrationFromAnotherDeviceState.save(null)
|
||||
}
|
||||
|
||||
private fun strHasSimplexFileLink(text: String): Boolean =
|
||||
text.startsWith("simplex:/file") || text.startsWith("https://simplex.chat/file")
|
||||
|
||||
private fun fileForTemporaryDatabase(): File =
|
||||
File(getMigrationTempFilesDirectory(), generateNewFileName("migration", "db", getMigrationTempFilesDirectory()))
|
||||
|
||||
private fun archivePath(): String {
|
||||
val archiveTime = Clock.System.now()
|
||||
val ts = SimpleDateFormat("yyyy-MM-dd'T'HHmmss", Locale.US).format(Date.from(archiveTime.toJavaInstant()))
|
||||
val archiveName = "simplex-chat.$ts.zip"
|
||||
val archivePath = File(getMigrationTempFilesDirectory(), archiveName)
|
||||
return archivePath.absolutePath
|
||||
}
|
||||
|
||||
private class MigrationFromChatReceiver(
|
||||
val ctrl: ChatCtrl,
|
||||
val databaseUrl: File,
|
||||
var receiveMessages: Boolean = true,
|
||||
val processReceivedMsg: suspend (CR) -> Unit
|
||||
) {
|
||||
fun start() {
|
||||
Log.d(TAG, "MigrationChatReceiver startReceiver")
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
while (receiveMessages) {
|
||||
try {
|
||||
val msg = ChatController.recvMsg(ctrl)
|
||||
if (msg != null && receiveMessages) {
|
||||
val r = msg.resp
|
||||
val rhId = msg.remoteHostId
|
||||
Log.d(TAG, "processReceivedMsg: ${r.responseType}")
|
||||
chatModel.addTerminalItem(TerminalItem.resp(rhId, r))
|
||||
val finishedWithoutTimeout = withTimeoutOrNull(60_000L) {
|
||||
processReceivedMsg(r)
|
||||
}
|
||||
if (finishedWithoutTimeout == null) {
|
||||
Log.e(TAG, "Timeout reached while processing received message: " + msg.resp.responseType)
|
||||
if (appPreferences.developerTools.get() && appPreferences.showSlowApiCalls.get()) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.possible_slow_function_title),
|
||||
text = generalGetString(MR.strings.possible_slow_function_desc).format(60, msg.resp.responseType + "\n" + Exception().stackTraceToString()),
|
||||
shareText = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "MigrationChatReceiver recvMsg/processReceivedMsg exception: " + e.stackTraceToString())
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "MigrationChatReceiver recvMsg/processReceivedMsg throwable: " + e.stackTraceToString())
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), e.stackTraceToString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stopAndCleanUp() {
|
||||
Log.d(TAG, "MigrationChatReceiver.stop")
|
||||
receiveMessages = false
|
||||
chatCloseStore(ctrl)
|
||||
File(databaseUrl.absolutePath + "_chat.db").delete()
|
||||
File(databaseUrl.absolutePath + "_agent.db").delete()
|
||||
}
|
||||
}
|
||||
+683
@@ -0,0 +1,683 @@
|
||||
package chat.simplex.common.views.migration
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionSpacer
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.getNetCfg
|
||||
import chat.simplex.common.model.ChatController.startChat
|
||||
import chat.simplex.common.model.ChatController.startChatWithTemporaryDatabase
|
||||
import chat.simplex.common.model.ChatCtrl
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.database.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.newchat.LinkTextView
|
||||
import chat.simplex.common.views.newchat.SimpleXLinkQRCode
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.datetime.*
|
||||
import kotlinx.serialization.*
|
||||
import java.io.File
|
||||
import java.net.URLEncoder
|
||||
import kotlin.math.max
|
||||
|
||||
@Serializable
|
||||
data class MigrationFileLinkData(
|
||||
val networkConfig: NetworkConfig?,
|
||||
) {
|
||||
@Serializable
|
||||
data class NetworkConfig(
|
||||
val socksProxy: String?,
|
||||
val hostMode: HostMode?,
|
||||
val requiredHostMode: Boolean?
|
||||
) {
|
||||
fun hasOnionConfigured(): Boolean = socksProxy != null || hostMode == HostMode.Onion
|
||||
|
||||
fun transformToPlatformSupported(): NetworkConfig {
|
||||
return if (hostMode != null && requiredHostMode != null) {
|
||||
NetworkConfig(
|
||||
socksProxy = if (hostMode == HostMode.Onion) socksProxy ?: NetCfg.proxyDefaults.socksProxy else socksProxy,
|
||||
hostMode = if (hostMode == HostMode.Onion) HostMode.OnionViaSocks else hostMode,
|
||||
requiredHostMode = requiredHostMode
|
||||
)
|
||||
} else this
|
||||
}
|
||||
}
|
||||
|
||||
fun addToLink(link: String) = link + "&data=" + URLEncoder.encode(jsonShort.encodeToString(this), "UTF-8")
|
||||
|
||||
companion object {
|
||||
suspend fun readFromLink(link: String): MigrationFileLinkData? =
|
||||
try {
|
||||
// val data = link.substringAfter("&data=").substringBefore("&")
|
||||
// json.decodeFromString(URLDecoder.decode(data, "UTF-8"))
|
||||
controller.standaloneFileInfo(link)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@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 var MutableState<MigrationToState>.state: MigrationToState
|
||||
get() = value
|
||||
set(v) { value = v }
|
||||
|
||||
@Composable
|
||||
fun MigrateToAnotherDeviceView(close: () -> Unit) {
|
||||
val migrationState = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf<MigrationToState>(MigrationToState.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
|
||||
}
|
||||
}
|
||||
val chatReceiver = remember { mutableStateOf(null as MigrationToChatReceiver?) }
|
||||
ModalView(
|
||||
enableClose = !backDisabled.value,
|
||||
close = {
|
||||
withBGApi {
|
||||
migrationState.cleanUpOnBack(chatReceiver.value)
|
||||
}
|
||||
close()
|
||||
},
|
||||
) {
|
||||
MigrateToAnotherDeviceLayout(
|
||||
migrationState = migrationState,
|
||||
chatReceiver = chatReceiver
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MigrateToAnotherDeviceLayout(
|
||||
migrationState: MutableState<MigrationToState>,
|
||||
chatReceiver: MutableState<MigrationToChatReceiver?>
|
||||
) {
|
||||
val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) }
|
||||
|
||||
Column(
|
||||
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).height(IntrinsicSize.Max),
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.migrate_to_device))
|
||||
SectionByState(migrationState, tempDatabaseFile.value, chatReceiver)
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
platform.androidLockPortraitOrientation()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SectionByState(
|
||||
migrationState: MutableState<MigrationToState>,
|
||||
tempDatabaseFile: File,
|
||||
chatReceiver: MutableState<MigrationToChatReceiver?>
|
||||
) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationToState>.ChatStopInProgressView() {
|
||||
Box {
|
||||
SectionView(stringResource(MR.strings.migration_to_device_stopping_chat).uppercase()) {}
|
||||
ProgressView()
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
stopChat()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationToState>.ChatStopFailedView(reason: String) {
|
||||
SectionView(stringResource(MR.strings.error_stopping_chat).uppercase()) {
|
||||
Text(reason)
|
||||
SectionSpacer()
|
||||
SettingsActionItemWithContent(
|
||||
icon = painterResource(MR.images.ic_report_filled),
|
||||
text = stringResource(MR.strings.auth_stop_chat),
|
||||
textColor = MaterialTheme.colors.error,
|
||||
click = ::stopChat
|
||||
){}
|
||||
SectionTextFooter(stringResource(MR.strings.migration_to_device_chat_should_be_stopped))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationToState>.PassphraseNotSetView() {
|
||||
DatabaseEncryptionView(chatModel, true)
|
||||
KeyChangeEffect(appPreferences.initialRandomDBPassphrase.state.value) {
|
||||
if (!appPreferences.initialRandomDBPassphrase.get()) {
|
||||
state = MigrationToState.UploadConfirmation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationToState>.PassphraseConfirmationView() {
|
||||
val useKeychain = remember { appPreferences.storeDBPassphrase.get() }
|
||||
val currentKey = rememberSaveable { mutableStateOf("") }
|
||||
val verifyingPassphrase = rememberSaveable { mutableStateOf(false) }
|
||||
Box {
|
||||
val view = LocalMultiplatformView()
|
||||
Column {
|
||||
ChatStoppedView()
|
||||
SectionSpacer()
|
||||
|
||||
SectionView(stringResource(MR.strings.migration_to_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),
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
disabled = verifyingPassphrase.value || currentKey.value.isEmpty(),
|
||||
click = {
|
||||
verifyingPassphrase.value = true
|
||||
hideKeyboard(view)
|
||||
withBGApi {
|
||||
verifyDatabasePassphrase(currentKey.value)
|
||||
verifyingPassphrase.value = false
|
||||
}
|
||||
}
|
||||
) {}
|
||||
SectionTextFooter(stringResource(MR.strings.migration_to_device_confirm_you_remember_passphrase))
|
||||
}
|
||||
}
|
||||
if (verifyingPassphrase.value) {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationToState>.UploadConfirmationView() {
|
||||
SectionView(stringResource(MR.strings.migration_to_device_confirm_upload).uppercase()) {
|
||||
SettingsActionItemWithContent(
|
||||
icon = painterResource(MR.images.ic_ios_share),
|
||||
text = stringResource(MR.strings.migration_to_device_archive_and_upload),
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
click = { state = MigrationToState.Archiving }
|
||||
){}
|
||||
SectionTextFooter(stringResource(MR.strings.migration_to_device_all_data_will_be_uploaded))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationToState>.ArchivingView() {
|
||||
Box {
|
||||
SectionView(stringResource(MR.strings.migration_to_device_archiving_database).uppercase()) {}
|
||||
ProgressView()
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
exportArchive()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationToState>.DatabaseInitView(tempDatabaseFile: File, totalBytes: Long, archivePath: String) {
|
||||
Box {
|
||||
SectionView(stringResource(MR.strings.migration_to_device_database_init).uppercase()) {}
|
||||
ProgressView()
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
prepareDatabase(tempDatabaseFile, totalBytes, archivePath)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationToState>.UploadProgressView(
|
||||
uploadedBytes: Long,
|
||||
totalBytes: Long,
|
||||
ctrl: ChatCtrl,
|
||||
user: User,
|
||||
tempDatabaseFile: File,
|
||||
chatReceiver: MutableState<MigrationToChatReceiver?>,
|
||||
archivePath: String,
|
||||
) {
|
||||
Box {
|
||||
SectionView(stringResource(MR.strings.migration_to_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)))
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
startUploading(totalBytes, ctrl, user, tempDatabaseFile, chatReceiver, archivePath)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationToState>.UploadFailedView(totalBytes: Long, archivePath: String, chatReceiver: MigrationToChatReceiver?) {
|
||||
SectionView(stringResource(MR.strings.migration_to_device_upload_failed).uppercase()) {
|
||||
SettingsActionItemWithContent(
|
||||
icon = painterResource(MR.images.ic_ios_share),
|
||||
text = stringResource(MR.strings.migration_to_device_repeat_upload),
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
click = {
|
||||
state = MigrationToState.DatabaseInit(totalBytes, archivePath)
|
||||
}
|
||||
) {}
|
||||
SectionTextFooter(stringResource(MR.strings.migration_to_device_try_again))
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
chatReceiver?.stopAndCleanUp()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LinkCreationView() {
|
||||
Box {
|
||||
SectionView(stringResource(MR.strings.migration_to_device_creating_archive_link).uppercase()) {}
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationToState>.LinkShownView(fileId: Long, link: String, ctrl: ChatCtrl) {
|
||||
SectionView {
|
||||
SettingsActionItemWithContent(
|
||||
icon = painterResource(MR.images.ic_close),
|
||||
text = stringResource(MR.strings.migration_to_device_cancel_migration),
|
||||
textColor = MaterialTheme.colors.error,
|
||||
click = {
|
||||
cancelMigration(fileId, ctrl)
|
||||
}
|
||||
) {}
|
||||
SettingsActionItemWithContent(
|
||||
icon = painterResource(MR.images.ic_check),
|
||||
text = stringResource(MR.strings.migration_to_device_finalize_migration),
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
click = {
|
||||
finishMigration(fileId, ctrl)
|
||||
}
|
||||
) {}
|
||||
SectionTextFooter(annotatedStringResource(MR.strings.migration_to_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()) {
|
||||
LinkTextView(link, true)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationToState>.FinishedView(chatDeletion: Boolean) {
|
||||
Box {
|
||||
SectionView(stringResource(MR.strings.migration_to_device_migration_complete).uppercase()) {
|
||||
SettingsActionItemWithContent(
|
||||
icon = painterResource(MR.images.ic_delete_forever),
|
||||
text = stringResource(MR.strings.migration_to_device_delete_database_from_device),
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
click = {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.delete_chat_profile_question),
|
||||
text = generalGetString(MR.strings.delete_chat_profile_action_cannot_be_undone_warning),
|
||||
confirmText = generalGetString(MR.strings.delete_verb),
|
||||
onConfirm = {
|
||||
deleteChatAndDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
) {}
|
||||
|
||||
SettingsActionItemWithContent(
|
||||
icon = painterResource(MR.images.ic_play_arrow_filled),
|
||||
text = stringResource(MR.strings.migration_to_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),
|
||||
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))
|
||||
}
|
||||
if (chatDeletion) {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProgressView() {
|
||||
DefaultProgressView(null)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LargeProgressView(value: Float, title: String, description: String) {
|
||||
Box(Modifier.padding(DEFAULT_PADDING).fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator(
|
||||
progress = value,
|
||||
(if (appPlatform.isDesktop) Modifier.size(DEFAULT_START_MODAL_WIDTH) else Modifier.size(windowWidth() - DEFAULT_PADDING * 2))
|
||||
.rotate(-90f),
|
||||
color = MaterialTheme.colors.primary,
|
||||
strokeWidth = 25.dp
|
||||
)
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(description, color = Color.Transparent)
|
||||
Text(title, style = MaterialTheme.typography.h1.copy(fontSize = 50.sp, fontWeight = FontWeight.Bold), color = MaterialTheme.colors.primary)
|
||||
Text(description, style = MaterialTheme.typography.subtitle1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableState<MigrationToState>.stopChat() {
|
||||
withBGApi {
|
||||
try {
|
||||
stopChatAsync(chatModel)
|
||||
try {
|
||||
controller.apiSaveAppSettings(AppSettings.current.prepareForExport())
|
||||
state = if (appPreferences.initialRandomDBPassphrase.get()) MigrationToState.PassphraseNotSet else MigrationToState.PassphraseConfirmation
|
||||
} catch (e: Exception) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.migrate_to_device_error_saving_settings),
|
||||
text = e.stackTraceToString()
|
||||
)
|
||||
state = MigrationToState.ChatStopFailed(reason = generalGetString(MR.strings.migrate_to_device_error_saving_settings))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
state = MigrationToState.ChatStopFailed(reason = e.stackTraceToString().take(10))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun MutableState<MigrationToState>.verifyDatabasePassphrase(dbKey: String) {
|
||||
if (controller.testStorageEncryption(dbKey)) {
|
||||
state = MigrationToState.UploadConfirmation
|
||||
} else {
|
||||
showErrorOnMigrationIfNeeded(DBMigrationResult.ErrorNotADatabase(""))
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableState<MigrationToState>.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)
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.migrate_to_device_exported_file_doesnt_exist))
|
||||
state = MigrationToState.UploadConfirmation
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.migrate_to_device_error_exporting_archive),
|
||||
text = e.stackTraceToString()
|
||||
)
|
||||
state = MigrationToState.UploadConfirmation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun initTemporaryDatabase(tempDatabaseFile: File, netCfg: NetCfg): Pair<ChatCtrl, User>? {
|
||||
val (status, ctrl) = chatInitTemporaryDatabase(tempDatabaseFile.absolutePath)
|
||||
showErrorOnMigrationIfNeeded(status)
|
||||
try {
|
||||
if (ctrl != null) {
|
||||
val user = startChatWithTemporaryDatabase(ctrl, netCfg)
|
||||
return if (user != null) ctrl to user else null
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Error while starting chat in temporary database: ${e.stackTraceToString()}")
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun MutableState<MigrationToState>.prepareDatabase(
|
||||
tempDatabaseFile: File,
|
||||
totalBytes: Long,
|
||||
archivePath: String,
|
||||
) {
|
||||
withLongRunningApi {
|
||||
val ctrlAndUser = initTemporaryDatabase(tempDatabaseFile, getNetCfg())
|
||||
if (ctrlAndUser == null) {
|
||||
state = MigrationToState.UploadFailed(totalBytes, archivePath)
|
||||
return@withLongRunningApi
|
||||
}
|
||||
|
||||
val (ctrl, user) = ctrlAndUser
|
||||
state = MigrationToState.UploadProgress(0L, totalBytes, 0L, archivePath, ctrl, user)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableState<MigrationToState>.startUploading(
|
||||
totalBytes: Long,
|
||||
ctrl: ChatCtrl,
|
||||
user: User,
|
||||
tempDatabaseFile: File,
|
||||
chatReceiver: MutableState<MigrationToChatReceiver?>,
|
||||
archivePath: String,
|
||||
) {
|
||||
withBGApi {
|
||||
chatReceiver.value = MigrationToChatReceiver(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)
|
||||
}
|
||||
}
|
||||
is CR.SndFileRedirectStartXFTP -> {
|
||||
delay(500)
|
||||
state = MigrationToState.LinkCreation
|
||||
}
|
||||
is CR.SndStandaloneFileComplete -> {
|
||||
delay(500)
|
||||
val cfg = getNetCfg()
|
||||
val data = MigrationFileLinkData(
|
||||
networkConfig = MigrationFileLinkData.NetworkConfig(
|
||||
socksProxy = cfg.socksProxy,
|
||||
hostMode = cfg.hostMode,
|
||||
requiredHostMode = cfg.requiredHostMode
|
||||
)
|
||||
)
|
||||
state = MigrationToState.LinkShown(msg.fileTransferMeta.fileId, data.addToLink(msg.rcvURIs[0]), ctrl)
|
||||
}
|
||||
else -> {
|
||||
Log.d(TAG, "unsupported event: ${msg.responseType}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chatReceiver.value?.start()
|
||||
|
||||
val (res, error) = controller.uploadStandaloneFile(user, CryptoFile.plain(File(archivePath).name), ctrl)
|
||||
if (res == null) {
|
||||
state = MigrationToState.UploadFailed(totalBytes, archivePath)
|
||||
return@withBGApi AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.migration_to_device_error_uploading_archive),
|
||||
error
|
||||
)
|
||||
}
|
||||
state = MigrationToState.UploadProgress(0, res.fileSize, res.fileId, archivePath, ctrl, user)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun cancelUploadedArchive(fileId: Long, ctrl: ChatCtrl) {
|
||||
controller.apiCancelFile(null, fileId, ctrl)
|
||||
}
|
||||
|
||||
private fun cancelMigration(fileId: Long, ctrl: ChatCtrl) {
|
||||
withBGApi {
|
||||
cancelUploadedArchive(fileId, ctrl)
|
||||
startChatAndDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableState<MigrationToState>.finishMigration(fileId: Long, ctrl: ChatCtrl) {
|
||||
withBGApi {
|
||||
cancelUploadedArchive(fileId, ctrl)
|
||||
state = MigrationToState.Finished(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableState<MigrationToState>.deleteChatAndDismiss() {
|
||||
withBGApi {
|
||||
try {
|
||||
deleteChatAsync(chatModel)
|
||||
chatModel.chatDbChanged.value = true
|
||||
state = MigrationToState.Finished(true)
|
||||
try {
|
||||
initChatController(startChat = { CompletableDeferred(false) })
|
||||
chatModel.chatDbChanged.value = false
|
||||
ModalManager.fullscreen.closeModals()
|
||||
} catch (e: Exception) {
|
||||
throw Exception(generalGetString(MR.strings.error_starting_chat) + "\n" + e.stackTraceToString())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.migration_to_device_error_deleting_database),
|
||||
text = e.stackTraceToString()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun startChatAndDismiss(dismiss: Boolean = true) {
|
||||
try {
|
||||
val user = chatModel.currentUser.value
|
||||
if (chatModel.chatDbChanged.value) {
|
||||
initChatController()
|
||||
chatModel.chatDbChanged.value = false
|
||||
} else if (user != null) {
|
||||
startChat(user)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.error_starting_chat),
|
||||
text = e.stackTraceToString()
|
||||
)
|
||||
}
|
||||
// Hide settings anyway if chatDbStatus is not ok, probably passphrase needs to be entered
|
||||
if (dismiss || chatModel.chatDbStatus.value != DBMigrationResult.OK) {
|
||||
ModalManager.fullscreen.closeModals()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun MutableState<MigrationToState>.cleanUpOnBack(chatReceiver: MigrationToChatReceiver?) {
|
||||
val s = state
|
||||
if (s !is MigrationToState.LinkShown && s !is MigrationToState.Finished) {
|
||||
chatModel.switchingUsersAndHosts.value = true
|
||||
startChatAndDismiss(false)
|
||||
chatModel.switchingUsersAndHosts.value = false
|
||||
}
|
||||
if (s is MigrationToState.UploadProgress) {
|
||||
cancelUploadedArchive(s.fileId, s.ctrl)
|
||||
}
|
||||
chatReceiver?.stopAndCleanUp()
|
||||
getMigrationTempFilesDirectory().deleteRecursively()
|
||||
}
|
||||
|
||||
private fun fileForTemporaryDatabase(): File =
|
||||
File(getMigrationTempFilesDirectory(), generateNewFileName("migration", "db", getMigrationTempFilesDirectory()))
|
||||
|
||||
private class MigrationToChatReceiver(
|
||||
val ctrl: ChatCtrl,
|
||||
val databaseUrl: File,
|
||||
var receiveMessages: Boolean = true,
|
||||
val processReceivedMsg: suspend (CR) -> Unit
|
||||
) {
|
||||
fun start() {
|
||||
Log.d(TAG, "MigrationChatReceiver startReceiver")
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
while (receiveMessages) {
|
||||
try {
|
||||
val msg = ChatController.recvMsg(ctrl)
|
||||
if (msg != null && receiveMessages) {
|
||||
val r = msg.resp
|
||||
val rhId = msg.remoteHostId
|
||||
Log.d(TAG, "processReceivedMsg: ${r.responseType}")
|
||||
chatModel.addTerminalItem(TerminalItem.resp(rhId, r))
|
||||
val finishedWithoutTimeout = withTimeoutOrNull(60_000L) {
|
||||
processReceivedMsg(r)
|
||||
}
|
||||
if (finishedWithoutTimeout == null) {
|
||||
Log.e(TAG, "Timeout reached while processing received message: " + msg.resp.responseType)
|
||||
if (appPreferences.developerTools.get() && appPreferences.showSlowApiCalls.get()) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.possible_slow_function_title),
|
||||
text = generalGetString(MR.strings.possible_slow_function_desc).format(60, msg.resp.responseType + "\n" + Exception().stackTraceToString()),
|
||||
shareText = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "MigrationChatReceiver recvMsg/processReceivedMsg exception: " + e.stackTraceToString())
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "MigrationChatReceiver recvMsg/processReceivedMsg throwable: " + e.stackTraceToString())
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), e.stackTraceToString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stopAndCleanUp() {
|
||||
Log.d(TAG, "MigrationChatReceiver.stop")
|
||||
receiveMessages = false
|
||||
chatCloseStore(ctrl)
|
||||
File(databaseUrl.absolutePath + "_chat.db").delete()
|
||||
File(databaseUrl.absolutePath + "_agent.db").delete()
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -301,7 +301,7 @@ private fun PasteLinkView(rhId: Long?, pastedLink: MutableState<String>, showQRC
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LinkTextView(link: String, share: Boolean) {
|
||||
fun LinkTextView(link: String, share: Boolean) {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
Row(Modifier.fillMaxWidth().heightIn(min = 46.dp).padding(horizontal = DEFAULT_PADDING), verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(Modifier.weight(1f).clickable {
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ fun SetupDatabasePassphrase(m: ChatModel) {
|
||||
prefs.storeDBPassphrase.set(false)
|
||||
|
||||
val newKeyValue = newKey.value
|
||||
val success = encryptDatabase(currentKey, newKey, confirmNewKey, mutableStateOf(true), saveInPreferences, mutableStateOf(true), progressIndicator)
|
||||
val success = encryptDatabase(currentKey, newKey, confirmNewKey, mutableStateOf(true), saveInPreferences, mutableStateOf(true), progressIndicator, false)
|
||||
if (success) {
|
||||
startChat(newKeyValue)
|
||||
nextStep()
|
||||
|
||||
+19
-2
@@ -5,7 +5,7 @@ import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -16,8 +16,10 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.*
|
||||
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.res.MR
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
|
||||
@@ -62,17 +64,32 @@ fun SimpleXInfoLayout(
|
||||
OnboardingActionButton(user, onboardingStage)
|
||||
}
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.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) } })
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = DEFAULT_PADDING.times(1.5f), top = DEFAULT_PADDING), contentAlignment = Alignment.Center
|
||||
.padding(bottom = DEFAULT_PADDING.times(1.5f), top = if (onboardingStage == null) DEFAULT_PADDING else 0.dp), contentAlignment = Alignment.Center
|
||||
) {
|
||||
SimpleButtonDecorated(text = stringResource(MR.strings.how_it_works), icon = painterResource(MR.images.ic_info),
|
||||
click = showModal { HowItWorks(user, onboardingStage) })
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
val state = chatModel.migrationState.value
|
||||
if (state != null && !ModalManager.fullscreen.hasModalsOpen()) {
|
||||
ModalManager.fullscreen.showCustomModal(animated = false) { close -> MigrateFromAnotherDeviceView(state, close) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
+95
-61
@@ -33,12 +33,7 @@ import chat.simplex.common.views.helpers.annotatedStringResource
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun NetworkAndServersView(
|
||||
chatModel: ChatModel,
|
||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
|
||||
) {
|
||||
fun NetworkAndServersView() {
|
||||
val currentRemoteHost by remember { chatModel.currentRemoteHost }
|
||||
// It's not a state, just a one-time value. Shouldn't be used in any state-related situations
|
||||
val netCfg = remember { chatModel.controller.getNetCfg() }
|
||||
@@ -55,9 +50,6 @@ fun NetworkAndServersView(
|
||||
onionHosts = onionHosts,
|
||||
sessionMode = sessionMode,
|
||||
proxyPort = proxyPort,
|
||||
showModal = showModal,
|
||||
showSettingsModal = showSettingsModal,
|
||||
showCustomModal = showCustomModal,
|
||||
toggleSocksProxy = { enable ->
|
||||
if (enable) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
@@ -154,13 +146,11 @@ fun NetworkAndServersView(
|
||||
onionHosts: MutableState<OnionHosts>,
|
||||
sessionMode: MutableState<TransportSessionMode>,
|
||||
proxyPort: State<Int>,
|
||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
|
||||
toggleSocksProxy: (Boolean) -> Unit,
|
||||
useOnion: (OnionHosts) -> Unit,
|
||||
updateSessionMode: (TransportSessionMode) -> Unit,
|
||||
) {
|
||||
val m = chatModel
|
||||
Column(
|
||||
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
@@ -168,17 +158,18 @@ fun NetworkAndServersView(
|
||||
AppBarTitle(stringResource(MR.strings.network_and_servers))
|
||||
if (!chatModel.desktopNoUserNoRemote) {
|
||||
SectionView(generalGetString(MR.strings.settings_section_title_messages)) {
|
||||
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.smp_servers), showCustomModal { m, close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.SMP, close) })
|
||||
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.smp_servers), { ModalManager.start.showCustomModal { close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.SMP, close) } })
|
||||
|
||||
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.xftp_servers), showCustomModal { m, close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.XFTP, close) })
|
||||
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.xftp_servers), { ModalManager.start.showCustomModal { close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.XFTP, close) } })
|
||||
|
||||
if (currentRemoteHost == null) {
|
||||
UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showSettingsModal)
|
||||
UseOnionHosts(onionHosts, networkUseSocksProxy, showSettingsModal, useOnion)
|
||||
val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.start.showModal(content = it) }
|
||||
UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showModal, chatModel.controller.appPrefs.networkProxyHostPort, false)
|
||||
UseOnionHosts(onionHosts, networkUseSocksProxy, showModal, useOnion)
|
||||
if (developerTools) {
|
||||
SessionModePicker(sessionMode, showSettingsModal, updateSessionMode)
|
||||
SessionModePicker(sessionMode, showModal, updateSessionMode)
|
||||
}
|
||||
SettingsActionItem(painterResource(MR.images.ic_cable), stringResource(MR.strings.network_settings), showSettingsModal { AdvancedNetworkSettingsView(it) })
|
||||
SettingsActionItem(painterResource(MR.images.ic_cable), stringResource(MR.strings.network_settings), { ModalManager.start.showModal { AdvancedNetworkSettingsView(m) } })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -196,18 +187,39 @@ fun NetworkAndServersView(
|
||||
}
|
||||
|
||||
SectionView(generalGetString(MR.strings.settings_section_title_calls)) {
|
||||
SettingsActionItem(painterResource(MR.images.ic_electrical_services), stringResource(MR.strings.webrtc_ice_servers), showModal { RTCServersView(it) })
|
||||
SettingsActionItem(painterResource(MR.images.ic_electrical_services), stringResource(MR.strings.webrtc_ice_servers), { ModalManager.start.showModal { RTCServersView(m) } })
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable fun OnionRelatedLayout(
|
||||
developerTools: Boolean,
|
||||
networkUseSocksProxy: MutableState<Boolean>,
|
||||
onionHosts: MutableState<OnionHosts>,
|
||||
sessionMode: MutableState<TransportSessionMode>,
|
||||
networkProxyHostPort: SharedPreference<String?>,
|
||||
proxyPort: State<Int>,
|
||||
toggleSocksProxy: (Boolean) -> Unit,
|
||||
useOnion: (OnionHosts) -> Unit,
|
||||
updateSessionMode: (TransportSessionMode) -> Unit,
|
||||
) {
|
||||
val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.fullscreen.showModal(content = it) }
|
||||
UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showModal, networkProxyHostPort, true)
|
||||
UseOnionHosts(onionHosts, networkUseSocksProxy, showModal, useOnion)
|
||||
if (developerTools) {
|
||||
SessionModePicker(sessionMode, showModal, updateSessionMode)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UseSocksProxySwitch(
|
||||
networkUseSocksProxy: MutableState<Boolean>,
|
||||
proxyPort: State<Int>,
|
||||
toggleSocksProxy: (Boolean) -> Unit,
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)
|
||||
showModal: (@Composable ModalData.() -> Unit) -> Unit,
|
||||
networkProxyHostPort: SharedPreference<String?> = chatModel.controller.appPrefs.networkProxyHostPort,
|
||||
migration: Boolean = false,
|
||||
) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(end = DEFAULT_PADDING),
|
||||
@@ -227,8 +239,11 @@ fun UseSocksProxySwitch(
|
||||
val text = buildAnnotatedString {
|
||||
append(generalGetString(MR.strings.network_socks_toggle_use_socks_proxy) + " (")
|
||||
val style = SpanStyle(color = MaterialTheme.colors.primary)
|
||||
val disabledStyle = SpanStyle(color = MaterialTheme.colors.onBackground)
|
||||
withAnnotation(tag = "PORT", annotation = generalGetString(MR.strings.network_proxy_port).format(proxyPort.value)) {
|
||||
withStyle(style) { append(generalGetString(MR.strings.network_proxy_port).format(proxyPort.value)) }
|
||||
withStyle(if (networkUseSocksProxy.value || !migration) style else disabledStyle) {
|
||||
append(generalGetString(MR.strings.network_proxy_port).format(proxyPort.value))
|
||||
}
|
||||
}
|
||||
append(")")
|
||||
}
|
||||
@@ -238,7 +253,9 @@ fun UseSocksProxySwitch(
|
||||
onClick = { offset ->
|
||||
text.getStringAnnotations(tag = "PORT", start = offset, end = offset)
|
||||
.firstOrNull()?.let { _ ->
|
||||
showSettingsModal { SockProxySettings(it) }()
|
||||
if (networkUseSocksProxy.value || !migration) {
|
||||
showModal { SockProxySettings(chatModel, networkProxyHostPort, migration) }
|
||||
}
|
||||
}
|
||||
},
|
||||
shouldConsumeEvent = { offset ->
|
||||
@@ -254,7 +271,11 @@ fun UseSocksProxySwitch(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SockProxySettings(m: ChatModel) {
|
||||
fun SockProxySettings(
|
||||
m: ChatModel,
|
||||
networkProxyHostPort: SharedPreference<String?> = m.controller.appPrefs.networkProxyHostPort,
|
||||
migration: Boolean,
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -262,17 +283,17 @@ fun SockProxySettings(m: ChatModel) {
|
||||
) {
|
||||
val defaultHostPort = remember { "localhost:9050" }
|
||||
AppBarTitle(generalGetString(MR.strings.network_socks_proxy_settings))
|
||||
val hostPort by remember { m.controller.appPrefs.networkProxyHostPort.state }
|
||||
val hostPortSaved by remember { networkProxyHostPort.state }
|
||||
val hostUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue(hostPort?.split(":")?.firstOrNull() ?: "localhost"))
|
||||
mutableStateOf(TextFieldValue(hostPortSaved?.split(":")?.firstOrNull() ?: "localhost"))
|
||||
}
|
||||
val portUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue(hostPort?.split(":")?.lastOrNull() ?: "9050"))
|
||||
mutableStateOf(TextFieldValue(hostPortSaved?.split(":")?.lastOrNull() ?: "9050"))
|
||||
}
|
||||
val save = {
|
||||
withBGApi {
|
||||
m.controller.appPrefs.networkProxyHostPort.set(hostUnsaved.value.text + ":" + portUnsaved.value.text)
|
||||
if (m.controller.appPrefs.networkUseSocksProxy.get()) {
|
||||
networkProxyHostPort.set(hostUnsaved.value.text + ":" + portUnsaved.value.text)
|
||||
if (m.controller.appPrefs.networkUseSocksProxy.get() && !migration) {
|
||||
m.controller.apiSetNetworkConfig(m.controller.getNetCfg())
|
||||
}
|
||||
}
|
||||
@@ -281,21 +302,21 @@ fun SockProxySettings(m: ChatModel) {
|
||||
SectionItemView {
|
||||
ResetToDefaultsButton({
|
||||
val reset = {
|
||||
m.controller.appPrefs.networkProxyHostPort.set(defaultHostPort)
|
||||
networkProxyHostPort.set(defaultHostPort)
|
||||
val newHost = defaultHostPort.split(":").first()
|
||||
val newPort = defaultHostPort.split(":").last()
|
||||
hostUnsaved.value = hostUnsaved.value.copy(newHost, TextRange(newHost.length))
|
||||
portUnsaved.value = portUnsaved.value.copy(newPort, TextRange(newPort.length))
|
||||
save()
|
||||
}
|
||||
if (m.controller.appPrefs.networkUseSocksProxy.get()) {
|
||||
if (m.controller.appPrefs.networkUseSocksProxy.get() && !migration) {
|
||||
showUpdateNetworkSettingsDialog {
|
||||
reset()
|
||||
}
|
||||
} else {
|
||||
reset()
|
||||
}
|
||||
}, disabled = hostPort == defaultHostPort)
|
||||
}, disabled = hostPortSaved == defaultHostPort)
|
||||
}
|
||||
SectionItemView {
|
||||
DefaultConfigurableTextField(
|
||||
@@ -321,14 +342,14 @@ fun SockProxySettings(m: ChatModel) {
|
||||
SectionCustomFooter {
|
||||
NetworkSectionFooter(
|
||||
revert = {
|
||||
val prevHost = m.controller.appPrefs.networkProxyHostPort.get()?.split(":")?.firstOrNull() ?: "localhost"
|
||||
val prevPort = m.controller.appPrefs.networkProxyHostPort.get()?.split(":")?.lastOrNull() ?: "9050"
|
||||
val prevHost = hostPortSaved?.split(":")?.firstOrNull() ?: "localhost"
|
||||
val prevPort = hostPortSaved?.split(":")?.lastOrNull() ?: "9050"
|
||||
hostUnsaved.value = hostUnsaved.value.copy(prevHost, TextRange(prevHost.length))
|
||||
portUnsaved.value = portUnsaved.value.copy(prevPort, TextRange(prevPort.length))
|
||||
},
|
||||
save = { if (m.controller.appPrefs.networkUseSocksProxy.get()) showUpdateNetworkSettingsDialog { save() } else save() },
|
||||
revertDisabled = hostPort == (hostUnsaved.value.text + ":" + portUnsaved.value.text),
|
||||
saveDisabled = hostPort == (hostUnsaved.value.text + ":" + portUnsaved.value.text) ||
|
||||
save = { if (m.controller.appPrefs.networkUseSocksProxy.get() && !migration) showUpdateNetworkSettingsDialog { save() } else save() },
|
||||
revertDisabled = hostPortSaved == (hostUnsaved.value.text + ":" + portUnsaved.value.text),
|
||||
saveDisabled = hostPortSaved == (hostUnsaved.value.text + ":" + portUnsaved.value.text) ||
|
||||
remember { derivedStateOf { !validHost(hostUnsaved.value.text) } }.value ||
|
||||
remember { derivedStateOf { !validPort(portUnsaved.value.text) } }.value
|
||||
)
|
||||
@@ -341,7 +362,7 @@ fun SockProxySettings(m: ChatModel) {
|
||||
private fun UseOnionHosts(
|
||||
onionHosts: MutableState<OnionHosts>,
|
||||
enabled: State<Boolean>,
|
||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showModal: (@Composable ModalData.() -> Unit) -> Unit,
|
||||
useOnion: (OnionHosts) -> Unit,
|
||||
) {
|
||||
val values = remember {
|
||||
@@ -353,29 +374,43 @@ private fun UseOnionHosts(
|
||||
}
|
||||
}
|
||||
}
|
||||
val onSelected = showModal {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.network_use_onion_hosts))
|
||||
SectionViewSelectable(null, onionHosts, values, useOnion)
|
||||
val onSelected = {
|
||||
showModal {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.network_use_onion_hosts))
|
||||
SectionViewSelectable(null, onionHosts, values, useOnion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionItemWithValue(
|
||||
generalGetString(MR.strings.network_use_onion_hosts),
|
||||
onionHosts,
|
||||
values,
|
||||
icon = painterResource(MR.images.ic_security),
|
||||
enabled = enabled,
|
||||
onSelected = onSelected
|
||||
)
|
||||
if (enabled.value) {
|
||||
SectionItemWithValue(
|
||||
generalGetString(MR.strings.network_use_onion_hosts),
|
||||
onionHosts,
|
||||
values,
|
||||
icon = painterResource(MR.images.ic_security),
|
||||
enabled = enabled,
|
||||
onSelected = onSelected
|
||||
)
|
||||
} else {
|
||||
// In reality, when socks proxy is disabled, this option acts like NEVER regardless of what was chosen before
|
||||
SectionItemWithValue(
|
||||
generalGetString(MR.strings.network_use_onion_hosts),
|
||||
remember { mutableStateOf(OnionHosts.NEVER) },
|
||||
listOf(ValueTitleDesc(OnionHosts.NEVER, generalGetString(MR.strings.network_use_onion_hosts_no), AnnotatedString(generalGetString(MR.strings.network_use_onion_hosts_no_desc)))),
|
||||
icon = painterResource(MR.images.ic_security),
|
||||
enabled = enabled,
|
||||
onSelected = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SessionModePicker(
|
||||
sessionMode: MutableState<TransportSessionMode>,
|
||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showModal: (@Composable ModalData.() -> Unit) -> Unit,
|
||||
updateSessionMode: (TransportSessionMode) -> Unit,
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
@@ -393,12 +428,14 @@ private fun SessionModePicker(
|
||||
sessionMode,
|
||||
values,
|
||||
icon = painterResource(MR.images.ic_safety_divider),
|
||||
onSelected = showModal {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.network_session_mode_transport_isolation))
|
||||
SectionViewSelectable(null, sessionMode, values, updateSessionMode)
|
||||
onSelected = {
|
||||
showModal {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.network_session_mode_transport_isolation))
|
||||
SectionViewSelectable(null, sessionMode, values, updateSessionMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -455,9 +492,6 @@ fun PreviewNetworkAndServersLayout() {
|
||||
developerTools = true,
|
||||
networkUseSocksProxy = remember { mutableStateOf(true) },
|
||||
proxyPort = remember { mutableStateOf(9050) },
|
||||
showModal = { {} },
|
||||
showSettingsModal = { {} },
|
||||
showCustomModal = { {} },
|
||||
toggleSocksProxy = {},
|
||||
onionHosts = remember { mutableStateOf(OnionHosts.PREFER) },
|
||||
sessionMode = remember { mutableStateOf(TransportSessionMode.User) },
|
||||
|
||||
+6
-3
@@ -28,6 +28,8 @@ 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.onboarding.SimpleXInfo
|
||||
import chat.simplex.common.views.onboarding.WhatsNewView
|
||||
import chat.simplex.common.views.remote.ConnectDesktopView
|
||||
@@ -135,12 +137,13 @@ 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)
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView(stringResource(MR.strings.settings_section_title_settings)) {
|
||||
SettingsActionItem(painterResource(if (notificationsMode.value == NotificationsMode.OFF) MR.images.ic_bolt_off else MR.images.ic_bolt), stringResource(MR.strings.notifications), showSettingsModal { NotificationsSettingsView(it) }, disabled = stopped, extraPadding = true)
|
||||
SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showSettingsModal { NetworkAndServersView(it, showModal, showSettingsModal, showCustomModal) }, disabled = stopped, extraPadding = true)
|
||||
SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showSettingsModal { NetworkAndServersView() }, disabled = stopped, extraPadding = true)
|
||||
SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped, extraPadding = true)
|
||||
SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped, extraPadding = true)
|
||||
SettingsActionItem(painterResource(MR.images.ic_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it, showSettingsModal) }, extraPadding = true)
|
||||
@@ -366,7 +369,7 @@ fun SettingsActionItem(icon: Painter, text: String, click: (() -> Unit)? = null,
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsActionItemWithContent(icon: Painter?, text: String? = null, click: (() -> Unit)? = null, iconColor: Color = MaterialTheme.colors.secondary, disabled: Boolean = false, extraPadding: Boolean = false, content: @Composable RowScope.() -> Unit) {
|
||||
fun SettingsActionItemWithContent(icon: Painter?, text: String? = null, click: (() -> Unit)? = null, iconColor: Color = MaterialTheme.colors.secondary, textColor: Color = MaterialTheme.colors.onBackground, disabled: Boolean = false, extraPadding: Boolean = false, content: @Composable RowScope.() -> Unit) {
|
||||
SectionItemView(
|
||||
click,
|
||||
extraPadding = extraPadding,
|
||||
@@ -382,7 +385,7 @@ fun SettingsActionItemWithContent(icon: Painter?, text: String? = null, click: (
|
||||
}
|
||||
if (text != null) {
|
||||
val padding = with(LocalDensity.current) { 6.sp.toDp() }
|
||||
Text(text, Modifier.weight(1f).padding(vertical = padding), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.onBackground)
|
||||
Text(text, Modifier.weight(1f).padding(vertical = padding), color = if (disabled) MaterialTheme.colors.secondary else textColor)
|
||||
Spacer(Modifier.width(DEFAULT_PADDING))
|
||||
Row(Modifier.widthIn(max = (windowWidth() - DEFAULT_PADDING * 2) / 2)) {
|
||||
content()
|
||||
|
||||
@@ -245,6 +245,7 @@
|
||||
<string name="auth_stop_chat">Stop chat</string>
|
||||
<string name="auth_open_chat_console">Open chat console</string>
|
||||
<string name="auth_open_chat_profiles">Open chat profiles</string>
|
||||
<string name="auth_open_migration_to_another_device">Open migration screen</string>
|
||||
<string name="lock_not_enabled">SimpleX Lock not enabled!</string>
|
||||
<string name="you_can_turn_on_lock">You can turn on SimpleX Lock via Settings.</string>
|
||||
|
||||
@@ -823,6 +824,7 @@
|
||||
<string name="opensource_protocol_and_code_anybody_can_run_servers">Open-source protocol and code – anybody can run the servers.</string>
|
||||
<string name="create_your_profile">Create your profile</string>
|
||||
<string name="make_private_connection">Make a private connection</string>
|
||||
<string name="migrate_from_another_device">Migrate from another device</string>
|
||||
<string name="how_it_works">How it works</string>
|
||||
|
||||
<!-- How SimpleX Works -->
|
||||
@@ -1081,6 +1083,7 @@
|
||||
<string name="confirm_new_passphrase">Confirm new passphrase…</string>
|
||||
<string name="update_database_passphrase">Update database passphrase</string>
|
||||
<string name="set_database_passphrase">Set database passphrase</string>
|
||||
<string name="set_passphrase">Set passphrase</string>
|
||||
<string name="enter_correct_current_passphrase">Please enter correct current passphrase.</string>
|
||||
<string name="database_is_not_encrypted">Your chat database is not encrypted - set passphrase to protect it.</string>
|
||||
<string name="keychain_is_storing_securely">Android Keystore is used to securely store passphrase - it allows notification service to work.</string>
|
||||
@@ -1842,4 +1845,64 @@
|
||||
<string name="agent_internal_error_title">Internal error</string>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<!-- 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>
|
||||
</resources>
|
||||
+11
-4
@@ -2,6 +2,7 @@ package chat.simplex.common.views.database
|
||||
|
||||
import SectionItemView
|
||||
import SectionTextFooter
|
||||
import TextIconSpaced
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -22,8 +23,9 @@ actual fun SavePassphraseSetting(
|
||||
useKeychain: Boolean,
|
||||
initialRandomDBPassphrase: Boolean,
|
||||
storedKey: Boolean,
|
||||
progressIndicator: Boolean,
|
||||
minHeight: Dp,
|
||||
enabled: Boolean,
|
||||
smallPadding: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
) {
|
||||
SectionItemView(minHeight = minHeight) {
|
||||
@@ -33,7 +35,11 @@ actual fun SavePassphraseSetting(
|
||||
stringResource(MR.strings.save_passphrase_in_settings),
|
||||
tint = if (storedKey) WarningOrange else MaterialTheme.colors.secondary
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
if (smallPadding) {
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
} else {
|
||||
TextIconSpaced(false)
|
||||
}
|
||||
Text(
|
||||
stringResource(MR.strings.save_passphrase_in_settings),
|
||||
Modifier.padding(end = 24.dp),
|
||||
@@ -43,7 +49,7 @@ actual fun SavePassphraseSetting(
|
||||
DefaultSwitch(
|
||||
checked = useKeychain,
|
||||
onCheckedChange = onCheckedChange,
|
||||
enabled = !initialRandomDBPassphrase && !progressIndicator
|
||||
enabled = enabled
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -55,13 +61,14 @@ actual fun DatabaseEncryptionFooter(
|
||||
chatDbEncrypted: Boolean?,
|
||||
storedKey: MutableState<Boolean>,
|
||||
initialRandomDBPassphrase: MutableState<Boolean>,
|
||||
migration: Boolean,
|
||||
) {
|
||||
if (chatDbEncrypted == false) {
|
||||
SectionTextFooter(generalGetString(MR.strings.database_is_not_encrypted))
|
||||
} else if (useKeychain.value) {
|
||||
if (storedKey.value) {
|
||||
SectionTextFooter(generalGetString(MR.strings.settings_is_storing_in_clear_text))
|
||||
if (initialRandomDBPassphrase.value) {
|
||||
if (initialRandomDBPassphrase.value && !migration) {
|
||||
SectionTextFooter(generalGetString(MR.strings.encrypted_with_random_passphrase))
|
||||
} else {
|
||||
SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase))
|
||||
|
||||
Reference in New Issue
Block a user