From d8b52ee0d355a5762bce182a8176885550769d7a Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Thu, 11 Apr 2024 17:31:30 +0700 Subject: [PATCH] ios: network observer (#3996) * ios: network observer * update * remove metered * update tcpTimeoutPerKb * network connection in settings --------- Co-authored-by: Evgeny Poberezkin --- apps/ios/Shared/Model/ChatModel.swift | 1 + apps/ios/Shared/Model/NetworkObserver.swift | 73 +++++++++++++++++++ apps/ios/Shared/Model/SimpleXAPI.swift | 7 ++ apps/ios/Shared/Views/Call/WebRTCClient.swift | 2 +- .../Views/Chat/ChatItem/CIVideoView.swift | 2 +- .../AdvancedNetworkSettings.swift | 2 +- .../UserSettings/NetworkAndServers.swift | 9 +++ apps/ios/SimpleX.xcodeproj/project.pbxproj | 4 + apps/ios/SimpleXChat/APITypes.swift | 35 ++++++++- .../chat/simplex/common/model/SimpleXAPI.kt | 4 +- .../usersettings/AdvancedNetworkSettings.kt | 2 +- 11 files changed, 133 insertions(+), 8 deletions(-) create mode 100644 apps/ios/Shared/Model/NetworkObserver.swift diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 462699e407..90a9406627 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -103,6 +103,7 @@ final class ChatModel: ObservableObject { // tracks keyboard height via subscription in AppDelegate @Published var keyboardHeight: CGFloat = 0 @Published var pasteboardHasStrings: Bool = UIPasteboard.general.hasStrings + @Published var networkInfo = UserNetworkInfo(networkType: .other, online: true) var messageDelivery: Dictionary Void> = [:] diff --git a/apps/ios/Shared/Model/NetworkObserver.swift b/apps/ios/Shared/Model/NetworkObserver.swift new file mode 100644 index 0000000000..84c35afa07 --- /dev/null +++ b/apps/ios/Shared/Model/NetworkObserver.swift @@ -0,0 +1,73 @@ +// +// NetworkObserver.swift +// SimpleX (iOS) +// +// Created by Avently on 05.04.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import Foundation +import Network +import SimpleXChat + +class NetworkObserver { + static let shared = NetworkObserver() + private let queue: DispatchQueue = DispatchQueue(label: "chat.simplex.app.NetworkObserver") + private var prevInfo: UserNetworkInfo? = nil + private var monitor: NWPathMonitor? + private let monitorLock: DispatchQueue = DispatchQueue(label: "chat.simplex.app.monitorLock") + + func restartMonitor() { + monitorLock.sync { + monitor?.cancel() + let mon = NWPathMonitor() + mon.pathUpdateHandler = { [weak self] path in + self?.networkPathChanged(path: path) + } + mon.start(queue: queue) + monitor = mon + } + } + + private func networkPathChanged(path: NWPath) { + let info = UserNetworkInfo( + networkType: networkTypeFromPath(path), + online: path.status == .satisfied + ) + if (prevInfo != info) { + prevInfo = info + setNetworkInfo(info) + } + } + + private func networkTypeFromPath(_ path: NWPath) -> UserNetworkType { + if path.usesInterfaceType(.wiredEthernet) { + .ethernet + } else if path.usesInterfaceType(.wifi) { + .wifi + } else if path.usesInterfaceType(.cellular) { + .cellular + } else if path.usesInterfaceType(.other) { + .other + } else { + .none + } + } + + private static var networkObserver: NetworkObserver? = nil + + private func setNetworkInfo(_ info: UserNetworkInfo) { + logger.debug("setNetworkInfo Network changed: \(String(describing: info))") + DispatchQueue.main.sync { + ChatModel.shared.networkInfo = info + } + if !hasChatCtrl() { return } + self.monitorLock.sync { + do { + try apiSetNetworkInfo(info) + } catch let err { + logger.error("setNetworkInfo error: \(responseError(err))") + } + } + } +} diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index a099069f77..94a424d319 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -534,6 +534,12 @@ func setNetworkConfig(_ cfg: NetCfg, ctrl: chat_ctrl? = nil) throws { throw r } +func apiSetNetworkInfo(_ networkInfo: UserNetworkInfo) throws { + let r = chatSendCmdSync(.apiSetNetworkInfo(networkInfo: networkInfo)) + if case .cmdOk = r { return } + throw r +} + func reconnectAllServers() async throws { try await sendCommandOkResp(.reconnectAllServers) } @@ -1297,6 +1303,7 @@ func initializeChat(start: Bool, confirmStart: Bool = false, dbKey: String? = ni defer { m.ctrlInitInProgress = false } (m.chatDbEncrypted, m.chatDbStatus) = chatMigrateInit(dbKey, confirmMigrations: confirmMigrations) if m.chatDbStatus != .ok { return } + NetworkObserver.shared.restartMonitor() // If we migrated successfully means previous re-encryption process on database level finished successfully too if encryptionStartedDefault.get() { encryptionStartedDefault.set(false) diff --git a/apps/ios/Shared/Views/Call/WebRTCClient.swift b/apps/ios/Shared/Views/Call/WebRTCClient.swift index 1806984d64..834af4a2c1 100644 --- a/apps/ios/Shared/Views/Call/WebRTCClient.swift +++ b/apps/ios/Shared/Views/Call/WebRTCClient.swift @@ -49,7 +49,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg } private let rtcAudioSession = RTCAudioSession.sharedInstance() - private let audioQueue = DispatchQueue(label: "audio") + private let audioQueue = DispatchQueue(label: "chat.simplex.app.audio") private var sendCallResponse: (WVAPIMessage) async -> Void var activeCall: Binding private var localRendererAspectRatio: Binding diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift index a3918e17bc..b4b190a43a 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift @@ -126,7 +126,7 @@ struct CIVideoView: View { if !decryptionInProgress { Button { decrypt(file: file) { - if let decrypted = urlDecrypted { + if urlDecrypted != nil { videoPlaying = true player?.play() } diff --git a/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift b/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift index 9da3bac00b..9f03b95321 100644 --- a/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift @@ -53,7 +53,7 @@ struct AdvancedNetworkSettings: View { timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout, values: [7_500000, 10_000000, 15_000000, 20_000000, 30_000000, 45_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: [15_000, 30_000, 45_000, 60_000, 90_000, 120_000], 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) 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/Shared/Views/UserSettings/NetworkAndServers.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift index d721cfad50..a6702b1821 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift @@ -24,6 +24,7 @@ private enum NetworkAlert: Identifiable { } struct NetworkAndServers: View { + @EnvironmentObject var m: ChatModel @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @State private var cfgLoaded = false @State private var currentNetCfg = NetCfg.defaults @@ -82,6 +83,14 @@ struct NetworkAndServers: View { Text("WebRTC ICE servers") } } + + Section("Network connection") { + HStack { + Text(m.networkInfo.networkType.text) + Spacer() + Image(systemName: "circle.fill").foregroundColor(m.networkInfo.online ? .green : .red) + } + } } } .onAppear { diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 6cd181940a..d7ca3e1cb5 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -187,6 +187,7 @@ 8C69FE7D2B8C7D2700267E38 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */; }; 8C7D949A2B88952700B7B9E1 /* MigrateToDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7D94992B88952700B7B9E1 /* MigrateToDevice.swift */; }; 8C7DF3202B7CDB0A00C886D0 /* MigrateFromDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7DF31F2B7CDB0A00C886D0 /* MigrateFromDevice.swift */; }; + 8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */; }; D7197A1829AE89660055C05A /* WebRTC in Frameworks */ = {isa = PBXBuildFile; productRef = D7197A1729AE89660055C05A /* WebRTC */; }; D72A9088294BD7A70047C86D /* NativeTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A9087294BD7A70047C86D /* NativeTextEditor.swift */; }; D741547829AF89AF0022400A /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D741547729AF89AF0022400A /* StoreKit.framework */; }; @@ -480,6 +481,7 @@ 8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = ""; }; 8C7D94992B88952700B7B9E1 /* MigrateToDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToDevice.swift; sourceTree = ""; }; 8C7DF31F2B7CDB0A00C886D0 /* MigrateFromDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateFromDevice.swift; sourceTree = ""; }; + 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkObserver.swift; sourceTree = ""; }; D72A9087294BD7A70047C86D /* NativeTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeTextEditor.swift; sourceTree = ""; }; D741547729AF89AF0022400A /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; }; D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; }; @@ -623,6 +625,7 @@ 5CB346E82869E8BA001FD2EF /* PushEnvironment.swift */, 5C93293E2928E0FD0090FFF9 /* AudioRecPlay.swift */, 5CBD2859295711D700EC2CF4 /* ImageUtils.swift */, + 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */, ); path = Model; sourceTree = ""; @@ -1176,6 +1179,7 @@ 5C9A5BDB2871E05400A5B906 /* SetNotificationsMode.swift in Sources */, 5CB0BA8E2827126500B3292C /* OnboardingView.swift in Sources */, 6442E0BE2880182D00CEC0F9 /* GroupChatInfoView.swift in Sources */, + 8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */, 5C2E261227A30FEA00F70299 /* TerminalView.swift in Sources */, 5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */, 5CA7DFC329302AF000F7FDDE /* AppSheet.swift in Sources */, diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index b18022cfec..c0ff697a1d 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -77,6 +77,7 @@ public enum ChatCommand { case apiGetChatItemTTL(userId: Int64) case apiSetNetworkConfig(networkConfig: NetCfg) case apiGetNetworkConfig + case apiSetNetworkInfo(networkInfo: UserNetworkInfo) case reconnectAllServers case apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings) case apiSetMemberSettings(groupId: Int64, groupMemberId: Int64, memberSettings: GroupMemberSettings) @@ -221,6 +222,7 @@ public enum ChatCommand { case let .apiGetChatItemTTL(userId): return "/_ttl \(userId)" case let .apiSetNetworkConfig(networkConfig): return "/_network \(encodeJSON(networkConfig))" case .apiGetNetworkConfig: return "/network" + case let .apiSetNetworkInfo(networkInfo): return "/_network info \(encodeJSON(networkInfo))" case .reconnectAllServers: return "/reconnect" case let .apiSetChatSettings(type, id, chatSettings): return "/_settings \(ref(type, id)) \(encodeJSON(chatSettings))" case let .apiSetMemberSettings(groupId, groupMemberId, memberSettings): return "/_member settings #\(groupId) \(groupMemberId) \(encodeJSON(memberSettings))" @@ -368,6 +370,7 @@ public enum ChatCommand { case .apiGetChatItemTTL: return "apiGetChatItemTTL" case .apiSetNetworkConfig: return "apiSetNetworkConfig" case .apiGetNetworkConfig: return "apiGetNetworkConfig" + case .apiSetNetworkInfo: return "apiSetNetworkInfo" case .reconnectAllServers: return "reconnectAllServers" case .apiSetChatSettings: return "apiSetChatSettings" case .apiSetMemberSettings: return "apiSetMemberSettings" @@ -1261,7 +1264,7 @@ public struct NetCfg: Codable, Equatable { sessionMode: TransportSessionMode.user, tcpConnectTimeout: 20_000_000, tcpTimeout: 15_000_000, - tcpTimeoutPerKb: 45_000, + tcpTimeoutPerKb: 10_000, tcpKeepAlive: KeepAliveOpts.defaults, smpPingInterval: 1200_000_000, smpPingCount: 3, @@ -1273,7 +1276,7 @@ public struct NetCfg: Codable, Equatable { sessionMode: TransportSessionMode.user, tcpConnectTimeout: 30_000_000, tcpTimeout: 20_000_000, - tcpTimeoutPerKb: 60_000, + tcpTimeoutPerKb: 15_000, tcpKeepAlive: KeepAliveOpts.defaults, smpPingInterval: 1200_000_000, smpPingCount: 3, @@ -2086,3 +2089,31 @@ public enum AppSettingsLockScreenCalls: String, Codable { case show case accept } + +public struct UserNetworkInfo: Codable, Equatable { + public let networkType: UserNetworkType + public let online: Bool + + public init(networkType: UserNetworkType, online: Bool) { + self.networkType = networkType + self.online = online + } +} + +public enum UserNetworkType: String, Codable { + case none + case cellular + case wifi + case ethernet + case other + + public var text: LocalizedStringKey { + switch self { + case .none: "No network connection" + case .cellular: "Cellular" + case .wifi: "WiFi" + case .ethernet: "Wired ethernet" + case .other: "Other" + } + } +} 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 38619cfef2..3ce9b68b5e 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 @@ -3035,7 +3035,7 @@ data class NetCfg( sessionMode = TransportSessionMode.User, tcpConnectTimeout = 20_000_000, tcpTimeout = 15_000_000, - tcpTimeoutPerKb = 45_000, + tcpTimeoutPerKb = 10_000, tcpKeepAlive = KeepAliveOpts.defaults, smpPingInterval = 1200_000_000, smpPingCount = 3 @@ -3049,7 +3049,7 @@ data class NetCfg( sessionMode = TransportSessionMode.User, tcpConnectTimeout = 30_000_000, tcpTimeout = 20_000_000, - tcpTimeoutPerKb = 60_000, + tcpTimeoutPerKb = 15_000, tcpKeepAlive = KeepAliveOpts.defaults, smpPingInterval = 1200_000_000, smpPingCount = 3 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 a2ea5959c3..c031a0fcb7 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 @@ -167,7 +167,7 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) { // can't be higher than 130ms to avoid overflow on 32bit systems TimeoutSettingRow( stringResource(MR.strings.network_option_protocol_timeout_per_kb), networkTCPTimeoutPerKb, - listOf(15_000, 30_000, 45_000, 60_000, 90_000, 120_000), secondsLabel + listOf(2_500, 5_000, 10_000, 15_000, 20_000, 30_000), secondsLabel ) } SectionItemView {