From 8f0a9cd6090920c89667557e082c1449c8a2ca0d Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 23 Nov 2023 21:22:29 +0000 Subject: [PATCH] ios: connect remote desktop via multicast (#3436) * ios: connect remote desktop via multicast * works * fix camera freeze when leaving linked devices view * label * fix linked devices * fix compatible * string --- apps/ios/Shared/Model/ChatModel.swift | 8 +- apps/ios/Shared/Model/SimpleXAPI.swift | 29 +++- .../RemoteAccess/ConnectDesktopView.swift | 152 ++++++++++++++++-- .../Views/UserSettings/SettingsView.swift | 6 +- apps/ios/SimpleXChat/APITypes.swift | 5 +- .../chat/simplex/common/model/SimpleXAPI.kt | 2 +- 6 files changed, 173 insertions(+), 29 deletions(-) diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 90e4272b0..4c0f36102 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -770,7 +770,7 @@ final class GMember: ObservableObject, Identifiable { } struct RemoteCtrlSession { - var ctrlAppInfo: CtrlAppInfo + var ctrlAppInfo: CtrlAppInfo? var appVersion: String var sessionState: UIRemoteCtrlSessionState @@ -782,6 +782,10 @@ struct RemoteCtrlSession { if case .connected = sessionState { true } else { false } } + var discovery: Bool { + if case .searching = sessionState { true } else { false } + } + var sessionCode: String? { switch sessionState { case let .pendingConfirmation(_, sessionCode): sessionCode @@ -793,6 +797,8 @@ struct RemoteCtrlSession { enum UIRemoteCtrlSessionState { case starting + case searching + case found(remoteCtrl: RemoteCtrlInfo, compatible: Bool) case connecting(remoteCtrl_: RemoteCtrlInfo?) case pendingConfirmation(remoteCtrl_: RemoteCtrlInfo?, sessionCode: String) case connected(remoteCtrl: RemoteCtrlInfo, sessionCode: String) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 9e4cc7cd0..e010de3e8 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -919,8 +919,10 @@ func findKnownRemoteCtrl() async throws { try await sendCommandOkResp(.findKnownRemoteCtrl) } -func confirmRemoteCtrl(_ rcId: Int64) async throws { - try await sendCommandOkResp(.confirmRemoteCtrl(remoteCtrlId: rcId)) +func confirmRemoteCtrl(_ rcId: Int64) async throws -> (RemoteCtrlInfo?, CtrlAppInfo, String) { + let r = await chatSendCmd(.confirmRemoteCtrl(remoteCtrlId: rcId)) + if case let .remoteCtrlConnecting(rc_, ctrlAppInfo, v) = r { return (rc_, ctrlAppInfo, v) } + throw r } func verifyRemoteCtrlSession(_ sessCode: String) async throws -> RemoteCtrlInfo { @@ -1714,9 +1716,17 @@ func processReceivedMsg(_ res: ChatResponse) async { await MainActor.run { m.updateGroupMemberConnectionStats(groupInfo, member, ratchetSyncProgress.connectionStats) } - case let .remoteCtrlFound(remoteCtrl): - // TODO multicast - logger.debug("\(String(describing: remoteCtrl))") + case let .remoteCtrlFound(remoteCtrl, ctrlAppInfo_, appVersion, compatible): + await MainActor.run { + if let sess = m.remoteCtrlSession, case .searching = sess.sessionState { + let state = UIRemoteCtrlSessionState.found(remoteCtrl: remoteCtrl, compatible: compatible) + m.remoteCtrlSession = RemoteCtrlSession( + ctrlAppInfo: ctrlAppInfo_, + appVersion: appVersion, + sessionState: state + ) + } + } case let .remoteCtrlSessionCode(remoteCtrl_, sessionCode): await MainActor.run { let state = UIRemoteCtrlSessionState.pendingConfirmation(remoteCtrl_: remoteCtrl_, sessionCode: sessionCode) @@ -1731,8 +1741,13 @@ func processReceivedMsg(_ res: ChatResponse) async { case .remoteCtrlStopped: // This delay is needed to cancel the session that fails on network failure, // e.g. when user did not grant permission to access local network yet. - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - switchToLocalSession() + if let sess = m.remoteCtrlSession { + m.remoteCtrlSession = nil + if case .connected = sess.sessionState { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + switchToLocalSession() + } + } } default: logger.debug("unsupported event: \(res.responseType)") diff --git a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift index 0ee231288..1f120860e 100644 --- a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift +++ b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift @@ -16,12 +16,19 @@ struct ConnectDesktopView: View { var viaSettings = false @AppStorage(DEFAULT_DEVICE_NAME_FOR_REMOTE_ACCESS) private var deviceName = UIDevice.current.name @AppStorage(DEFAULT_CONFIRM_REMOTE_SESSIONS) private var confirmRemoteSessions = false - @AppStorage(DEFAULT_CONNECT_REMOTE_VIA_MULTICAST) private var connectRemoteViaMulticast = false - @AppStorage(DEFAULT_OFFER_REMOTE_MULTICAST) private var offerRemoteMulticast = true + @AppStorage(DEFAULT_CONNECT_REMOTE_VIA_MULTICAST) private var connectRemoteViaMulticast = true + @AppStorage(DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO) private var connectRemoteViaMulticastAuto = true @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @State private var sessionAddress: String = "" @State private var remoteCtrls: [RemoteCtrlInfo] = [] @State private var alert: ConnectDesktopAlert? + @State private var showConnectScreen = true + @State private var showQRCodeScanner = true + @State private var firstAppearance = true + + private var useMulticast: Bool { + connectRemoteViaMulticast && !remoteCtrls.isEmpty + } private enum ConnectDesktopAlert: Identifiable { case unlinkDesktop(rc: RemoteCtrlInfo) @@ -67,9 +74,14 @@ struct ConnectDesktopView: View { var viewBody: some View { Group { - if let session = m.remoteCtrlSession { + let discovery = m.remoteCtrlSession?.discovery + if discovery == true || (discovery == nil && !showConnectScreen) { + searchingDesktopView() + } else if let session = m.remoteCtrlSession { switch session.sessionState { case .starting: connectingDesktopView(session, nil) + case .searching: searchingDesktopView() + case let .found(rc, compatible): foundDesktopView(session, rc, compatible) case let .connecting(rc_): connectingDesktopView(session, rc_) case let .pendingConfirmation(rc_, sessCode): if confirmRemoteSessions || rc_ == nil { @@ -81,16 +93,35 @@ struct ConnectDesktopView: View { } case let .connected(rc, _): activeSessionView(session, rc) } - } else { + // The hack below prevents camera freezing when exiting linked devices view. + // Using showQRCodeScanner inside connectDesktopView or passing it as parameter still results in freezing. + } else if showQRCodeScanner || firstAppearance { connectDesktopView() + } else { + connectDesktopView(showScanner: false) } } .onAppear { setDeviceName(deviceName) updateRemoteCtrls() + showConnectScreen = !useMulticast + if m.remoteCtrlSession != nil { + disconnectDesktop() + } else if useMulticast { + findKnownDesktop() + } + // The hack below prevents camera freezing when exiting linked devices view. + // `firstAppearance` prevents camera flicker when the view first opens. + // moving `showQRCodeScanner = false` to `onDisappear` (to avoid `firstAppearance`) does not prevent freeze. + showQRCodeScanner = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { + firstAppearance = false + showQRCodeScanner = true + } } .onDisappear { if m.remoteCtrlSession != nil { + showConnectScreen = false disconnectDesktop() } } @@ -134,12 +165,14 @@ struct ConnectDesktopView: View { .interactiveDismissDisabled(m.activeRemoteCtrl) } - private func connectDesktopView() -> some View { + private func connectDesktopView(showScanner: Bool = true) -> some View { List { Section("This device name") { devicesView() } - scanDesctopAddressView() + if showScanner { + scanDesctopAddressView() + } if developerTools { desktopAddressView() } @@ -167,6 +200,56 @@ struct ConnectDesktopView: View { .navigationTitle("Connecting to desktop") } + private func searchingDesktopView() -> some View { + List { + Section("This device name") { + devicesView() + } + Section("Found desktop") { + Text("Waiting for desktop...").italic() + Button { + disconnectDesktop(.dismiss) + } label: { + Label("Scan QR code", systemImage: "qrcode") + } + } + } + .navigationTitle("Connecting to desktop") + } + + @ViewBuilder private func foundDesktopView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo, _ compatible: Bool) -> some View { + let v = List { + Section("This device name") { + devicesView() + } + Section("Found desktop") { + ctrlDeviceNameText(session, rc) + ctrlDeviceVersionText(session) + if !compatible { + Text("Not compatible!").foregroundColor(.red) + } else if !connectRemoteViaMulticastAuto { + Button { + confirmKnownDesktop(rc) + } label: { + Label("Connect", systemImage: "checkmark") + } + } + } + if !compatible && !connectRemoteViaMulticastAuto { + Section { + disconnectButton("Cancel") + } + } + } + .navigationTitle("Found desktop") + + if compatible && connectRemoteViaMulticastAuto { + v.onAppear { confirmKnownDesktop(rc) } + } else { + v + } + } + private func verifySessionView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?, _ sessCode: String) -> some View { List { Section("Connected to desktop") { @@ -191,7 +274,7 @@ struct ConnectDesktopView: View { } private func ctrlDeviceNameText(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> Text { - var t = Text(rc?.deviceViewName ?? session.ctrlAppInfo.deviceName) + var t = Text(rc?.deviceViewName ?? session.ctrlAppInfo?.deviceName ?? "") if (rc == nil) { t = t + Text(" ") + Text("(new)").italic() } @@ -199,8 +282,8 @@ struct ConnectDesktopView: View { } private func ctrlDeviceVersionText(_ session: RemoteCtrlSession) -> Text { - let v = session.ctrlAppInfo.appVersionRange.maxVersion - var t = Text("v\(v)") + let v = session.ctrlAppInfo?.appVersionRange.maxVersion + var t = Text("v\(v ?? "")") if v != session.appVersion { t = t + Text(" ") + Text("(this device v\(session.appVersion))").italic() } @@ -301,7 +384,10 @@ struct ConnectDesktopView: View { Section("Linked desktop options") { Toggle("Verify connections", isOn: $confirmRemoteSessions) - Toggle("Discover on network", isOn: $connectRemoteViaMulticast).disabled(true) + Toggle("Discover via local network", isOn: $connectRemoteViaMulticast) + if connectRemoteViaMulticast { + Toggle("Connect automatically", isOn: $connectRemoteViaMulticastAuto) + } } } .navigationTitle("Linked desktops") @@ -335,10 +421,42 @@ struct ConnectDesktopView: View { } } - private func connectDesktopAddress(_ addr: String) { + private func findKnownDesktop() { Task { do { - let (rc_, ctrlAppInfo, v) = try await connectRemoteCtrl(desktopAddress: addr) + try await findKnownRemoteCtrl() + await MainActor.run { + m.remoteCtrlSession = RemoteCtrlSession( + ctrlAppInfo: nil, + appVersion: "", + sessionState: .searching + ) + showConnectScreen = true + } + } catch let e { + await MainActor.run { + errorAlert(e) + } + } + } + } + + private func confirmKnownDesktop(_ rc: RemoteCtrlInfo) { + connectDesktop_ { + try await confirmRemoteCtrl(rc.remoteCtrlId) + } + } + + private func connectDesktopAddress(_ addr: String) { + connectDesktop_ { + try await connectRemoteCtrl(desktopAddress: addr) + } + } + + private func connectDesktop_(_ connect: @escaping () async throws -> (RemoteCtrlInfo?, CtrlAppInfo, String)) { + Task { + do { + let (rc_, ctrlAppInfo, v) = try await connect() await MainActor.run { sessionAddress = "" m.remoteCtrlSession = RemoteCtrlSession( @@ -380,11 +498,11 @@ struct ConnectDesktopView: View { } } - private func disconnectButton() -> some View { + private func disconnectButton(_ label: LocalizedStringKey = "Disconnect") -> some View { Button { disconnectDesktop(.dismiss) } label: { - Label("Disconnect", systemImage: "multiply") + Label(label, systemImage: "multiply") } } @@ -393,7 +511,11 @@ struct ConnectDesktopView: View { do { try await stopRemoteCtrl() await MainActor.run { - switchToLocalSession() + if case .connected = m.remoteCtrlSession?.sessionState { + switchToLocalSession() + } else { + m.remoteCtrlSession = nil + } switch action { case .back: dismiss() case .dismiss: dismiss() diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index 423786eb6..f889d9c39 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -56,7 +56,7 @@ let DEFAULT_SHOW_UNREAD_AND_FAVORITES = "showUnreadAndFavorites" let DEFAULT_DEVICE_NAME_FOR_REMOTE_ACCESS = "deviceNameForRemoteAccess" let DEFAULT_CONFIRM_REMOTE_SESSIONS = "confirmRemoteSessions" let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST = "connectRemoteViaMulticast" -let DEFAULT_OFFER_REMOTE_MULTICAST = "offerRemoteMulticast" +let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "connectRemoteViaMulticastAuto" let appDefaults: [String: Any] = [ DEFAULT_SHOW_LA_NOTICE: false, @@ -91,8 +91,8 @@ let appDefaults: [String: Any] = [ DEFAULT_CUSTOM_DISAPPEARING_MESSAGE_TIME: 300, DEFAULT_SHOW_UNREAD_AND_FAVORITES: false, DEFAULT_CONFIRM_REMOTE_SESSIONS: false, - DEFAULT_CONNECT_REMOTE_VIA_MULTICAST: false, - DEFAULT_OFFER_REMOTE_MULTICAST: true + DEFAULT_CONNECT_REMOTE_VIA_MULTICAST: true, + DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO: true, ] enum SimpleXLinkMode: String, Identifiable { diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index e7409a072..ad0e5ee10 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -609,7 +609,7 @@ public enum ChatResponse: Decodable, Error { case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection) // remote desktop responses/events case remoteCtrlList(remoteCtrls: [RemoteCtrlInfo]) - case remoteCtrlFound(remoteCtrl: RemoteCtrlInfo) + case remoteCtrlFound(remoteCtrl: RemoteCtrlInfo, ctrlAppInfo_: CtrlAppInfo?, appVersion: String, compatible: Bool) case remoteCtrlConnecting(remoteCtrl_: RemoteCtrlInfo?, ctrlAppInfo: CtrlAppInfo, appVersion: String) case remoteCtrlSessionCode(remoteCtrl_: RemoteCtrlInfo?, sessionCode: String) case remoteCtrlConnected(remoteCtrl: RemoteCtrlInfo) @@ -903,7 +903,7 @@ public enum ChatResponse: Decodable, Error { case let .newContactConnection(u, connection): return withUser(u, String(describing: connection)) case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection)) case let .remoteCtrlList(remoteCtrls): return String(describing: remoteCtrls) - case let .remoteCtrlFound(remoteCtrl): return String(describing: remoteCtrl) + case let .remoteCtrlFound(remoteCtrl, ctrlAppInfo_, appVersion, compatible): return "remoteCtrl:\n\(String(describing: remoteCtrl))\nctrlAppInfo_:\n\(String(describing: ctrlAppInfo_))\nappVersion: \(appVersion)\ncompatible: \(compatible)" case let .remoteCtrlConnecting(remoteCtrl_, ctrlAppInfo, appVersion): return "remoteCtrl_:\n\(String(describing: remoteCtrl_))\nctrlAppInfo:\n\(String(describing: ctrlAppInfo))\nappVersion: \(appVersion)" case let .remoteCtrlSessionCode(remoteCtrl_, sessionCode): return "remoteCtrl_:\n\(String(describing: remoteCtrl_))\nsessionCode: \(sessionCode)" case let .remoteCtrlConnected(remoteCtrl): return String(describing: remoteCtrl) @@ -1546,6 +1546,7 @@ public struct RemoteCtrlInfo: Decodable { public enum RemoteCtrlSessionState: Decodable { case starting + case searching case connecting case pendingConfirmation(sessionCode: String) case connected(sessionCode: String) 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 20fa4abf5..56b4b0a13 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 @@ -1394,7 +1394,7 @@ object ChatController { chatModel.remoteHosts.addAll(hosts) } - suspend fun startRemoteHost(rhId: Long?, multicast: Boolean = false): Triple? { + suspend fun startRemoteHost(rhId: Long?, multicast: Boolean = true): Triple? { val r = sendCmd(null, CC.StartRemoteHost(rhId, multicast)) if (r is CR.RemoteHostStarted) return Triple(r.remoteHost_, r.invitation, r.ctrlPort) apiErrorAlert("startRemoteHost", generalGetString(MR.strings.error_alert_title), r)