diff --git a/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift b/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift index 9f03b95321..a6ad211939 100644 --- a/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift @@ -51,9 +51,10 @@ struct AdvancedNetworkSettings: View { } .disabled(currentNetCfg == NetCfg.proxyDefaults) - timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout, values: [7_500000, 10_000000, 15_000000, 20_000000, 30_000000, 45_000000], label: secondsLabel) + timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout, values: [5_000000, 7_500000, 10_000000, 15_000000, 20_000000, 30_000000, 40_000000], label: secondsLabel) timeoutSettingPicker("Protocol timeout", selection: $netCfg.tcpTimeout, values: [5_000000, 7_000000, 10_000000, 15_000000, 20_000000, 30_000000], label: secondsLabel) timeoutSettingPicker("Protocol timeout per KB", selection: $netCfg.tcpTimeoutPerKb, values: [2_500, 5_000, 10_000, 15_000, 20_000, 30_000], label: secondsLabel) + intSettingPicker("Receiving concurrency", selection: $netCfg.rcvConcurrency, values: [1, 2, 4, 8, 12, 16, 24], label: "") timeoutSettingPicker("PING interval", selection: $netCfg.smpPingInterval, values: [120_000000, 300_000000, 600_000000, 1200_000000, 2400_000000, 3600_000000], label: secondsLabel) intSettingPicker("PING count", selection: $netCfg.smpPingCount, values: [1, 2, 3, 5, 8], label: "") Toggle("Enable TCP keep-alive", isOn: $enableKeepAlive) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 5c8308c3b6..f9143a547f 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -29,6 +29,11 @@ 5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */; }; 5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C13730A28156D2700F43030 /* ContactConnectionView.swift */; }; 5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */; }; + 5C22177A2BD40D1800A8B0E7 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C2217752BD40D1800A8B0E7 /* libgmp.a */; }; + 5C22177B2BD40D1800A8B0E7 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C2217762BD40D1800A8B0E7 /* libgmpxx.a */; }; + 5C22177C2BD40D1800A8B0E7 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C2217772BD40D1800A8B0E7 /* libffi.a */; }; + 5C22177D2BD40D1800A8B0E7 /* libHSsimplex-chat-5.7.0.0-KdKN1sKiHR7tY2gjTFt3O-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C2217782BD40D1800A8B0E7 /* libHSsimplex-chat-5.7.0.0-KdKN1sKiHR7tY2gjTFt3O-ghc9.6.3.a */; }; + 5C22177E2BD40D1800A8B0E7 /* libHSsimplex-chat-5.7.0.0-KdKN1sKiHR7tY2gjTFt3O.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C2217792BD40D1800A8B0E7 /* libHSsimplex-chat-5.7.0.0-KdKN1sKiHR7tY2gjTFt3O.a */; }; 5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */; }; 5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260A27A30CFA00F70299 /* ChatListView.swift */; }; 5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260E27A30FDC00F70299 /* ChatView.swift */; }; @@ -110,11 +115,6 @@ 5CC1C99527A6CF7F000D9FF6 /* ShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */; }; 5CC2C0FC2809BF11000C35E3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FA2809BF11000C35E3 /* Localizable.strings */; }; 5CC2C0FF2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FD2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings */; }; - 5CC83D1A2BCC504B00A0C558 /* libHSsimplex-chat-5.7.0.0-AhbVfRKDsEZ5w5ND1HSTLv.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC83D152BCC504B00A0C558 /* libHSsimplex-chat-5.7.0.0-AhbVfRKDsEZ5w5ND1HSTLv.a */; }; - 5CC83D1B2BCC504B00A0C558 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC83D162BCC504B00A0C558 /* libgmpxx.a */; }; - 5CC83D1C2BCC504B00A0C558 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC83D172BCC504B00A0C558 /* libgmp.a */; }; - 5CC83D1D2BCC504B00A0C558 /* libHSsimplex-chat-5.7.0.0-AhbVfRKDsEZ5w5ND1HSTLv-ghc9.6.4.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC83D182BCC504B00A0C558 /* libHSsimplex-chat-5.7.0.0-AhbVfRKDsEZ5w5ND1HSTLv-ghc9.6.4.a */; }; - 5CC83D1E2BCC504B00A0C558 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC83D192BCC504B00A0C558 /* libffi.a */; }; 5CC868F329EB540C0017BBFD /* CIRcvDecryptionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */; }; 5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */; }; 5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CD67B8D2B0E858A00C510B1 /* hs_init.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -284,6 +284,11 @@ 5C13730A28156D2700F43030 /* ContactConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionView.swift; sourceTree = ""; }; 5C13730C2815740A00F43030 /* DebugJSON.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = DebugJSON.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemView.swift; sourceTree = ""; }; + 5C2217752BD40D1800A8B0E7 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 5C2217762BD40D1800A8B0E7 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 5C2217772BD40D1800A8B0E7 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 5C2217782BD40D1800A8B0E7 /* libHSsimplex-chat-5.7.0.0-KdKN1sKiHR7tY2gjTFt3O-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.0.0-KdKN1sKiHR7tY2gjTFt3O-ghc9.6.3.a"; sourceTree = ""; }; + 5C2217792BD40D1800A8B0E7 /* libHSsimplex-chat-5.7.0.0-KdKN1sKiHR7tY2gjTFt3O.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.0.0-KdKN1sKiHR7tY2gjTFt3O.a"; sourceTree = ""; }; 5C245F3C2B501E98001CC39F /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; 5C245F3D2B501F13001CC39F /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = "tr.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; }; 5C245F3E2B501F13001CC39F /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -408,11 +413,6 @@ 5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheet.swift; sourceTree = ""; }; 5CC2C0FB2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; 5CC2C0FE2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = "ru.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; }; - 5CC83D152BCC504B00A0C558 /* libHSsimplex-chat-5.7.0.0-AhbVfRKDsEZ5w5ND1HSTLv.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.0.0-AhbVfRKDsEZ5w5ND1HSTLv.a"; sourceTree = ""; }; - 5CC83D162BCC504B00A0C558 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 5CC83D172BCC504B00A0C558 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5CC83D182BCC504B00A0C558 /* libHSsimplex-chat-5.7.0.0-AhbVfRKDsEZ5w5ND1HSTLv-ghc9.6.4.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.0.0-AhbVfRKDsEZ5w5ND1HSTLv-ghc9.6.4.a"; sourceTree = ""; }; - 5CC83D192BCC504B00A0C558 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIRcvDecryptionError.swift; sourceTree = ""; }; 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavStackCompat.swift; sourceTree = ""; }; 5CD67B8D2B0E858A00C510B1 /* hs_init.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = hs_init.h; sourceTree = ""; }; @@ -535,13 +535,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5CC83D1D2BCC504B00A0C558 /* libHSsimplex-chat-5.7.0.0-AhbVfRKDsEZ5w5ND1HSTLv-ghc9.6.4.a in Frameworks */, - 5CC83D1B2BCC504B00A0C558 /* libgmpxx.a in Frameworks */, - 5CC83D1C2BCC504B00A0C558 /* libgmp.a in Frameworks */, - 5CC83D1A2BCC504B00A0C558 /* libHSsimplex-chat-5.7.0.0-AhbVfRKDsEZ5w5ND1HSTLv.a in Frameworks */, - 5CC83D1E2BCC504B00A0C558 /* libffi.a in Frameworks */, + 5C22177B2BD40D1800A8B0E7 /* libgmpxx.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, + 5C22177D2BD40D1800A8B0E7 /* libHSsimplex-chat-5.7.0.0-KdKN1sKiHR7tY2gjTFt3O-ghc9.6.3.a in Frameworks */, + 5C22177A2BD40D1800A8B0E7 /* libgmp.a in Frameworks */, + 5C22177C2BD40D1800A8B0E7 /* libffi.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, + 5C22177E2BD40D1800A8B0E7 /* libHSsimplex-chat-5.7.0.0-KdKN1sKiHR7tY2gjTFt3O.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -605,11 +605,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5CC83D192BCC504B00A0C558 /* libffi.a */, - 5CC83D172BCC504B00A0C558 /* libgmp.a */, - 5CC83D162BCC504B00A0C558 /* libgmpxx.a */, - 5CC83D182BCC504B00A0C558 /* libHSsimplex-chat-5.7.0.0-AhbVfRKDsEZ5w5ND1HSTLv-ghc9.6.4.a */, - 5CC83D152BCC504B00A0C558 /* libHSsimplex-chat-5.7.0.0-AhbVfRKDsEZ5w5ND1HSTLv.a */, + 5C2217772BD40D1800A8B0E7 /* libffi.a */, + 5C2217752BD40D1800A8B0E7 /* libgmp.a */, + 5C2217762BD40D1800A8B0E7 /* libgmpxx.a */, + 5C2217782BD40D1800A8B0E7 /* libHSsimplex-chat-5.7.0.0-KdKN1sKiHR7tY2gjTFt3O-ghc9.6.3.a */, + 5C2217792BD40D1800A8B0E7 /* libHSsimplex-chat-5.7.0.0-KdKN1sKiHR7tY2gjTFt3O.a */, ); path = Libraries; sourceTree = ""; diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index f33bdfbdd8..541799a46c 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -1257,6 +1257,7 @@ public struct NetCfg: Codable, Equatable { public var tcpConnectTimeout: Int // microseconds public var tcpTimeout: Int // microseconds public var tcpTimeoutPerKb: Int // microseconds + public var rcvConcurrency: Int // pool size public var tcpKeepAlive: KeepAliveOpts? public var smpPingInterval: Int // microseconds public var smpPingCount: Int // times @@ -1265,9 +1266,10 @@ public struct NetCfg: Codable, Equatable { public static let defaults: NetCfg = NetCfg( socksProxy: nil, sessionMode: TransportSessionMode.user, - tcpConnectTimeout: 20_000_000, + tcpConnectTimeout: 10_000_000, tcpTimeout: 15_000_000, tcpTimeoutPerKb: 10_000, + rcvConcurrency: 12, tcpKeepAlive: KeepAliveOpts.defaults, smpPingInterval: 1200_000_000, smpPingCount: 3, @@ -1277,9 +1279,10 @@ public struct NetCfg: Codable, Equatable { public static let proxyDefaults: NetCfg = NetCfg( socksProxy: nil, sessionMode: TransportSessionMode.user, - tcpConnectTimeout: 30_000_000, + tcpConnectTimeout: 20_000_000, tcpTimeout: 20_000_000, tcpTimeoutPerKb: 15_000, + rcvConcurrency: 8, tcpKeepAlive: KeepAliveOpts.defaults, smpPingInterval: 1200_000_000, smpPingCount: 3, diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index df4de134c2..cb07ba43ce 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -29,6 +29,7 @@ let GROUP_DEFAULT_NETWORK_SESSION_MODE = "networkSessionMode" let GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT = "networkTCPConnectTimeout" let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT = "networkTCPTimeout" let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB = "networkTCPTimeoutPerKb" +let GROUP_DEFAULT_NETWORK_RCV_CONCURRENCY = "networkRcvConcurrency" let GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL = "networkSMPPingInterval" let GROUP_DEFAULT_NETWORK_SMP_PING_COUNT = "networkSMPPingCount" let GROUP_DEFAULT_NETWORK_ENABLE_KEEP_ALIVE = "networkEnableKeepAlive" @@ -55,6 +56,7 @@ public func registerGroupDefaults() { GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT: NetCfg.defaults.tcpConnectTimeout, GROUP_DEFAULT_NETWORK_TCP_TIMEOUT: NetCfg.defaults.tcpTimeout, GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB: NetCfg.defaults.tcpTimeoutPerKb, + GROUP_DEFAULT_NETWORK_RCV_CONCURRENCY: NetCfg.defaults.rcvConcurrency, GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL: NetCfg.defaults.smpPingInterval, GROUP_DEFAULT_NETWORK_SMP_PING_COUNT: NetCfg.defaults.smpPingCount, GROUP_DEFAULT_NETWORK_ENABLE_KEEP_ALIVE: NetCfg.defaults.enableKeepAlive, @@ -278,6 +280,7 @@ public func getNetCfg() -> NetCfg { let tcpConnectTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT) let tcpTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT) let tcpTimeoutPerKb = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB) + let rcvConcurrency = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_RCV_CONCURRENCY) let smpPingInterval = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL) let smpPingCount = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_SMP_PING_COUNT) let enableKeepAlive = groupDefaults.bool(forKey: GROUP_DEFAULT_NETWORK_ENABLE_KEEP_ALIVE) @@ -297,6 +300,7 @@ public func getNetCfg() -> NetCfg { tcpConnectTimeout: tcpConnectTimeout, tcpTimeout: tcpTimeout, tcpTimeoutPerKb: tcpTimeoutPerKb, + rcvConcurrency: rcvConcurrency, tcpKeepAlive: tcpKeepAlive, smpPingInterval: smpPingInterval, smpPingCount: smpPingCount, @@ -310,6 +314,7 @@ public func setNetCfg(_ cfg: NetCfg) { groupDefaults.set(cfg.tcpConnectTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT) groupDefaults.set(cfg.tcpTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT) groupDefaults.set(cfg.tcpTimeoutPerKb, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB) + groupDefaults.set(cfg.rcvConcurrency, forKey: GROUP_DEFAULT_NETWORK_RCV_CONCURRENCY) groupDefaults.set(cfg.smpPingInterval, forKey: GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL) groupDefaults.set(cfg.smpPingCount, forKey: GROUP_DEFAULT_NETWORK_SMP_PING_COUNT) if let tcpKeepAlive = cfg.tcpKeepAlive { diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallAudioDeviceManager.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallAudioDeviceManager.kt new file mode 100644 index 0000000000..9c6f76461d --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallAudioDeviceManager.kt @@ -0,0 +1,235 @@ +package chat.simplex.common.views.call + +import android.content.Context +import android.media.* +import android.media.AudioManager.OnCommunicationDeviceChangedListener +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.runtime.* +import chat.simplex.common.platform.* +import dev.icerock.moko.resources.ImageResource +import chat.simplex.res.MR +import dev.icerock.moko.resources.StringResource +import java.util.concurrent.Executors + +interface CallAudioDeviceManagerInterface { + val devices: State> + val currentDevice: MutableState + fun start() + fun stop() + // AudioDeviceInfo.AudioDeviceType + fun selectLastExternalDeviceOrDefault(speaker: Boolean, keepAnyNonEarpiece: Boolean) + // AudioDeviceInfo.AudioDeviceType + fun selectDevice(id: Int) + + companion object { + fun new(): CallAudioDeviceManagerInterface = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PostSCallAudioDeviceManager() + } else { + PreSCallAudioDeviceManager() + } + } +} + +@RequiresApi(Build.VERSION_CODES.S) +class PostSCallAudioDeviceManager: CallAudioDeviceManagerInterface { + private val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager + override val devices: MutableState> = mutableStateOf(emptyList()) + override val currentDevice: MutableState = mutableStateOf(null) + + private val audioCallback = object: AudioDeviceCallback() { + override fun onAudioDevicesAdded(addedDevices: Array) { + Log.d(TAG, "Added audio devices: ${addedDevices.map { it.type }}") + super.onAudioDevicesAdded(addedDevices) + val oldDevices = devices.value + devices.value = am.availableCommunicationDevices + Log.d(TAG, "Added audio devices2: ${devices.value.map { it.type }}") + + if (devices.value.size - oldDevices.size > 0) { + selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.supportsVideo() == true, false) + } + } + + override fun onAudioDevicesRemoved(removedDevices: Array) { + Log.d(TAG, "Removed audio devices: ${removedDevices.map { it.type }}") + super.onAudioDevicesRemoved(removedDevices) + devices.value = am.availableCommunicationDevices + } + } + + private val listener: OnCommunicationDeviceChangedListener = OnCommunicationDeviceChangedListener { device -> + devices.value = am.availableCommunicationDevices + currentDevice.value = device + } + + override fun start() { + am.registerAudioDeviceCallback(audioCallback, null) + am.addOnCommunicationDeviceChangedListener(Executors.newSingleThreadExecutor(), listener) + } + + override fun stop() { + am.unregisterAudioDeviceCallback(audioCallback) + am.removeOnCommunicationDeviceChangedListener(listener) + } + + override fun selectLastExternalDeviceOrDefault(speaker: Boolean, keepAnyNonEarpiece: Boolean) { + Log.d(TAG, "selectLastExternalDeviceOrDefault: set audio mode, speaker enabled: $speaker") + am.mode = AudioManager.MODE_IN_COMMUNICATION + val commDevice = am.communicationDevice + if (keepAnyNonEarpiece && commDevice != null && commDevice.type != AudioDeviceInfo.TYPE_BUILTIN_EARPIECE) { + // some external device or speaker selected already, no need to change it + return + } + + val preferredSecondaryDevice = if (speaker) AudioDeviceInfo.TYPE_BUILTIN_SPEAKER else AudioDeviceInfo.TYPE_BUILTIN_EARPIECE + val externalDevice = devices.value.lastOrNull { it.type != AudioDeviceInfo.TYPE_BUILTIN_SPEAKER && it.type != AudioDeviceInfo.TYPE_BUILTIN_EARPIECE } + // External device already selected + if (externalDevice != null && externalDevice.type == am.communicationDevice?.type) { + return + } + if (externalDevice != null) { + am.setCommunicationDevice(externalDevice) + } else if (am.communicationDevice?.type != preferredSecondaryDevice) { + am.availableCommunicationDevices.firstOrNull { it.type == preferredSecondaryDevice }?.let { + am.setCommunicationDevice(it) + } + } + } + + override fun selectDevice(id: Int) { + am.mode = AudioManager.MODE_IN_COMMUNICATION + val device = devices.value.lastOrNull { it.id == id } + if (device != null && am.communicationDevice?.id != id ) { + am.setCommunicationDevice(device) + } + } +} + +class PreSCallAudioDeviceManager: CallAudioDeviceManagerInterface { + private val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager + override val devices: MutableState> = mutableStateOf(emptyList()) + override val currentDevice: MutableState = mutableStateOf(null) + + private val audioCallback = object: AudioDeviceCallback() { + override fun onAudioDevicesAdded(addedDevices: Array) { + Log.d(TAG, "Added audio devices: ${addedDevices.map { it.type }}") + super.onAudioDevicesAdded(addedDevices) + val wasSize = devices.value.size + devices.value += addedDevices.filter { it.hasSupportedType() } + val addedCount = devices.value.size - wasSize + //if (addedCount > 0 && chatModel.activeCall.value?.callState == CallState.Connected) { + // Setting params in Connected state makes sure that Bluetooth will NOT be broken on Android < 12 + selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.supportsVideo() == true, false) + //} + } + + override fun onAudioDevicesRemoved(removedDevices: Array) { + Log.d(TAG, "Removed audio devices: ${removedDevices.map { it.type }}") + super.onAudioDevicesRemoved(removedDevices) + val wasSize = devices.value.size + devices.value = devices.value.filterNot { removedDevices.any { rm -> rm.id == it.id } } + //val removedCount = wasSize - devices.value.size + //if (devices.value.count { it.hasSupportedType() } == 2 && chatModel.activeCall.value?.callState == CallState.Connected) { + // Setting params in Connected state makes sure that Bluetooth will NOT be broken on Android < 12 + selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.supportsVideo() == true, true) + //} + } + } + + override fun start() { + am.registerAudioDeviceCallback(audioCallback, null) + } + + override fun stop() { + am.unregisterAudioDeviceCallback(audioCallback) + } + + override fun selectLastExternalDeviceOrDefault(speaker: Boolean, keepAnyNonEarpiece: Boolean) { + Log.d(TAG, "selectLastExternalDeviceOrDefault: set audio mode, speaker enabled: $speaker") + val preferredSecondaryDevice = if (speaker) AudioDeviceInfo.TYPE_BUILTIN_SPEAKER else AudioDeviceInfo.TYPE_BUILTIN_EARPIECE + val externalDevice = devices.value.lastOrNull { it.hasSupportedType() && it.isSource && it.isSink && it.type != AudioDeviceInfo.TYPE_BUILTIN_SPEAKER && it.type != AudioDeviceInfo.TYPE_BUILTIN_EARPIECE } + if (externalDevice != null) { + selectDevice(externalDevice.id) + } else { + am.stopBluetoothSco() + am.isWiredHeadsetOn = false + am.isSpeakerphoneOn = preferredSecondaryDevice == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER + am.isBluetoothScoOn = false + val newCurrentDevice = devices.value.firstOrNull { it.type == preferredSecondaryDevice } + adaptToCurrentlyActiveDevice(newCurrentDevice) + } + } + + override fun selectDevice(id: Int) { + val device = devices.value.lastOrNull { it.id == id } + val isExternalDevice = device != null && device.type != AudioDeviceInfo.TYPE_BUILTIN_SPEAKER && device.type != AudioDeviceInfo.TYPE_BUILTIN_EARPIECE + if (isExternalDevice) { + am.isSpeakerphoneOn = false + if (device?.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES) { + am.isWiredHeadsetOn = true + am.stopBluetoothSco() + am.isBluetoothScoOn = false + } else { + am.isWiredHeadsetOn = false + am.startBluetoothSco() + am.isBluetoothScoOn = true + } + adaptToCurrentlyActiveDevice(device) + } else { + am.stopBluetoothSco() + am.isWiredHeadsetOn = false + am.isSpeakerphoneOn = device?.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER + am.isBluetoothScoOn = false + adaptToCurrentlyActiveDevice(device) + } + } + + private fun adaptToCurrentlyActiveDevice(newCurrentDevice: AudioDeviceInfo?) { + currentDevice.value = newCurrentDevice + } + + private fun AudioDeviceInfo.hasSupportedType(): Boolean = when (type) { + AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> true + AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> true + + AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> true + AudioDeviceInfo.TYPE_BLE_HEADSET -> true + AudioDeviceInfo.TYPE_BLE_SPEAKER -> true + AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> true + else -> false + } +} + +val AudioDeviceInfo.icon: ImageResource + get() = when (this.type) { + AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> MR.images.ic_volume_down + AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> MR.images.ic_volume_up + + AudioDeviceInfo.TYPE_BLUETOOTH_SCO, + AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, + AudioDeviceInfo.TYPE_BLE_HEADSET, + AudioDeviceInfo.TYPE_BLE_SPEAKER -> MR.images.ic_bluetooth + + AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> MR.images.ic_headphones + + AudioDeviceInfo.TYPE_USB_HEADSET, AudioDeviceInfo.TYPE_USB_DEVICE -> MR.images.ic_usb + else -> MR.images.ic_brand_awareness_filled + } + +val AudioDeviceInfo.name: StringResource? + get() = when (this.type) { + AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> MR.strings.audio_device_earpiece + AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> MR.strings.audio_device_speaker + + AudioDeviceInfo.TYPE_BLUETOOTH_SCO, + AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, + AudioDeviceInfo.TYPE_BLE_HEADSET, + AudioDeviceInfo.TYPE_BLE_SPEAKER -> null // Use product name instead + + AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> MR.strings.audio_device_wired_headphones + + AudioDeviceInfo.TYPE_USB_HEADSET, AudioDeviceInfo.TYPE_USB_DEVICE -> null // Use product name instead + else -> null // Use product name instead + } + diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt index af261e2a98..a93f79f7bc 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt @@ -26,8 +26,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalContext -import dev.icerock.moko.resources.compose.painterResource -import dev.icerock.moko.resources.compose.stringResource +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -39,14 +38,14 @@ import androidx.webkit.WebViewAssetLoader import androidx.webkit.WebViewClientCompat import chat.simplex.common.helpers.showAllowPermissionInSettingsAlert import chat.simplex.common.model.* -import chat.simplex.common.ui.theme.* -import chat.simplex.common.model.ChatModel -import chat.simplex.common.model.Contact import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import com.google.accompanist.permissions.* import dev.icerock.moko.resources.StringResource +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.* import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull @@ -71,7 +70,6 @@ fun activeCallDestroyWebView() = withApi { actual fun ActiveCallView() { val call = remember { chatModel.activeCall }.value val scope = rememberCoroutineScope() - val audioViaBluetooth = rememberSaveable { mutableStateOf(false) } val proximityLock = remember { val pm = (androidAppContext.getSystemService(Context.POWER_SERVICE) as PowerManager) if (pm.isWakeLockLevelSupported(PROXIMITY_SCREEN_OFF_WAKE_LOCK)) { @@ -87,41 +85,16 @@ actual fun ActiveCallView() { wasConnected.value = true } } + val callAudioDeviceManager = remember { CallAudioDeviceManagerInterface.new() } DisposableEffect(Unit) { - val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager - var btDeviceCount = 0 - val audioCallback = object: AudioDeviceCallback() { - override fun onAudioDevicesAdded(addedDevices: Array) { - Log.d(TAG, "Added audio devices: ${addedDevices.map { it.type }}") - super.onAudioDevicesAdded(addedDevices) - val addedCount = addedDevices.count { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO } - btDeviceCount += addedCount - audioViaBluetooth.value = btDeviceCount > 0 - if (addedCount > 0 && chatModel.activeCall.value?.callState == CallState.Connected) { - // Setting params in Connected state makes sure that Bluetooth will NOT be broken on Android < 12 - setCallSound(chatModel.activeCall.value?.soundSpeaker ?: return, audioViaBluetooth) - } - } - override fun onAudioDevicesRemoved(removedDevices: Array) { - Log.d(TAG, "Removed audio devices: ${removedDevices.map { it.type }}") - super.onAudioDevicesRemoved(removedDevices) - val removedCount = removedDevices.count { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO } - btDeviceCount -= removedCount - audioViaBluetooth.value = btDeviceCount > 0 - if (btDeviceCount == 0 && chatModel.activeCall.value?.callState == CallState.Connected) { - // Setting params in Connected state makes sure that Bluetooth will NOT be broken on Android < 12 - setCallSound(chatModel.activeCall.value?.soundSpeaker ?: return, audioViaBluetooth) - } - } - } - am.registerAudioDeviceCallback(audioCallback, null) + callAudioDeviceManager.start() onDispose { CallSoundsPlayer.stop() if (wasConnected.value) { CallSoundsPlayer.vibrate() } dropAudioManagerOverrides() - am.unregisterAudioDeviceCallback(audioCallback) + callAudioDeviceManager.stop() if (proximityLock?.isHeld == true) { proximityLock.release() } @@ -147,7 +120,7 @@ actual fun ActiveCallView() { val callType = CallType(call.localMedia, r.capabilities) chatModel.controller.apiSendCallInvitation(callRh, call.contact, callType) updateActiveCall(call) { it.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities) } - setCallSound(call.soundSpeaker, audioViaBluetooth) + callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.soundSpeaker, true) CallSoundsPlayer.startConnectingCallSound(scope) activeCallWaitDeliveryReceipt(scope) } @@ -168,7 +141,7 @@ actual fun ActiveCallView() { val callStatus = json.decodeFromString("\"${r.state.connectionState}\"") if (callStatus == WebRTCCallStatus.Connected) { updateActiveCall(call) { it.copy(callState = CallState.Connected, connectedAt = Clock.System.now()) } - setCallSound(call.soundSpeaker, audioViaBluetooth) + callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.soundSpeaker, true) } withBGApi { chatModel.controller.apiCallStatus(callRh, call.contact, callStatus) } } catch (e: Throwable) { @@ -177,7 +150,7 @@ actual fun ActiveCallView() { is WCallResponse.Connected -> { updateActiveCall(call) { it.copy(callState = CallState.Connected, connectionInfo = r.connectionInfo) } scope.launch { - setCallSound(call.soundSpeaker, audioViaBluetooth) + callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.soundSpeaker, true) } } is WCallResponse.End -> { @@ -223,7 +196,7 @@ actual fun ActiveCallView() { else -> false } if (call != null && showOverlay) { - ActiveCallOverlay(call, chatModel, audioViaBluetooth) + ActiveCallOverlay(call, chatModel, callAudioDeviceManager) } } val context = LocalContext.current @@ -249,19 +222,21 @@ actual fun ActiveCallView() { } @Composable -private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, audioViaBluetooth: MutableState) { +private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, callAudioDeviceManager: CallAudioDeviceManagerInterface) { ActiveCallOverlayLayout( call = call, - speakerCanBeEnabled = !audioViaBluetooth.value, + devices = remember { callAudioDeviceManager.devices }.value, + currentDevice = remember { callAudioDeviceManager.currentDevice }, dismiss = { withBGApi { chatModel.callManager.endCall(call) } }, toggleAudio = { chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Audio, enable = !call.audioEnabled)) }, + selectDevice = { callAudioDeviceManager.selectDevice(it.id) }, toggleVideo = { chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Video, enable = !call.videoEnabled)) }, toggleSound = { var call = chatModel.activeCall.value if (call != null) { call = call.copy(soundSpeaker = !call.soundSpeaker) chatModel.activeCall.value = call - setCallSound(call.soundSpeaker, audioViaBluetooth) + callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.soundSpeaker, true) } }, flipCamera = { chatModel.callCommand.add(WCallCommand.Camera(call.localCamera.flipped)) } @@ -272,42 +247,18 @@ private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, audioViaBluetoot fun ActiveCallOverlayDisabled(call: Call) { ActiveCallOverlayLayout( call = call, - speakerCanBeEnabled = false, + devices = emptyList(), + currentDevice = remember { mutableStateOf(null) }, enabled = false, dismiss = {}, toggleAudio = {}, + selectDevice = {}, toggleVideo = {}, toggleSound = {}, flipCamera = {} ) } -private fun setCallSound(speaker: Boolean, audioViaBluetooth: MutableState) { - val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager - Log.d(TAG, "setCallSound: set audio mode, speaker enabled: $speaker") - am.mode = AudioManager.MODE_IN_COMMUNICATION - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - val btDevice = am.availableCommunicationDevices.lastOrNull { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO } - val preferredSecondaryDevice = if (speaker) AudioDeviceInfo.TYPE_BUILTIN_SPEAKER else AudioDeviceInfo.TYPE_BUILTIN_EARPIECE - if (btDevice != null) { - am.setCommunicationDevice(btDevice) - } else if (am.communicationDevice?.type != preferredSecondaryDevice) { - am.availableCommunicationDevices.firstOrNull { it.type == preferredSecondaryDevice }?.let { - am.setCommunicationDevice(it) - } - } - } else { - if (audioViaBluetooth.value) { - am.isSpeakerphoneOn = false - am.startBluetoothSco() - } else { - am.stopBluetoothSco() - am.isSpeakerphoneOn = speaker - } - am.isBluetoothScoOn = am.isBluetoothScoAvailableOffCall && audioViaBluetooth.value - } -} - private fun dropAudioManagerOverrides() { val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager am.mode = AudioManager.MODE_NORMAL @@ -323,10 +274,12 @@ private fun dropAudioManagerOverrides() { @Composable private fun ActiveCallOverlayLayout( call: Call, - speakerCanBeEnabled: Boolean, + devices: List, + currentDevice: State, enabled: Boolean = true, dismiss: () -> Unit, toggleAudio: () -> Unit, + selectDevice: (AudioDeviceInfo) -> Unit, toggleVideo: () -> Unit, toggleSound: () -> Unit, flipCamera: () -> Unit @@ -339,6 +292,32 @@ private fun ActiveCallOverlayLayout( } } Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { + @Composable + fun SelectSoundDevice() { + if (devices.size == 2 && + devices.all { it.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE || it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER } || + currentDevice.value == null || + devices.none { it.id == currentDevice.value?.id } + ) { + ToggleSoundButton(call, enabled, toggleSound) + } else { + ExposedDropDownSettingWithIcon( + devices.map { Triple(it, it.icon, if (it.name != null) generalGetString(it.name!!) else it.productName.toString()) }, + currentDevice, + fontSize = 18.sp, + iconSize = 40.dp, + listIconSize = 30.dp, + iconColor = Color(0xFFFFFFD8), + minWidth = 300.dp, + onSelected = { + if (it != null) { + selectDevice(it) + } + } + ) + } + } + when (media) { CallMediaType.Video -> { VideoCallInfoView(call) @@ -347,7 +326,7 @@ private fun ActiveCallOverlayLayout( } Row(Modifier.fillMaxWidth().padding(horizontal = 6.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { ToggleAudioButton(call, enabled, toggleAudio) - Spacer(Modifier.size(40.dp)) + SelectSoundDevice() IconButton(onClick = dismiss, enabled = enabled) { Icon(painterResource(MR.images.ic_call_end_filled), stringResource(MR.strings.icon_descr_hang_up), tint = if (enabled) Color.Red else MaterialTheme.colors.secondary, modifier = Modifier.size(64.dp)) } @@ -385,7 +364,7 @@ private fun ActiveCallOverlayLayout( } Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) { Box(Modifier.padding(end = 32.dp)) { - ToggleSoundButton(call, speakerCanBeEnabled && enabled, toggleSound) + SelectSoundDevice() } } } @@ -780,9 +759,11 @@ fun PreviewActiveCallOverlayVideo() { RTCIceCandidate(RTCIceCandidateType.Host, "tcp") ) ), - speakerCanBeEnabled = true, + devices = emptyList(), + currentDevice = remember { mutableStateOf(null) }, dismiss = {}, toggleAudio = {}, + selectDevice = {}, toggleVideo = {}, toggleSound = {}, flipCamera = {} @@ -807,9 +788,11 @@ fun PreviewActiveCallOverlayAudio() { RTCIceCandidate(RTCIceCandidateType.Host, "udp") ) ), - speakerCanBeEnabled = true, + devices = emptyList(), + currentDevice = remember { mutableStateOf(null) }, dismiss = {}, toggleAudio = {}, + selectDevice = {}, toggleVideo = {}, toggleSound = {}, flipCamera = {} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 90d6995504..920a440267 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -135,6 +135,7 @@ class AppPreferences { 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 networkTCPTimeoutPerKb = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_TIMEOUT_PER_KB, NetCfg.defaults.tcpTimeoutPerKb, NetCfg.proxyDefaults.tcpTimeoutPerKb) + val networkRcvConcurrency = mkIntPreference(SHARED_PREFS_NETWORK_RCV_CONCURRENCY, NetCfg.defaults.rcvConcurrency) val networkSMPPingInterval = mkLongPreference(SHARED_PREFS_NETWORK_SMP_PING_INTERVAL, NetCfg.defaults.smpPingInterval) val networkSMPPingCount = mkIntPreference(SHARED_PREFS_NETWORK_SMP_PING_COUNT, NetCfg.defaults.smpPingCount) val networkEnableKeepAlive = mkBoolPreference(SHARED_PREFS_NETWORK_ENABLE_KEEP_ALIVE, NetCfg.defaults.enableKeepAlive) @@ -304,6 +305,7 @@ class AppPreferences { 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_TCP_TIMEOUT_PER_KB = "networkTCPTimeoutPerKb" + private const val SHARED_PREFS_NETWORK_RCV_CONCURRENCY = "networkRcvConcurrency" private const val SHARED_PREFS_NETWORK_SMP_PING_INTERVAL = "NetworkSMPPingInterval" private const val SHARED_PREFS_NETWORK_SMP_PING_COUNT = "NetworkSMPPingCount" private const val SHARED_PREFS_NETWORK_ENABLE_KEEP_ALIVE = "NetworkEnableKeepAlive" @@ -2305,6 +2307,7 @@ object ChatController { val tcpConnectTimeout = appPrefs.networkTCPConnectTimeout.get() val tcpTimeout = appPrefs.networkTCPTimeout.get() val tcpTimeoutPerKb = appPrefs.networkTCPTimeoutPerKb.get() + val rcvConcurrency = appPrefs.networkRcvConcurrency.get() val smpPingInterval = appPrefs.networkSMPPingInterval.get() val smpPingCount = appPrefs.networkSMPPingCount.get() val enableKeepAlive = appPrefs.networkEnableKeepAlive.get() @@ -2324,6 +2327,7 @@ object ChatController { tcpConnectTimeout = tcpConnectTimeout, tcpTimeout = tcpTimeout, tcpTimeoutPerKb = tcpTimeoutPerKb, + rcvConcurrency = rcvConcurrency, tcpKeepAlive = tcpKeepAlive, smpPingInterval = smpPingInterval, smpPingCount = smpPingCount @@ -2341,6 +2345,7 @@ object ChatController { appPrefs.networkTCPConnectTimeout.set(cfg.tcpConnectTimeout) appPrefs.networkTCPTimeout.set(cfg.tcpTimeout) appPrefs.networkTCPTimeoutPerKb.set(cfg.tcpTimeoutPerKb) + appPrefs.networkRcvConcurrency.set(cfg.rcvConcurrency) appPrefs.networkSMPPingInterval.set(cfg.smpPingInterval) appPrefs.networkSMPPingCount.set(cfg.smpPingCount) if (cfg.tcpKeepAlive != null) { @@ -3034,6 +3039,7 @@ data class NetCfg( val tcpConnectTimeout: Long, // microseconds val tcpTimeout: Long, // microseconds val tcpTimeoutPerKb: Long, // microseconds + val rcvConcurrency: Int, // pool size val tcpKeepAlive: KeepAliveOpts?, val smpPingInterval: Long, // microseconds val smpPingCount: Int, @@ -3058,9 +3064,10 @@ data class NetCfg( hostMode = HostMode.OnionViaSocks, requiredHostMode = false, sessionMode = TransportSessionMode.User, - tcpConnectTimeout = 20_000_000, + tcpConnectTimeout = 10_000_000, tcpTimeout = 15_000_000, tcpTimeoutPerKb = 10_000, + rcvConcurrency = 12, tcpKeepAlive = KeepAliveOpts.defaults, smpPingInterval = 1200_000_000, smpPingCount = 3 @@ -3072,9 +3079,10 @@ data class NetCfg( hostMode = HostMode.OnionViaSocks, requiredHostMode = false, sessionMode = TransportSessionMode.User, - tcpConnectTimeout = 30_000_000, + tcpConnectTimeout = 20_000_000, tcpTimeout = 20_000_000, tcpTimeoutPerKb = 15_000, + rcvConcurrency = 8, tcpKeepAlive = KeepAliveOpts.defaults, smpPingInterval = 1200_000_000, smpPingCount = 3 diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt index 043aa5ec84..7e57bda928 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.unit.* import chat.simplex.res.MR import chat.simplex.common.ui.theme.* import chat.simplex.common.views.usersettings.SettingsActionItemWithContent +import dev.icerock.moko.resources.ImageResource @Composable fun ExposedDropDownSetting( @@ -79,6 +80,60 @@ fun ExposedDropDownSetting( } } +@Composable +fun ExposedDropDownSettingWithIcon( + values: List>, + selection: State, + fontSize: TextUnit = 16.sp, + iconSize: Dp = 40.dp, + listIconSize: Dp = 30.dp, + iconColor: Color = MenuTextColor, + enabled: State = mutableStateOf(true), + minWidth: Dp = 200.dp, + onSelected: (T) -> Unit +) { + val expanded = remember { mutableStateOf(false) } + ExposedDropdownMenuBox( + expanded = expanded.value, + onExpandedChange = { + expanded.value = !expanded.value && enabled.value + } + ) { + Row( + Modifier.padding(start = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End + ) { + val choice = values.first { it.first == selection.value } + Icon(painterResource(choice.second), choice.third, Modifier.size(iconSize), tint = iconColor) + } + DefaultExposedDropdownMenu( + modifier = Modifier.widthIn(min = minWidth), + expanded = expanded, + ) { + values.forEach { selectionOption -> + DropdownMenuItem( + onClick = { + onSelected(selectionOption.first) + expanded.value = false + }, + contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f) + ) { + Icon(painterResource(selectionOption.second), selectionOption.third, Modifier.size(listIconSize)) + Spacer(Modifier.width(15.dp)) + Text( + selectionOption.third, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = MenuTextColor, + fontSize = fontSize, + ) + } + } + } + } +} + @Composable fun ExposedDropDownSettingRow( title: String, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt index c031a0fcb7..88368c9d1d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt @@ -34,6 +34,7 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) { val networkTCPConnectTimeout = remember { mutableStateOf(currentCfgVal.tcpConnectTimeout) } val networkTCPTimeout = remember { mutableStateOf(currentCfgVal.tcpTimeout) } val networkTCPTimeoutPerKb = remember { mutableStateOf(currentCfgVal.tcpTimeoutPerKb) } + var networkRcvConcurrency = remember { mutableStateOf(currentCfgVal.rcvConcurrency) } val networkSMPPingInterval = remember { mutableStateOf(currentCfgVal.smpPingInterval) } val networkSMPPingCount = remember { mutableStateOf(currentCfgVal.smpPingCount) } val networkEnableKeepAlive = remember { mutableStateOf(currentCfgVal.enableKeepAlive) } @@ -68,6 +69,7 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) { tcpConnectTimeout = networkTCPConnectTimeout.value, tcpTimeout = networkTCPTimeout.value, tcpTimeoutPerKb = networkTCPTimeoutPerKb.value, + rcvConcurrency = networkRcvConcurrency.value, tcpKeepAlive = tcpKeepAlive, smpPingInterval = networkSMPPingInterval.value, smpPingCount = networkSMPPingCount.value @@ -78,6 +80,7 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) { networkTCPConnectTimeout.value = cfg.tcpConnectTimeout networkTCPTimeout.value = cfg.tcpTimeout networkTCPTimeoutPerKb.value = cfg.tcpTimeoutPerKb + networkRcvConcurrency.value = cfg.rcvConcurrency networkSMPPingInterval.value = cfg.smpPingInterval networkSMPPingCount.value = cfg.smpPingCount networkEnableKeepAlive.value = cfg.enableKeepAlive @@ -110,6 +113,7 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) { networkTCPConnectTimeout, networkTCPTimeout, networkTCPTimeoutPerKb, + networkRcvConcurrency, networkSMPPingInterval, networkSMPPingCount, networkEnableKeepAlive, @@ -128,6 +132,7 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) { networkTCPConnectTimeout: MutableState, networkTCPTimeout: MutableState, networkTCPTimeoutPerKb: MutableState, + networkRcvConcurrency: MutableState, networkSMPPingInterval: MutableState, networkSMPPingCount: MutableState, networkEnableKeepAlive: MutableState, @@ -154,7 +159,7 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) { SectionItemView { TimeoutSettingRow( stringResource(MR.strings.network_option_tcp_connection_timeout), networkTCPConnectTimeout, - listOf(7_500000, 10_000000, 15_000000, 20_000000, 30_000_000, 45_000_000), secondsLabel + listOf(5_000000, 7_500000, 10_000000, 15_000000, 20_000000, 30_000000, 40_000000), secondsLabel ) } SectionItemView { @@ -170,6 +175,12 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) { listOf(2_500, 5_000, 10_000, 15_000, 20_000, 30_000), secondsLabel ) } + SectionItemView { + IntSettingRow( + stringResource(MR.strings.network_option_rcv_concurrency), networkRcvConcurrency, + listOf(1, 2, 4, 8, 12, 16, 24), "" + ) + } SectionItemView { TimeoutSettingRow( stringResource(MR.strings.network_option_ping_interval), networkSMPPingInterval, @@ -418,6 +429,7 @@ fun PreviewAdvancedNetworkSettingsLayout() { networkTCPConnectTimeout = remember { mutableStateOf(10_000000) }, networkTCPTimeout = remember { mutableStateOf(10_000000) }, networkTCPTimeoutPerKb = remember { mutableStateOf(10_000) }, + networkRcvConcurrency = remember { mutableStateOf(8) }, networkSMPPingInterval = remember { mutableStateOf(10_000000) }, networkSMPPingCount = remember { mutableStateOf(3) }, networkEnableKeepAlive = remember { mutableStateOf(true) }, diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 7c7cb2c384..d30db06f9f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -836,6 +836,9 @@ Grant in settings Find this permission in Android settings and grant it manually. Open settings + Earpiece + Speaker + Headphones The next generation of private messaging @@ -1456,6 +1459,7 @@ TCP connection timeout Protocol timeout Protocol timeout per KB + Receiving concurrency PING interval PING count Enable TCP keep-alive diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bluetooth.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bluetooth.svg new file mode 100644 index 0000000000..53bc5becaa --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bluetooth.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_brand_awareness_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_brand_awareness_filled.svg new file mode 100644 index 0000000000..fef2b1d2ce --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_brand_awareness_filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_headphones.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_headphones.svg new file mode 100644 index 0000000000..2bda1e9d74 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_headphones.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_phone_bluetooth_speaker.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_phone_bluetooth_speaker.svg new file mode 100644 index 0000000000..513bb38c40 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_phone_bluetooth_speaker.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_usb.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_usb.svg new file mode 100644 index 0000000000..068bfc1a82 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_usb.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cabal.project b/cabal.project index 02deb0dbfb..a0ba556c1c 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: c00c223f3bb295a62d8507e453bbeac61d102e3a + tag: 3d40393ae83260245c7122b850fb42cd4243deba source-repository-package type: git diff --git a/package.yaml b/package.yaml index e844c52c26..2ef0fce30b 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplex-chat -version: 5.7.0.0 +version: 5.7.0.1 #synopsis: #description: homepage: https://github.com/simplex-chat/simplex-chat#readme diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 0b264d3628..0961e737c4 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."c00c223f3bb295a62d8507e453bbeac61d102e3a" = "0zbsz70rjhvrlkkiwnw43v7lg6r05lp5rwk7jmnn21zfjib4l621"; + "https://github.com/simplex-chat/simplexmq.git"."3d40393ae83260245c7122b850fb42cd4243deba" = "0hkfz5k8kn1lqq0ms0b266pzsk48pq1gkfm7wgw6w5mr1pw149ay"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index aac942c6b3..c16c56f324 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 5.7.0.0 +version: 5.7.0.1 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat