diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 797e68db4f..6c5d5504e6 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -2182,9 +2182,11 @@ func refreshCallInvitations() async throws { } } -func justRefreshCallInvitations() throws { +func justRefreshCallInvitations() async throws { let callInvitations = try apiGetCallInvitationsSync() - ChatModel.shared.callInvitations = callsByChat(callInvitations) + await MainActor.run { + ChatModel.shared.callInvitations = callsByChat(callInvitations) + } } private func callsByChat(_ callInvitations: [RcvCallInvitation]) -> [ChatId: RcvCallInvitation] { @@ -2194,8 +2196,9 @@ private func callsByChat(_ callInvitations: [RcvCallInvitation]) -> [ChatId: Rcv } func activateCall(_ callInvitation: RcvCallInvitation) { - if !callInvitation.user.showNotifications { return } let m = ChatModel.shared + logger.debug("reportNewIncomingCall activeCallUUID \(String(describing: m.activeCall?.callUUID)) invitationUUID \(String(describing: callInvitation.callUUID))") + if !callInvitation.user.showNotifications || m.activeCall?.callUUID == callInvitation.callUUID { return } CallController.shared.reportNewIncomingCall(invitation: callInvitation) { error in if let error = error { DispatchQueue.main.async { diff --git a/apps/ios/Shared/Views/Call/CallController.swift b/apps/ios/Shared/Views/Call/CallController.swift index bfa26700e5..36887d6184 100644 --- a/apps/ios/Shared/Views/Call/CallController.swift +++ b/apps/ios/Shared/Views/Call/CallController.swift @@ -61,12 +61,30 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) { logger.debug("CallController.provider CXAnswerCallAction") - if callManager.answerIncomingCall(callUUID: action.callUUID.uuidString.lowercased()) { - // WebRTC call should be in connected state to fulfill. - // Otherwise no audio and mic working on lockscreen - fulfillOnConnect = action - } else { - action.fail() + Task { + let chatIsReady = await waitUntilChatStarted(timeoutMs: 30_000, stepMs: 500) + logger.debug("CallController chat started \(chatIsReady) \(ChatModel.shared.chatInitialized) \(ChatModel.shared.chatRunning == true) \(String(describing: AppChatState.shared.value))") + if !chatIsReady { + action.fail() + return + } + if !ChatModel.shared.callInvitations.values.contains(where: { inv in inv.callUUID == action.callUUID.uuidString.lowercased() }) { + try? await justRefreshCallInvitations() + logger.debug("CallController: updated call invitations chat") + } + await MainActor.run { + logger.debug("CallController.provider will answer on call") + + if callManager.answerIncomingCall(callUUID: action.callUUID.uuidString.lowercased()) { + logger.debug("CallController.provider answered on call") + // WebRTC call should be in connected state to fulfill. + // Otherwise no audio and mic working on lockscreen + fulfillOnConnect = action + } else { + logger.debug("CallController.provider will fail the call") + action.fail() + } + } } } @@ -156,6 +174,19 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse } } + private func waitUntilChatStarted(timeoutMs: UInt64, stepMs: UInt64) async -> Bool { + logger.debug("CallController waiting until chat started") + var t: UInt64 = 0 + repeat { + if ChatModel.shared.chatInitialized, ChatModel.shared.chatRunning == true, case .active = AppChatState.shared.value { + return true + } + _ = try? await Task.sleep(nanoseconds: stepMs * 1000000) + t += stepMs + } while t < timeoutMs + return false + } + @objc(pushRegistry:didUpdatePushCredentials:forType:) func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) { logger.debug("CallController: didUpdate push credentials for type \(type.rawValue)") @@ -171,32 +202,19 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse self.reportExpiredCall(payload: payload, completion) return } - if (!ChatModel.shared.chatInitialized) { - logger.debug("CallController: initializing chat") - do { - try initializeChat(start: true, refreshInvitations: false) - } catch let error { - logger.error("CallController: initializing chat error: \(error)") - self.reportExpiredCall(payload: payload, completion) - return - } - } - logger.debug("CallController: initialized chat") - startChatForCall() - logger.debug("CallController: started chat") - self.shouldSuspendChat = true - // There are no invitations in the model, as it was processed by NSE - try? justRefreshCallInvitations() - logger.debug("CallController: updated call invitations chat") - // logger.debug("CallController justRefreshCallInvitations: \(String(describing: m.callInvitations))") // Extract the call information from the push notification payload let m = ChatModel.shared if let contactId = payload.dictionaryPayload["contactId"] as? String, - let invitation = m.callInvitations[contactId] { - let update = self.cxCallUpdate(invitation: invitation) - if let callUUID = invitation.callUUID, let uuid = UUID(uuidString: callUUID) { + let displayName = payload.dictionaryPayload["displayName"] as? String, + let callUUID = payload.dictionaryPayload["callUUID"] as? String, + let uuid = UUID(uuidString: callUUID), + let callTsInterval = payload.dictionaryPayload["callTs"] as? TimeInterval, + let mediaStr = payload.dictionaryPayload["media"] as? String, + let media = CallMediaType(rawValue: mediaStr) { + let update = self.cxCallUpdate(contactId, displayName, media) + let callTs = Date(timeIntervalSince1970: callTsInterval) + if callTs.timeIntervalSinceNow >= -180 { logger.debug("CallController: report pushkit call via CallKit") - let update = self.cxCallUpdate(invitation: invitation) self.provider.reportNewIncomingCall(with: uuid, update: update) { error in if error != nil { m.callInvitations.removeValue(forKey: contactId) @@ -205,11 +223,31 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse completion() } } else { + logger.debug("CallController will expire call 1") self.reportExpiredCall(update: update, completion) } } else { + logger.debug("CallController will expire call 2") self.reportExpiredCall(payload: payload, completion) } + + //DispatchQueue.main.asyncAfter(deadline: .now() + 10) { + if (!ChatModel.shared.chatInitialized) { + logger.debug("CallController: initializing chat") + do { + try initializeChat(start: true, refreshInvitations: false) + } catch let error { + logger.error("CallController: initializing chat error: \(error)") + if let call = ChatModel.shared.activeCall { + self.endCall(call: call, completed: completion) + } + return + } + } + logger.debug("CallController: initialized chat") + startChatForCall() + logger.debug("CallController: started chat") + self.shouldSuspendChat = true } // This function fulfils the requirement to always report a call when PushKit notification is received, @@ -261,6 +299,14 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse return update } + private func cxCallUpdate(_ contactId: String, _ displayName: String, _ media: CallMediaType) -> CXCallUpdate { + let update = CXCallUpdate() + update.remoteHandle = CXHandle(type: .generic, value: contactId) + update.hasVideo = media == .video + update.localizedCallerName = displayName + return update + } + func reportIncomingCall(call: Call, connectedAt dateConnected: Date?) { logger.debug("CallController: reporting incoming call connected") if CallController.useCallKit() { diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index 1a2a27ba9b..81d0c9eac1 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -339,7 +339,9 @@ class NotificationService: UNNotificationServiceExtension { CXProvider.reportNewIncomingVoIPPushPayload([ "displayName": invitation.contact.displayName, "contactId": invitation.contact.id, - "media": invitation.callType.media.rawValue + "callUUID": invitation.callUUID ?? "", + "media": invitation.callType.media.rawValue, + "callTs": invitation.callTs.timeIntervalSince1970 ]) { error in logger.debug("reportNewIncomingVoIPPushPayload result: \(error)") deliver(error == nil ? nil : createCallInvitationNtf(invitation)) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 48689d1010..1a348fc93e 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -214,11 +214,11 @@ D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; }; D7F0E33929964E7E0068AF69 /* LZString in Frameworks */ = {isa = PBXBuildFile; productRef = D7F0E33829964E7E0068AF69 /* LZString */; }; E51CC1E62C62085600DB91FE /* OneHandUICard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51CC1E52C62085600DB91FE /* OneHandUICard.swift */; }; - E51ED58A2C7A26FE009F2C7C /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED5852C7A26FE009F2C7C /* libffi.a */; }; - E51ED58B2C7A26FE009F2C7C /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED5862C7A26FE009F2C7C /* libgmpxx.a */; }; - E51ED58C2C7A26FE009F2C7C /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED5872C7A26FE009F2C7C /* libgmp.a */; }; - E51ED58D2C7A26FE009F2C7C /* libHSsimplex-chat-6.0.3.0-JVz5IxfwvrHaD2mJGTgT4.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED5882C7A26FE009F2C7C /* libHSsimplex-chat-6.0.3.0-JVz5IxfwvrHaD2mJGTgT4.a */; }; - E51ED58E2C7A26FE009F2C7C /* libHSsimplex-chat-6.0.3.0-JVz5IxfwvrHaD2mJGTgT4-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED5892C7A26FE009F2C7C /* libHSsimplex-chat-6.0.3.0-JVz5IxfwvrHaD2mJGTgT4-ghc9.6.3.a */; }; + E51ED5A82C7F5F4B009F2C7C /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED5A32C7F5F4B009F2C7C /* libgmpxx.a */; }; + E51ED5A92C7F5F4B009F2C7C /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED5A42C7F5F4B009F2C7C /* libffi.a */; }; + E51ED5AA2C7F5F4B009F2C7C /* libHSsimplex-chat-6.0.3.0-7BSMDwqB9CRFek7eb5Gzw5.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED5A52C7F5F4B009F2C7C /* libHSsimplex-chat-6.0.3.0-7BSMDwqB9CRFek7eb5Gzw5.a */; }; + E51ED5AB2C7F5F4B009F2C7C /* libHSsimplex-chat-6.0.3.0-7BSMDwqB9CRFek7eb5Gzw5-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED5A62C7F5F4B009F2C7C /* libHSsimplex-chat-6.0.3.0-7BSMDwqB9CRFek7eb5Gzw5-ghc9.6.3.a */; }; + E51ED5AC2C7F5F4B009F2C7C /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED5A72C7F5F4B009F2C7C /* libgmp.a */; }; E5DCF8DB2C56FAC1007928CC /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; }; E5DCF9712C590272007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF96F2C590272007928CC /* Localizable.strings */; }; E5DCF9842C5902CE007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF9822C5902CE007928CC /* Localizable.strings */; }; @@ -550,11 +550,11 @@ 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; }; D7AA2C3429A936B400737B40 /* MediaEncryption.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = MediaEncryption.playground; path = Shared/MediaEncryption.playground; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; E51CC1E52C62085600DB91FE /* OneHandUICard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneHandUICard.swift; sourceTree = ""; }; - E51ED5852C7A26FE009F2C7C /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - E51ED5862C7A26FE009F2C7C /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - E51ED5872C7A26FE009F2C7C /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - E51ED5882C7A26FE009F2C7C /* libHSsimplex-chat-6.0.3.0-JVz5IxfwvrHaD2mJGTgT4.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.3.0-JVz5IxfwvrHaD2mJGTgT4.a"; sourceTree = ""; }; - E51ED5892C7A26FE009F2C7C /* libHSsimplex-chat-6.0.3.0-JVz5IxfwvrHaD2mJGTgT4-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.3.0-JVz5IxfwvrHaD2mJGTgT4-ghc9.6.3.a"; sourceTree = ""; }; + E51ED5A32C7F5F4B009F2C7C /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + E51ED5A42C7F5F4B009F2C7C /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + E51ED5A52C7F5F4B009F2C7C /* libHSsimplex-chat-6.0.3.0-7BSMDwqB9CRFek7eb5Gzw5.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.3.0-7BSMDwqB9CRFek7eb5Gzw5.a"; sourceTree = ""; }; + E51ED5A62C7F5F4B009F2C7C /* libHSsimplex-chat-6.0.3.0-7BSMDwqB9CRFek7eb5Gzw5-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.3.0-7BSMDwqB9CRFek7eb5Gzw5-ghc9.6.3.a"; sourceTree = ""; }; + E51ED5A72C7F5F4B009F2C7C /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; E5DCF9702C590272007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; E5DCF9722C590274007928CC /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = ""; }; E5DCF9732C590275007928CC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; @@ -645,14 +645,14 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - E51ED58D2C7A26FE009F2C7C /* libHSsimplex-chat-6.0.3.0-JVz5IxfwvrHaD2mJGTgT4.a in Frameworks */, - E51ED58E2C7A26FE009F2C7C /* libHSsimplex-chat-6.0.3.0-JVz5IxfwvrHaD2mJGTgT4-ghc9.6.3.a in Frameworks */, + E51ED5A82C7F5F4B009F2C7C /* libgmpxx.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - E51ED58B2C7A26FE009F2C7C /* libgmpxx.a in Frameworks */, + E51ED5AC2C7F5F4B009F2C7C /* libgmp.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, + E51ED5AB2C7F5F4B009F2C7C /* libHSsimplex-chat-6.0.3.0-7BSMDwqB9CRFek7eb5Gzw5-ghc9.6.3.a in Frameworks */, + E51ED5A92C7F5F4B009F2C7C /* libffi.a in Frameworks */, + E51ED5AA2C7F5F4B009F2C7C /* libHSsimplex-chat-6.0.3.0-7BSMDwqB9CRFek7eb5Gzw5.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, - E51ED58C2C7A26FE009F2C7C /* libgmp.a in Frameworks */, - E51ED58A2C7A26FE009F2C7C /* libffi.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -729,11 +729,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - E51ED5852C7A26FE009F2C7C /* libffi.a */, - E51ED5872C7A26FE009F2C7C /* libgmp.a */, - E51ED5862C7A26FE009F2C7C /* libgmpxx.a */, - E51ED5892C7A26FE009F2C7C /* libHSsimplex-chat-6.0.3.0-JVz5IxfwvrHaD2mJGTgT4-ghc9.6.3.a */, - E51ED5882C7A26FE009F2C7C /* libHSsimplex-chat-6.0.3.0-JVz5IxfwvrHaD2mJGTgT4.a */, + E51ED5A42C7F5F4B009F2C7C /* libffi.a */, + E51ED5A72C7F5F4B009F2C7C /* libgmp.a */, + E51ED5A32C7F5F4B009F2C7C /* libgmpxx.a */, + E51ED5A62C7F5F4B009F2C7C /* libHSsimplex-chat-6.0.3.0-7BSMDwqB9CRFek7eb5Gzw5-ghc9.6.3.a */, + E51ED5A52C7F5F4B009F2C7C /* libHSsimplex-chat-6.0.3.0-7BSMDwqB9CRFek7eb5Gzw5.a */, ); path = Libraries; sourceTree = "";