diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 7b4812450b..9418e9a2e5 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -84,8 +84,15 @@ class AppPreferences(val context: Context) { val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null) val chatArchiveTime = mkDatePreference(SHARED_PREFS_CHAT_ARCHIVE_TIME, null) val chatLastStart = mkDatePreference(SHARED_PREFS_CHAT_LAST_START, null) - val useSocksProxy = mkBoolPreference(SHARED_PREFS_USE_SOCKS_PROXY, false) val developerTools = mkBoolPreference(SHARED_PREFS_DEVELOPER_TOOLS, false) + val networkUseSocksProxy = mkBoolPreference(SHARED_PREFS_NETWORK_USE_SOCKS_PROXY, false) + val networkTCPConnectTimeout = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT, NetCfg.defaults().tcpConnectTimeout, NetCfg.proxyDefaults().tcpConnectTimeout) + val networkTCPTimeout = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_TIMEOUT, NetCfg.defaults().tcpTimeout, NetCfg.proxyDefaults().tcpTimeout) + val networkSMPPingInterval = mkLongPreference(SHARED_PREFS_NETWORK_SMP_PING_INTERVAL, NetCfg.defaults().smpPingInterval) + val networkEnableKeepAlive = mkBoolPreference(SHARED_PREFS_NETWORK_ENABLE_KEEP_ALIVE, NetCfg.defaults().enableKeepAlive) + val networkTCPKeepIdle = mkIntPreference(SHARED_PREFS_NETWORK_TCP_KEEP_IDLE, KeepAliveOpts.defaults().keepIdle) + val networkTCPKeepIntvl = mkIntPreference(SHARED_PREFS_NETWORK_TCP_KEEP_INTVL, KeepAliveOpts.defaults().keepIntvl) + val networkTCPKeepCnt = mkIntPreference(SHARED_PREFS_NETWORK_TCP_KEEP_CNT, KeepAliveOpts.defaults().keepCnt) private fun mkIntPreference(prefName: String, default: Int) = Preference( @@ -93,6 +100,20 @@ class AppPreferences(val context: Context) { set = fun(value) = sharedPreferences.edit().putInt(prefName, value).apply() ) + private fun mkLongPreference(prefName: String, default: Long) = + Preference( + get = fun() = sharedPreferences.getLong(prefName, default), + set = fun(value) = sharedPreferences.edit().putLong(prefName, value).apply() + ) + + private fun mkTimeoutPreference(prefName: String, default: Long, proxyDefault: Long): Preference { + val d = if (networkUseSocksProxy.get()) proxyDefault else default + return Preference( + get = fun() = sharedPreferences.getLong(prefName, d), + set = fun(value) = sharedPreferences.edit().putLong(prefName, value).apply() + ) + } + private fun mkBoolPreference(prefName: String, default: Boolean) = Preference( get = fun() = sharedPreferences.getBoolean(prefName, default), @@ -130,8 +151,15 @@ class AppPreferences(val context: Context) { private const val SHARED_PREFS_CHAT_ARCHIVE_NAME = "ChatArchiveName" private const val SHARED_PREFS_CHAT_ARCHIVE_TIME = "ChatArchiveTime" private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart" - private const val SHARED_PREFS_USE_SOCKS_PROXY = "UseSocksProxy" private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools" + private const val SHARED_PREFS_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy" + private const val SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT = "NetworkTCPConnectTimeout" + private const val SHARED_PREFS_NETWORK_TCP_TIMEOUT = "NetworkTCPTimeout" + private const val SHARED_PREFS_NETWORK_SMP_PING_INTERVAL = "NetworkSMPPingInterval" + private const val SHARED_PREFS_NETWORK_ENABLE_KEEP_ALIVE = "NetworkEnableKeepAlive" + private const val SHARED_PREFS_NETWORK_TCP_KEEP_IDLE = "NetworkTCPKeepIdle" + private const val SHARED_PREFS_NETWORK_TCP_KEEP_INTVL = "NetworkTCPKeepIntvl" + private const val SHARED_PREFS_NETWORK_TCP_KEEP_CNT = "NetworkTCPKeepCnt" } } @@ -150,9 +178,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager Log.d(TAG, "user: $user") try { if (chatModel.chatRunning.value == true) return - if (chatModel.controller.appPrefs.useSocksProxy.get()) { - setNetworkConfig(NetCfg(socksProxy = ":9050", tcpTimeout = 10_000_000)) - } + apiSetNetworkConfig(getNetCfg()) val justStarted = apiStartChat() if (justStarted) { apiSetFilesFolder(getAppFilesDirectory(appContext)) @@ -339,14 +365,14 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager } } - suspend fun getNetworkConfig(): NetCfg? { + suspend fun apiGetNetworkConfig(): NetCfg? { val r = sendCmd(CC.APIGetNetworkConfig()) if (r is CR.NetworkConfig) return r.networkConfig Log.e(TAG, "getNetworkConfig bad response: ${r.responseType} ${r.details}") return null } - suspend fun setNetworkConfig(cfg: NetCfg): Boolean { + suspend fun apiSetNetworkConfig(cfg: NetCfg): Boolean { val r = sendCmd(CC.APISetNetworkConfig(cfg)) return when (r) { is CR.CmdOk -> true @@ -1023,6 +1049,45 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager context.startActivity(this) } } + + fun getNetCfg(): NetCfg { + val useSocksProxy = appPrefs.networkUseSocksProxy.get() + val socksProxy = if (useSocksProxy) ":9050" else null + val tcpConnectTimeout = appPrefs.networkTCPConnectTimeout.get() + val tcpTimeout = appPrefs.networkTCPTimeout.get() + val smpPingInterval = appPrefs.networkSMPPingInterval.get() + val enableKeepAlive = appPrefs.networkEnableKeepAlive.get() + val tcpKeepAlive = if (enableKeepAlive) { + val keepIdle = appPrefs.networkTCPKeepIdle.get() + val keepIntvl = appPrefs.networkTCPKeepIntvl.get() + val keepCnt = appPrefs.networkTCPKeepCnt.get() + KeepAliveOpts(keepIdle = keepIdle, keepIntvl = keepIntvl, keepCnt = keepCnt) + } else { + null + } + return NetCfg( + socksProxy = socksProxy, + tcpConnectTimeout = tcpConnectTimeout, + tcpTimeout = tcpTimeout, + tcpKeepAlive = tcpKeepAlive, + smpPingInterval = smpPingInterval + ) + } + + fun setNetCfg(cfg: NetCfg) { + appPrefs.networkUseSocksProxy.set(cfg.useSocksProxy) + appPrefs.networkTCPConnectTimeout.set(cfg.tcpConnectTimeout) + appPrefs.networkTCPTimeout.set(cfg.tcpTimeout) + appPrefs.networkSMPPingInterval.set(cfg.smpPingInterval) + if (cfg.tcpKeepAlive != null) { + appPrefs.networkEnableKeepAlive.set(true) + appPrefs.networkTCPKeepIdle.set(cfg.tcpKeepAlive.keepIdle) + appPrefs.networkTCPKeepIntvl.set(cfg.tcpKeepAlive.keepIntvl) + appPrefs.networkTCPKeepCnt.set(cfg.tcpKeepAlive.keepCnt) + } else { + appPrefs.networkEnableKeepAlive.set(false) + } + } } class Preference(val get: () -> T, val set: (T) -> Unit) @@ -1197,7 +1262,48 @@ class ComposedMessage(val filePath: String?, val quotedItemId: Long?, val msgCon class ArchiveConfig(val archivePath: String, val disableCompression: Boolean? = null, val parentTempDirectory: String? = null) @Serializable -class NetCfg(val socksProxy: String? = null, val tcpTimeout: Int) +data class NetCfg( + val socksProxy: String? = null, + val tcpConnectTimeout: Long, // microseconds + val tcpTimeout: Long, // microseconds + val tcpKeepAlive: KeepAliveOpts?, + val smpPingInterval: Long // microseconds +) { + val useSocksProxy: Boolean get() = socksProxy != null + val enableKeepAlive: Boolean get() = tcpKeepAlive != null + + companion object { + fun defaults(): NetCfg = + NetCfg( + socksProxy = null, + tcpConnectTimeout = 7_500_000, + tcpTimeout = 5_000_000, + tcpKeepAlive = KeepAliveOpts.defaults(), + smpPingInterval = 600_000_000 + ) + + fun proxyDefaults(): NetCfg = + NetCfg( + socksProxy = ":9050", + tcpConnectTimeout = 15_000_000, + tcpTimeout = 10_000_000, + tcpKeepAlive = KeepAliveOpts.defaults(), + smpPingInterval = 600_000_000 + ) + } +} + +@Serializable +data class KeepAliveOpts( + val keepIdle: Int, // seconds + val keepIntvl: Int, // seconds + val keepCnt: Int // times +) { + companion object { + fun defaults(): KeepAliveOpts = + KeepAliveOpts(keepIdle = 30, keepIntvl = 15, keepCnt = 4) + } +} val json = Json { prettyPrint = true diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/AdvancedNetworkSettings.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/AdvancedNetworkSettings.kt new file mode 100644 index 0000000000..13083ddf81 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/AdvancedNetworkSettings.kt @@ -0,0 +1,432 @@ +package chat.simplex.app.views.usersettings + +import SectionCustomFooter +import SectionDivider +import SectionItemView +import SectionSpacer +import SectionView +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import chat.simplex.app.R +import chat.simplex.app.model.* +import chat.simplex.app.ui.theme.* +import chat.simplex.app.views.helpers.* +import java.text.DecimalFormat + +@Composable +fun AdvancedNetworkSettingsView(chatModel: ChatModel) { + val currentCfg = remember { mutableStateOf(chatModel.controller.getNetCfg()) } + val currentCfgVal = currentCfg.value // used only on initialization + val networkTCPConnectTimeout = remember { mutableStateOf(currentCfgVal.tcpConnectTimeout) } + val networkTCPTimeout = remember { mutableStateOf(currentCfgVal.tcpTimeout) } + val networkSMPPingInterval = remember { mutableStateOf(currentCfgVal.smpPingInterval) } + val networkEnableKeepAlive = remember { mutableStateOf(currentCfgVal.enableKeepAlive) } + val networkTCPKeepIdle: MutableState + val networkTCPKeepIntvl: MutableState + val networkTCPKeepCnt: MutableState + if (currentCfgVal.tcpKeepAlive != null) { + networkTCPKeepIdle = remember { mutableStateOf(currentCfgVal.tcpKeepAlive.keepIdle) } + networkTCPKeepIntvl = remember { mutableStateOf(currentCfgVal.tcpKeepAlive.keepIntvl) } + networkTCPKeepCnt = remember { mutableStateOf(currentCfgVal.tcpKeepAlive.keepCnt) } + } else { + networkTCPKeepIdle = remember { mutableStateOf(KeepAliveOpts.defaults().keepIdle) } + networkTCPKeepIntvl = remember { mutableStateOf(KeepAliveOpts.defaults().keepIntvl) } + networkTCPKeepCnt = remember { mutableStateOf(KeepAliveOpts.defaults().keepCnt) } + } + + fun buildCfg(): NetCfg { + val socksProxy = currentCfg.value.socksProxy + val tcpConnectTimeout = networkTCPConnectTimeout.value + val tcpTimeout = networkTCPTimeout.value + val smpPingInterval = networkSMPPingInterval.value + val enableKeepAlive = networkEnableKeepAlive.value + val tcpKeepAlive = if (enableKeepAlive) { + val keepIdle = networkTCPKeepIdle.value + val keepIntvl = networkTCPKeepIntvl.value + val keepCnt = networkTCPKeepCnt.value + KeepAliveOpts(keepIdle = keepIdle, keepIntvl = keepIntvl, keepCnt = keepCnt) + } else { + null + } + return NetCfg( + socksProxy = socksProxy, + tcpConnectTimeout = tcpConnectTimeout, + tcpTimeout = tcpTimeout, + tcpKeepAlive = tcpKeepAlive, + smpPingInterval = smpPingInterval + ) + } + + fun updateView(cfg: NetCfg) { + networkTCPConnectTimeout.value = cfg.tcpConnectTimeout + networkTCPTimeout.value = cfg.tcpTimeout + networkSMPPingInterval.value = cfg.smpPingInterval + networkEnableKeepAlive.value = cfg.enableKeepAlive + if (cfg.tcpKeepAlive != null) { + networkTCPKeepIdle.value = cfg.tcpKeepAlive.keepIdle + networkTCPKeepIntvl.value = cfg.tcpKeepAlive.keepIntvl + networkTCPKeepCnt.value = cfg.tcpKeepAlive.keepCnt + } else { + networkTCPKeepIdle.value = KeepAliveOpts.defaults().keepIdle + networkTCPKeepIntvl.value = KeepAliveOpts.defaults().keepIntvl + networkTCPKeepCnt.value = KeepAliveOpts.defaults().keepCnt + } + } + + fun saveCfg(cfg: NetCfg) { + withApi { + chatModel.controller.apiSetNetworkConfig(cfg) + currentCfg.value = cfg + chatModel.controller.setNetCfg(cfg) + } + } + + fun reset() { + val newCfg = if (currentCfg.value.useSocksProxy) NetCfg.proxyDefaults() else NetCfg.defaults() + updateView(newCfg) + saveCfg(newCfg) + } + + fun updateSettingsDialog(action: () -> Unit) { + AlertManager.shared.showAlertMsg( + title = generalGetString(R.string.update_network_settings_question), + text = generalGetString(R.string.updating_settings_will_reconnect_client_to_all_servers), + confirmText = generalGetString(R.string.update_network_settings_confirmation), + onConfirm = action + ) + } + + AdvancedNetworkSettingsLayout( + networkTCPConnectTimeout, + networkTCPTimeout, + networkSMPPingInterval, + networkEnableKeepAlive, + networkTCPKeepIdle, + networkTCPKeepIntvl, + networkTCPKeepCnt, + resetDisabled = if (currentCfg.value.useSocksProxy) currentCfg.value == NetCfg.proxyDefaults() else currentCfg.value == NetCfg.defaults(), + reset = { updateSettingsDialog(::reset) }, + footerDisabled = buildCfg() == currentCfg.value, + revert = { updateView(currentCfg.value) }, + save = { updateSettingsDialog { saveCfg(buildCfg()) } } + ) +} + +@Composable fun AdvancedNetworkSettingsLayout( + networkTCPConnectTimeout: MutableState, + networkTCPTimeout: MutableState, + networkSMPPingInterval: MutableState, + networkEnableKeepAlive: MutableState, + networkTCPKeepIdle: MutableState, + networkTCPKeepIntvl: MutableState, + networkTCPKeepCnt: MutableState, + resetDisabled: Boolean, + reset: () -> Unit, + footerDisabled: Boolean, + revert: () -> Unit, + save: () -> Unit +) { + val secondsLabel = stringResource(R.string.network_option_seconds_label) + + Column( + Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.Start, + ) { + Text( + stringResource(R.string.network_settings_title), + Modifier.padding(start = 16.dp, bottom = 24.dp), + style = MaterialTheme.typography.h1 + ) + SectionSpacer() + + SectionView { + SectionItemView { + ResetToDefaultsButton(reset, disabled = resetDisabled) + } + SectionDivider() + SectionItemView { + TimeoutSettingRow( + stringResource(R.string.network_option_tcp_connection_timeout), networkTCPConnectTimeout, + listOf(2_500000, 5_000000, 7_500000, 10_000000, 15_000000, 20_000000), secondsLabel + ) + } + SectionDivider() + SectionItemView { + TimeoutSettingRow( + stringResource(R.string.network_option_protocol_timeout), networkTCPTimeout, + listOf(1_500000, 3_000000, 5_000000, 7_000000, 10_000000, 15_000000), secondsLabel + ) + } + SectionDivider() + SectionItemView { + TimeoutSettingRow( + stringResource(R.string.network_option_ping_interval), networkSMPPingInterval, + listOf(120_000000, 300_000000, 600_000000, 1200_000000, 2400_000000), secondsLabel + ) + } + SectionDivider() + SectionItemView { + EnableKeepAliveSwitch(networkEnableKeepAlive) + } + SectionDivider() + if (networkEnableKeepAlive.value) { + SectionItemView { + IntSettingRow("TCP_KEEPIDLE", networkTCPKeepIdle, listOf(15, 30, 60, 120, 180), secondsLabel) + } + SectionDivider() + SectionItemView { + IntSettingRow("TCP_KEEPINTVL", networkTCPKeepIntvl, listOf(5, 10, 15, 30, 60), secondsLabel) + } + SectionDivider() + SectionItemView { + IntSettingRow("TCP_KEEPCNT", networkTCPKeepCnt, listOf(1, 2, 4, 6, 8), "") + } + } else { + SectionItemView { + Text("TCP_KEEPIDLE", color = HighOrLowlight) + } + SectionDivider() + SectionItemView { + Text("TCP_KEEPINTVL", color = HighOrLowlight) + } + SectionDivider() + SectionItemView { + Text("TCP_KEEPCNT", color = HighOrLowlight) + } + } + } + SectionCustomFooter { + SettingsSectionFooter(revert, save, footerDisabled) + } + SectionSpacer() + } +} + +@Composable +fun ResetToDefaultsButton(reset: () -> Unit, disabled: Boolean) { + val modifier = if (disabled) Modifier else Modifier.clickable { reset() } + Row( + modifier.fillMaxSize(), + verticalAlignment = Alignment.CenterVertically + ) { + val color = if (disabled) HighOrLowlight else MaterialTheme.colors.primary + Text(stringResource(R.string.network_options_reset_to_defaults), color = color) + } +} + +@Composable +fun EnableKeepAliveSwitch( + networkEnableKeepAlive: MutableState +) { + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(stringResource(R.string.network_option_enable_tcp_keep_alive)) + Switch( + checked = networkEnableKeepAlive.value, + onCheckedChange = { networkEnableKeepAlive.value = it }, + colors = SwitchDefaults.colors( + checkedThumbColor = MaterialTheme.colors.primary, + uncheckedThumbColor = HighOrLowlight + ), + ) + } +} + +@Composable +fun IntSettingRow(title: String, selection: MutableState, values: List, label: String) { + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + var expanded by remember { mutableStateOf(false) } + + Text(title) + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { + expanded = !expanded + } + ) { + Row( + Modifier.width(140.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End + ) { + Text( + "${selection.value} $label", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = HighOrLowlight + ) + Spacer(Modifier.size(4.dp)) + Icon( + if (!expanded) Icons.Outlined.ExpandMore else Icons.Outlined.ExpandLess, + generalGetString(R.string.invite_to_group_button), + modifier = Modifier.padding(start = 8.dp), + tint = HighOrLowlight + ) + } + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { + expanded = false + } + ) { + values.forEach { selectionOption -> + DropdownMenuItem( + onClick = { + selection.value = selectionOption + expanded = false + } + ) { + Text( + "$selectionOption $label", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + } + } +} + +@Composable +fun TimeoutSettingRow(title: String, selection: MutableState, values: List, label: String) { + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + var expanded by remember { mutableStateOf(false) } + + Text(title) + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { + expanded = !expanded + } + ) { + val df = DecimalFormat("#.#") + + Row( + Modifier.width(140.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End + ) { + Text( + "${df.format(selection.value / 1_000_000.toDouble())} $label", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = HighOrLowlight + ) + Spacer(Modifier.size(4.dp)) + Icon( + if (!expanded) Icons.Outlined.ExpandMore else Icons.Outlined.ExpandLess, + generalGetString(R.string.invite_to_group_button), + modifier = Modifier.padding(start = 8.dp), + tint = HighOrLowlight + ) + } + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { + expanded = false + } + ) { + values.forEach { selectionOption -> + DropdownMenuItem( + onClick = { + selection.value = selectionOption + expanded = false + } + ) { + Text( + "${df.format(selectionOption / 1_000_000.toDouble())} $label", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + } + } +} + +@Composable +fun SettingsSectionFooter(revert: () -> Unit, save: () -> Unit, disabled: Boolean) { + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + FooterButton(Icons.Outlined.Replay, stringResource(R.string.network_options_revert), revert, disabled) + FooterButton(Icons.Outlined.Check, stringResource(R.string.network_options_save), save, disabled) + } +} + +@Composable +fun FooterButton(icon: ImageVector, title: String, action: () -> Unit, disabled: Boolean) { + Surface( + shape = RoundedCornerShape(20.dp), + color = Color.Black.copy(alpha = 0f) + ) { + val modifier = if (disabled) Modifier else Modifier.clickable { action() } + Row( + modifier.padding(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + icon, + title, + tint = if (disabled) HighOrLowlight else MaterialTheme.colors.primary + ) + Text( + title, + color = if (disabled) HighOrLowlight else MaterialTheme.colors.primary + ) + } + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewAdvancedNetworkSettingsLayout() { + SimpleXTheme { + AdvancedNetworkSettingsLayout( + networkTCPConnectTimeout = remember { mutableStateOf(10_000000) }, + networkTCPTimeout = remember { mutableStateOf(10_000000) }, + networkSMPPingInterval = remember { mutableStateOf(10_000000) }, + networkEnableKeepAlive = remember { mutableStateOf(true) }, + networkTCPKeepIdle = remember { mutableStateOf(10) }, + networkTCPKeepIntvl = remember { mutableStateOf(10) }, + networkTCPKeepCnt = remember { mutableStateOf(10) }, + resetDisabled = false, + reset = {}, + footerDisabled = false, + revert = {}, + save = {} + ) + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/NetworkAndServers.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/NetworkAndServers.kt new file mode 100644 index 0000000000..d5ac8cc536 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/NetworkAndServers.kt @@ -0,0 +1,144 @@ +package chat.simplex.app.views.usersettings + +import SectionDivider +import SectionItemView +import SectionView +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import chat.simplex.app.R +import chat.simplex.app.model.ChatModel +import chat.simplex.app.model.NetCfg +import chat.simplex.app.ui.theme.* +import chat.simplex.app.views.helpers.* + +@Composable +fun NetworkAndServersView( + chatModel: ChatModel, + showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), + showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit) +) { + val netCfg: MutableState = remember { mutableStateOf(chatModel.controller.getNetCfg()) } + val networkUseSocksProxy: MutableState = remember { mutableStateOf(netCfg.value.useSocksProxy) } + val developerTools = chatModel.controller.appPrefs.developerTools.get() + + NetworkAndServersLayout( + developerTools = developerTools, + networkUseSocksProxy = networkUseSocksProxy, + showModal = showModal, + showSettingsModal = showSettingsModal, + toggleSocksProxy = { enable -> + if (enable) { + AlertManager.shared.showAlertMsg( + title = generalGetString(R.string.network_enable_socks), + text = generalGetString(R.string.network_enable_socks_info), + confirmText = generalGetString(R.string.confirm_verb), + onConfirm = { + withApi { + chatModel.controller.apiSetNetworkConfig(NetCfg.proxyDefaults()) + chatModel.controller.setNetCfg(NetCfg.proxyDefaults()) + networkUseSocksProxy.value = true + } + } + ) + } else { + AlertManager.shared.showAlertMsg( + title = generalGetString(R.string.network_disable_socks), + text = generalGetString(R.string.network_disable_socks_info), + confirmText = generalGetString(R.string.confirm_verb), + onConfirm = { + withApi { + chatModel.controller.apiSetNetworkConfig(NetCfg.defaults()) + chatModel.controller.setNetCfg(NetCfg.defaults()) + networkUseSocksProxy.value = false + } + } + ) + } + } + ) +} + +@Composable fun NetworkAndServersLayout( + developerTools: Boolean, + networkUseSocksProxy: MutableState, + showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), + showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), + toggleSocksProxy: (Boolean) -> Unit +) { + Column( + Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + stringResource(R.string.network_and_servers), + Modifier.padding(start = 16.dp, bottom = 24.dp), + style = MaterialTheme.typography.h1 + ) + SectionView { + SettingsActionItem(Icons.Outlined.Dns, stringResource(R.string.smp_servers), showModal { SMPServersView(it) }) + SectionDivider() + SectionItemView { + UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy) + } + if (developerTools) { + SectionDivider() + SettingsActionItem(Icons.Outlined.Cable, stringResource(R.string.network_settings), showSettingsModal { AdvancedNetworkSettingsView(it) }) + } + } + } +} + +@Composable +fun UseSocksProxySwitch( + networkUseSocksProxy: MutableState, + toggleSocksProxy: (Boolean) -> Unit +) { + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + Icons.Outlined.SettingsEthernet, + stringResource(R.string.network_socks_toggle), + tint = HighOrLowlight + ) + Text(stringResource(R.string.network_socks_toggle)) + } + Switch( + checked = networkUseSocksProxy.value, + onCheckedChange = toggleSocksProxy, + colors = SwitchDefaults.colors( + checkedThumbColor = MaterialTheme.colors.primary, + uncheckedThumbColor = HighOrLowlight + ), + ) + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewNetworkAndServersLayout() { + SimpleXTheme { + NetworkAndServersLayout( + developerTools = true, + networkUseSocksProxy = remember { mutableStateOf(true) }, + showModal = { {} }, + showSettingsModal = { {} }, + toggleSocksProxy = {} + ) + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/NetworkSettings.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/NetworkSettings.kt deleted file mode 100644 index 72c2d2c463..0000000000 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/NetworkSettings.kt +++ /dev/null @@ -1,102 +0,0 @@ -package chat.simplex.app.views.usersettings - -import SectionView -import androidx.compose.foundation.layout.* -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import chat.simplex.app.R -import chat.simplex.app.model.ChatModel -import chat.simplex.app.model.NetCfg -import chat.simplex.app.ui.theme.HighOrLowlight -import chat.simplex.app.ui.theme.SimpleXTheme -import chat.simplex.app.views.helpers.* - -@Composable -fun NetworkSettingsView(chatModel: ChatModel, netCfg: NetCfg) { - val useSocksProxy = remember { mutableStateOf(netCfg.socksProxy != null) } - fun setSocksProxy(value: Boolean) { - chatModel.controller.appPrefs.useSocksProxy.set(value) - useSocksProxy.value = value - } - - NetworkSettingsLayout( - useSocksProxy, - toggleSocksProxy = { enable -> - if (enable) { - AlertManager.shared.showAlertMsg( - title = generalGetString(R.string.network_enable_socks), - text = generalGetString(R.string.network_enable_socks_info), - confirmText = generalGetString(R.string.confirm_verb), - onConfirm = { - withApi { - chatModel.controller.setNetworkConfig(NetCfg(socksProxy = ":9050", tcpTimeout = 10_000_000)) - setSocksProxy(true) - } - } - ) - } else { - AlertManager.shared.showAlertMsg( - title = generalGetString(R.string.network_disable_socks), - text = generalGetString(R.string.network_disable_socks_info), - confirmText = generalGetString(R.string.confirm_verb), - onConfirm = { - withApi { - chatModel.controller.setNetworkConfig(NetCfg(tcpTimeout = 5_000_000)) - setSocksProxy(false) - } - } - ) - } - } - ) -} - -@Composable fun NetworkSettingsLayout( - useSocksProxy: MutableState, - toggleSocksProxy: (Boolean) -> Unit -) { - Column( - Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text( - stringResource(R.string.network_settings_title), - Modifier.padding(start = 16.dp, bottom = 24.dp), - style = MaterialTheme.typography.h1 - ) - SectionView(stringResource(R.string.settings_section_title_socks)) { - Row( - Modifier.padding(start = 10.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text(stringResource(R.string.network_socks_toggle)) - Spacer(Modifier.fillMaxWidth().weight(1f)) - Switch( - checked = useSocksProxy.value, - onCheckedChange = toggleSocksProxy, - colors = SwitchDefaults.colors( - checkedThumbColor = MaterialTheme.colors.primary, - uncheckedThumbColor = HighOrLowlight - ), - ) - } - } - } -} - -@Preview(showBackground = true) -@Composable -fun PreviewNetworkSettings() { - SimpleXTheme { - NetworkSettingsLayout( - useSocksProxy = remember { mutableStateOf(true) }, - toggleSocksProxy = {} - ) - } -} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt index d1c68259a4..74d8970a20 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt @@ -63,19 +63,6 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) { } } }, showCustomModal = { modalView -> { ModalManager.shared.showCustomModal { close -> modalView(chatModel, close) } } }, showTerminal = { ModalManager.shared.showCustomModal { close -> TerminalView(chatModel, close) } }, - showNetworkSettings = { - withApi { - val cfg = chatModel.controller.getNetworkConfig() - if (cfg != null) { - ModalManager.shared.showCustomModal { close -> - ModalView(close = close, modifier = Modifier, - background = if (isSystemInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight) { - NetworkSettingsView(chatModel, cfg) - } - } - } - } - }, showAppearance = { withApi { ModalManager.shared.showCustomModal { close -> @@ -118,7 +105,6 @@ fun SettingsLayout( showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit), showTerminal: () -> Unit, - showNetworkSettings: () -> Unit, showAppearance: () -> Unit // showVideoChatPrototype: () -> Unit ) { @@ -149,17 +135,15 @@ fun SettingsLayout( SectionSpacer() SectionView(stringResource(R.string.settings_section_title_settings)) { + PrivateNotificationsItem(runServiceInBackground, setRunServiceInBackground, stopped) + SectionDivider() SettingsActionItem(Icons.Outlined.Videocam, stringResource(R.string.settings_audio_video_calls), showSettingsModal { CallSettingsView(it) }, disabled = stopped) SectionDivider() SettingsActionItem(Icons.Outlined.Lock, stringResource(R.string.privacy_and_security), showSettingsModal { PrivacySettingsView(it, setPerformLA) }, disabled = stopped) SectionDivider() - PrivateNotificationsItem(runServiceInBackground, setRunServiceInBackground, stopped) - SectionDivider() - SettingsActionItem(Icons.Outlined.Dns, stringResource(R.string.smp_servers), showModal { SMPServersView(it) }, disabled = stopped) - SectionDivider() - SettingsActionItem(Icons.Outlined.SettingsEthernet, stringResource(R.string.network_settings), showNetworkSettings, disabled = stopped) - SectionDivider() SettingsActionItem(Icons.Outlined.LightMode, stringResource(R.string.appearance_settings), showAppearance, disabled = stopped) + SectionDivider() + SettingsActionItem(Icons.Outlined.WifiTethering, stringResource(R.string.network_and_servers), showSettingsModal { NetworkAndServersView(it, showModal, showSettingsModal) }, disabled = stopped) } SectionSpacer() @@ -370,7 +354,6 @@ fun PreviewSettingsLayout() { showSettingsModal = { {} }, showCustomModal = { {} }, showTerminal = {}, - showNetworkSettings = {}, showAppearance = {}, // showVideoChatPrototype = {} ) diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml index 19e3001102..8d483270c2 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -272,6 +272,7 @@ Введите SMP серверы, каждый сервер в отдельной строке: Инфо Сохранить + Сеть & серверы Настройки сети Настройки сети Использовать SOCKS прокси (порт 9050) @@ -612,4 +613,17 @@ Профиль группы хранится на устройствах членов, а не на серверах. Сохранить профиль группы Ошибка при сохранении профиля группы + + + Сбросить настройки + сек + Таймаут TCP соединения + Таймаут протокола + Интервал PING + Включить TCP keep-alive + Отменить изменения + Сохранить + Обновить настройки сети? + Обновление настроек приведет к переподключению клиента ко всем серверам. + Обновить diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 9eb7c81c60..c8a26ff348 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -277,7 +277,8 @@ Enter one SMP server per line: How to Save - Network + Network & servers + Advanced network settings Network settings Use SOCKS proxy (port 9050) Use SOCKS proxy? @@ -614,4 +615,17 @@ Group profile is stored on members\' devices, not on the servers. Save group profile Error saving group profile + + + Reset to defaults + sec + TCP connection timeout + Protocol timeout + PING interval + Enable TCP keep-alive + Revert + Save + Update network settings? + Updating settings will re-connect the client to all servers. + Update