diff --git a/WireAVS/Package.swift b/WireAVS/Package.swift index ce04c2191b6..3f1c403a437 100644 --- a/WireAVS/Package.swift +++ b/WireAVS/Package.swift @@ -17,8 +17,8 @@ let package = Package( targets: [ .binaryTarget( name: "WireAVS", - url: "https://github.com/wireapp/wire-avs/releases/download/10.1.57/avs.xcframework.zip", - checksum: "2231a0582a2f7217c2a7a3d9884dbe314e1dcc04ea3ee78c3e78937f9b78b039" + url: "https://github.com/wireapp/wire-avs/releases/download/10.1.62/avs.xcframework.zip", + checksum: "6dc362a9de57ebba0cf9fb9b67bfeb3ba469d5da7e4196ccf448b39d1050b0d2" ) ] ) diff --git a/WireDomain/Package.swift b/WireDomain/Package.swift index 4ca8efb2988..55f45b09d45 100644 --- a/WireDomain/Package.swift +++ b/WireDomain/Package.swift @@ -16,7 +16,8 @@ let package = Package( .package(path: "../WireNetwork"), .package(path: "../WireFoundation"), .package(path: "../WireLogging"), - .package(path: "../WirePlugins") + .package(path: "../WirePlugins"), + .package(path: "../WireAVS") ], targets: [ .target( @@ -24,7 +25,8 @@ let package = Package( dependencies: [ "WireNetwork", "WireLogging", - "WireFoundation" + "WireFoundation", + "WireAVS" ] ), .target( diff --git a/WireDomain/Sources/WireDomain/Event Decryption/MLSMessageDecryptor.swift b/WireDomain/Sources/WireDomain/Event Decryption/MLSMessageDecryptor.swift index ceac18c1c13..eb22899f167 100644 --- a/WireDomain/Sources/WireDomain/Event Decryption/MLSMessageDecryptor.swift +++ b/WireDomain/Sources/WireDomain/Event Decryption/MLSMessageDecryptor.swift @@ -59,6 +59,7 @@ struct MLSMessageDecryptor: MLSMessageDecryptorProtocol { id: conversationID.id, domain: conversationID.domain ) else { + WireLogger.mls.error("WOW NSE MLS: conversation not found for \(conversationID.id)") throw MLSMessageDecryptorError.conversationNotFound } @@ -66,10 +67,13 @@ struct MLSMessageDecryptor: MLSMessageDecryptorProtocol { conversation: mlsConversation ) else { // MLS conversation should have a group id. + WireLogger.mls.error("WOW NSE MLS: missing MLS group ID for conversation \(conversationID.id)") throw MLSMessageDecryptorError.missingMLSGroupID } + WireLogger.mls.info("WOW NSE MLS: decrypting for groupID=\(mlsGroupID), isMLSReady=\(isMLSReady), context=\(context != nil ? "present" : "nil")") guard isMLSReady else { + WireLogger.mls.error("WOW NSE MLS: MLS conversation not ready for \(conversationID.id)") throw MLSMessageDecryptorError.mlsConversationNotReady } @@ -94,6 +98,7 @@ struct MLSMessageDecryptor: MLSMessageDecryptorProtocol { var decryptedEvent = eventData decryptedEvent.decryptedMessages = decryptedMessages + WireLogger.mls.info("WOW NSE MLS: decryptionResults count=\(decryptionResults.count) for groupID=\(mlsGroupID)") return decryptedEvent } catch let error as WireDataModel.MLSDecryptionService.MLSMessageDecryptionError { diff --git a/WireDomain/Sources/WireDomain/Notifications/Builders/Conversation/ConversationCallingEventNotificationBuilder.swift b/WireDomain/Sources/WireDomain/Notifications/Builders/Conversation/ConversationCallingEventNotificationBuilder.swift index 5546e059783..514400588d6 100644 --- a/WireDomain/Sources/WireDomain/Notifications/Builders/Conversation/ConversationCallingEventNotificationBuilder.swift +++ b/WireDomain/Sources/WireDomain/Notifications/Builders/Conversation/ConversationCallingEventNotificationBuilder.swift @@ -68,12 +68,13 @@ struct ConversationCallingEventNotificationBuilder: ConversationCallingEventNoti if displayCallKitNotification { // First, let's try to return a CallKit notification if possible. - return await buildCallKitNotification( - callContent: callContent, - accountID: accountID, - conversationID: resolvedConversationID, - senderID: senderID - ) +// return await buildCallKitNotification( +// callContent: callContent, +// accountID: accountID, +// conversationID: resolvedConversationID, +// senderID: senderID +// ) + return nil } else if displayCallNotification { // If not, try to return a regular call notification. diff --git a/WireDomain/Sources/WireDomain/Notifications/Builders/Conversation/ConversationMessageAddEventNotificationBuilder.swift b/WireDomain/Sources/WireDomain/Notifications/Builders/Conversation/ConversationMessageAddEventNotificationBuilder.swift index abfb124262e..340a68a37f0 100644 --- a/WireDomain/Sources/WireDomain/Notifications/Builders/Conversation/ConversationMessageAddEventNotificationBuilder.swift +++ b/WireDomain/Sources/WireDomain/Notifications/Builders/Conversation/ConversationMessageAddEventNotificationBuilder.swift @@ -113,7 +113,7 @@ struct ConversationMessageAddEventNotificationBuilder: ConversationMessageAddEve conversationID: content.conversationID, senderID: content.senderID ) { - callingNotification + callingNotification //nil } else { await buildMessageContentNotification( message: content.message, diff --git a/WireDomain/Sources/WireDomain/Notifications/Components/AVSCallingEventService.swift b/WireDomain/Sources/WireDomain/Notifications/Components/AVSCallingEventService.swift new file mode 100644 index 00000000000..d810a8f508b --- /dev/null +++ b/WireDomain/Sources/WireDomain/Notifications/Components/AVSCallingEventService.swift @@ -0,0 +1,223 @@ +// +// Wire +// Copyright (C) 2026 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Foundation +import avs +import WireLogging + +public enum CallClosedReason: Int32 { + case normal, canceled, answeredElsewhere, rejectedElsewhere + case timeout, lostMedia, internalError, inputOutputError + case stillOngoing, securityDegraded, outdatedClient + case datachannel, timeoutECONN, noOneJoined, everyoneLeft, unknown + + init(wcall_reason: Int32) { + switch wcall_reason { + case WCALL_REASON_NORMAL: self = .normal + case WCALL_REASON_CANCELED: self = .canceled + case WCALL_REASON_ANSWERED_ELSEWHERE: self = .answeredElsewhere + case WCALL_REASON_REJECTED: self = .rejectedElsewhere + case WCALL_REASON_TIMEOUT: self = .timeout + case WCALL_REASON_LOST_MEDIA: self = .lostMedia + case WCALL_REASON_ERROR: self = .internalError + case WCALL_REASON_IO_ERROR: self = .inputOutputError + case WCALL_REASON_STILL_ONGOING: self = .stillOngoing + case WCALL_REASON_OUTDATED_CLIENT: self = .outdatedClient + case WCALL_REASON_TIMEOUT_ECONN: self = .timeoutECONN + case WCALL_REASON_DATACHANNEL: self = .datachannel + case WCALL_REASON_NOONE_JOINED: self = .noOneJoined + case WCALL_REASON_EVERYONE_LEFT: self = .everyoneLeft + default: self = .unknown + } + } +} + +public protocol AVSCallingEventServiceProtocol: AnyObject { + + var onIncomingCall: ((_ conversationId: String, _ shouldRing: Bool, _ isVideoCall: Bool) -> Void)? { get set } + var onMissedCall: ((_ conversationId: String, _ messageTime: Date, _ isVideoCall: Bool) -> Void)? { get set } + var onCallClosed: ((_ reason: CallClosedReason, _ conversationId: String) -> Void)? { get set } + + func start() + func process(data: Data, + currentTime: UInt32, + serverTime: UInt32, + conversationId: String, + userId: String, + clientId: String, + conversationType: Int32 + ) + func end() +} + +public final class AVSCallingEventService: AVSCallingEventServiceProtocol { + + // MARK: - Closure properties (set by the caller in NSEClientScope) + + public var onIncomingCall: ((_ conversationId: String, _ shouldRing: Bool, _ isVideoCall: Bool) -> Void)? + public var onMissedCall: ((_ conversationId: String, _ messageTime: Date, _ isVideoCall: Bool) -> Void)? + public var onCallClosed: ((_ reason: CallClosedReason, _ conversationId: String) -> Void)? + + // MARK: - Private + + private var handle: UInt32 + private var contextPointer: UnsafeMutableRawPointer? + + // MARK: - Init + private static let avsInitialize: Void = { + let result = wcall_init(WCALL_ENV_DEFAULT) + if result != 0 { + WireLogger.calling.error("NSE AVS: wcall_init failed with code \(result)", attributes: .newNSE, .safePublic) + } + + }() + + public init(userID: String, clientID: String) { + //Self.avsInitialize + self.handle = 0 + wcall_set_log_handler({ level, msgPtr, _ in + guard let msg = msgPtr.flatMap({ String(cString: $0) }) else { return } + WireLogger.calling.debug(msg, attributes: .newNSE, .safePublic) + }, nil) + let retained = Unmanaged.passRetained(self) + self.contextPointer = retained.toOpaque() + self.handle = wcall_event_create( + userID, + clientID, + Self.incomingCallHandler, + Self.missedCallHandler, + Self.closedCallHandler, + contextPointer + ) + WireLogger.calling.info("WOW NSE AVS: wcall_event_create handle=\(self.handle)", attributes: .newNSE, .safePublic) + } + deinit { + if let ptr = contextPointer { + Unmanaged.fromOpaque(ptr).release() + } + WireLogger.calling.info("12345 caled DEINIT", attributes: .newNSE, .safePublic) + + } + + // MARK: - AVSCallingEventServiceProtocol + + public func start() { + wcall_event_start(handle) + } + + public func process( + data: Data, + currentTime: UInt32, + serverTime: UInt32, + conversationId: String, + userId: String, + clientId: String, + conversationType: Int32 + ) { + data.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) in + guard let bytes = ptr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { return } + wcall_event_process( + handle, + bytes, + data.count, + currentTime, + serverTime, + conversationId, + userId, + clientId, + conversationType + ) + } + } + + public func end() { + wcall_event_end(handle) + } + + // MARK: - Static C Callbacks + // + // These match the exact C function pointer signatures expected by wcall_create. + // contextRef is the Unmanaged pointer to self, set during init. + + private static let incomingCallHandler: @convention(c) ( + UnsafePointer?, // conversationId + UInt32, // messageTime + UnsafePointer?, // userId + UnsafePointer?, // clientId + Int32, // isVideoCall (1 = true) + Int32, // shouldRing (1 = true) + Int32, // conversationType + UnsafeMutableRawPointer? // contextRef → self + ) -> Void = { conversationIdPtr, _, _, _, isVideoCallFlag, shouldRingFlag, _, contextRef in + guard + let contextRef, + let conversationId = conversationIdPtr.flatMap({ String(cString: $0) }) + else { return } + + let service = Unmanaged.fromOpaque(contextRef).takeUnretainedValue() + WireLogger.mls.info("12345 \(service.onIncomingCall == nil), conversationId = \(conversationId)") + service.onIncomingCall?( + conversationId, + shouldRingFlag == 1, + isVideoCallFlag == 1 + ) + } + + private static let missedCallHandler: @convention(c) ( + UnsafePointer?, // conversationId + UInt32, // messageTime + UnsafePointer?, // userId + UnsafePointer?, // clientId + Int32, // isVideoCall (1 = true) + UnsafeMutableRawPointer? // contextRef → self + ) -> Void = { conversationIdPtr, messageTime, _, _, isVideoCallFlag, contextRef in + guard + let contextRef, + let conversationId = conversationIdPtr.flatMap({ String(cString: $0) }) + else { return } + + let service = Unmanaged.fromOpaque(contextRef).takeUnretainedValue() + // Mirror AVSWrapper: treat messageTime=0 as "now" + let nonZeroTime = messageTime != 0 ? messageTime : UInt32(Date().timeIntervalSince1970) + service.onMissedCall?( + conversationId, + Date(timeIntervalSince1970: TimeInterval(nonZeroTime)), + isVideoCallFlag == 1 + ) + } + + private static let closedCallHandler: @convention(c) ( + Int32, // reason (WCALL_REASON_*) + UnsafePointer?, // conversationId + UInt32, // messageTime + UnsafePointer?, // userId + UnsafePointer?, // clientId + UnsafeMutableRawPointer? // contextRef → self + ) -> Void = { reasonCode, conversationIdPtr, _, _, _, contextRef in + guard + let contextRef, + let conversationId = conversationIdPtr.flatMap({ String(cString: $0) }) + else { return } + + let service = Unmanaged.fromOpaque(contextRef).takeUnretainedValue() + service.onCallClosed?( + CallClosedReason(wcall_reason: reasonCode), + conversationId + ) + } +} diff --git a/WireDomain/Sources/WireDomain/Notifications/Components/CallKitReportingCoordinator.swift b/WireDomain/Sources/WireDomain/Notifications/Components/CallKitReportingCoordinator.swift new file mode 100644 index 00000000000..811813bef3a --- /dev/null +++ b/WireDomain/Sources/WireDomain/Notifications/Components/CallKitReportingCoordinator.swift @@ -0,0 +1,434 @@ +// +// Wire +// Copyright (C) 2026 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Foundation +import CallKit +import WireLogging +import WireDataModel +/// Coordinates CallKit reporting in response to AVS calling events. +/// +/// This coordinator: +/// - Sets up callbacks on AVSCallingEventService +/// - Handles incoming call and call closed events +/// - Reports to CallKit via CXProvider +/// - Manages async task lifecycle for CallKit operations +/// +/// **Lifecycle:** Owned by NSEClientScope, lives for the duration of notification processing. +/// **Thread-safety:** All methods should be called from the same actor/queue. +//final class CallKitReportingCoordinator { +// +// // MARK: - Properties +// +// private let accountID: UUID +// private var callKitReportTask: Task? +// private var didReportIncomingCall: Bool = false +// +// // MARK: - Initialization +// +// /// Creates a coordinator and sets up AVS callbacks. +// /// +// /// - Parameters: +// /// - accountID: The account identifier for CallKit content +// /// - avsService: The AVS service to observe for calling events +// /// +// /// **Important:** This initializer sets callbacks on the AVS service that +// /// capture `self` strongly. This is safe because the coordinator doesn't +// /// own the service, preventing retain cycles. +// init(accountID: UUID, avsService: any AVSCallingEventServiceProtocol) { +// self.accountID = accountID +// +// WireLogger.calling.debug( +// "CallKitReportingCoordinator: initializing for account \(accountID)", +// attributes: .newNSE, .safePublic +// ) +// +// // Set up callbacks - capturing self strongly is safe here +// // because we don't own avsService (no retain cycle) +// avsService.onIncomingCall = { [self] conversationId, shouldRing, isVideoCall in +// self.handleIncomingCall( +// conversationId: conversationId, +// shouldRing: shouldRing, +// isVideoCall: isVideoCall +// ) +// } +// +// avsService.onMissedCall = { conversationId, messageTime, isVideoCall in +// WireLogger.calling.info( +// "CallKitReportingCoordinator: onMissedCall fired", +// attributes: .newNSE, .safePublic +// ) +// // Nothing to do here — the missed call text notification +// // is already built by ConversationCallingEventNotificationBuilder +// // from the same event in the event stream. +// WireLogger.calling.info( +// "AVS: missed call in conversation \(conversationId)", +// attributes: .newNSE, .safePublic +// ) +// } +// +// avsService.onCallClosed = { [self] reason, conversationId in +// self.handleCallClosed(reason: reason, conversationId: conversationId) +// } +// } +// +// deinit { +// WireLogger.calling.debug( +// "CallKitReportingCoordinator: deallocating", +// attributes: .newNSE, .safePublic +// ) +// } +// +// // MARK: - Public API +// +// /// Waits for any pending CallKit reporting tasks to complete. +// /// +// /// Call this before finishing notification processing to ensure +// /// CallKit has been properly notified. +// /// +// /// - Returns: Completes when the CallKit task finishes (or immediately if no task) +// func waitForCompletion() async { +// WireLogger.calling.debug( +// "CallKitReportingCoordinator: waiting for completion", +// attributes: .newNSE, .safePublic +// ) +// +// await callKitReportTask?.value +// +// WireLogger.calling.debug( +// "CallKitReportingCoordinator: completion awaited", +// attributes: .newNSE, .safePublic +// ) +// } +// +// // MARK: - Private Callback Handlers +// +// /// Handles incoming call events from AVS. +// private func handleIncomingCall( +// conversationId: String, +// shouldRing: Bool, +// isVideoCall: Bool +// ) { +// WireLogger.calling.info( +// "CallKitReportingCoordinator: onIncomingCall fired, conversationId=\(conversationId), shouldRing=\(shouldRing)", +// attributes: .newNSE, .safePublic +// ) +// +// guard let qualifiedID = QualifiedID(rawValue: conversationId) else { +// WireLogger.calling.error( +// "CallKitReportingCoordinator: invalid conversation ID: \(conversationId)", +// attributes: .newNSE, .safePublic +// ) +// return +// } +// +// let callKitContent: [String: Any] = [ +// "accountID": accountID.uuidString, +// "conversationID": qualifiedID.uuid.uuidString, +// "shouldRing": shouldRing, +// "hasVideo": isVideoCall, +// "callerName": "" +// ] +// +// didReportIncomingCall = shouldRing +// +// WireLogger.calling.debug( +// "CallKitReportingCoordinator: reporting to CallKit, content: \(callKitContent)", +// attributes: .newNSE, .safePublic +// ) +// +// callKitReportTask = Task { +// await withCheckedContinuation { continuation in +// CXProvider.reportNewIncomingVoIPPushPayload(callKitContent) { error in +// if let error { +// WireLogger.calling.error( +// "CallKitReportingCoordinator: reportNewIncomingVoIPPushPayload error: \(error)", +// attributes: .newNSE, .safePublic +// ) +// } else { +// WireLogger.calling.info( +// "CallKitReportingCoordinator: reportNewIncomingVoIPPushPayload done", +// attributes: .newNSE, .safePublic +// ) +// } +// continuation.resume() +// } +// } +// } +// } +// +// /// Handles call closed events from AVS. +// private func handleCallClosed(reason: CallClosedReason, conversationId: String) { +// WireLogger.calling.info( +// "CallKitReportingCoordinator: onCallClosed fired, reason=\(reason)", +// attributes: .newNSE, .safePublic +// ) +// +// guard didReportIncomingCall else { +// WireLogger.calling.debug( +// "CallKitReportingCoordinator: no incoming call was reported, skipping CallKit update", +// attributes: .newNSE, .safePublic +// ) +// return +// } +// +// // Stop ringing — only if we previously started it +// let callKitContent: [String: Any] = [ +// "accountID": accountID.uuidString, +// "conversationID": conversationId, +// "shouldRing": false +// ] +// +// didReportIncomingCall = false +// +// WireLogger.calling.debug( +// "CallKitReportingCoordinator: stopping CallKit ring", +// attributes: .newNSE, .safePublic +// ) +// +// callKitReportTask = Task { +// do { +// try await CXProvider.reportNewIncomingVoIPPushPayload(callKitContent) +// WireLogger.calling.info( +// "CallKitReportingCoordinator: CallKit ring stopped successfully", +// attributes: .newNSE, .safePublic +// ) +// } catch { +// WireLogger.calling.error( +// "CallKitReportingCoordinator: error stopping CallKit ring: \(error)", +// attributes: .newNSE, .safePublic +// ) +// } +// } +// } +//} + +/// Coordinates CallKit reporting in response to AVS calling events. +/// Callback-based, no stream/bus abstraction. +final class CallKitReportingCoordinator { + + // MARK: - Properties + + private let accountID: UUID + private var didReportIncomingCall = false + + // Keep all callback-created async work so NSE can await it. + private let pendingTasksLock = NSLock() + private var pendingTasks: [UUID: Task] = [:] + + // Non-blocking proof that callbacks were alive. + private let callbackCountLock = NSLock() + private var _callbackCount = 0 + var callbackCount: Int { + callbackCountLock.lock() + defer { callbackCountLock.unlock() } + return _callbackCount + } + + // MARK: - Init + + init(accountID: UUID, avsService: any AVSCallingEventServiceProtocol) { + self.accountID = accountID + + WireLogger.calling.debug( + "CallKitReportingCoordinator: initializing for account \(accountID)", + attributes: .newNSE, .safePublic + ) + + avsService.onIncomingCall = { [self] conversationId, shouldRing, isVideoCall in + markCallbackReceived() + handleIncomingCall( + conversationId: conversationId, + shouldRing: shouldRing, + isVideoCall: isVideoCall + ) + } + + avsService.onMissedCall = { [self] conversationId, _, _ in + markCallbackReceived() + WireLogger.calling.info( + "CallKitReportingCoordinator: onMissedCall fired, conversationId=\(conversationId)", + attributes: .newNSE, .safePublic + ) + } + + avsService.onCallClosed = { [self] reason, conversationId in + markCallbackReceived() + handleCallClosed(reason: reason, conversationId: conversationId) + } + } + + deinit { + WireLogger.calling.debug( + "CallKitReportingCoordinator: deallocating (callbackCount=\(callbackCount))", + attributes: .newNSE, .safePublic + ) + } + + // MARK: - Public API + + /// Wait for all callback-started CallKit tasks to finish. + /// Does not wait for "future callbacks", only current outstanding tasks. + func waitForCompletion() async { + WireLogger.calling.debug( + "CallKitReportingCoordinator: waiting for completion (callbackCount=\(callbackCount))", + attributes: .newNSE, .safePublic + ) + + while true { + let snapshot: [Task] = { + pendingTasksLock.lock() + defer { pendingTasksLock.unlock() } + return Array(pendingTasks.values) + }() + + guard !snapshot.isEmpty else { break } + + for task in snapshot { + await task.value + } + } + + WireLogger.calling.debug( + "CallKitReportingCoordinator: completion awaited", + attributes: .newNSE, .safePublic + ) + } + + // MARK: - Private Helpers + + private func markCallbackReceived() { + callbackCountLock.lock() + _callbackCount += 1 + callbackCountLock.unlock() + } + + private func registerPendingTask(_ task: Task) { + let id = UUID() + + pendingTasksLock.lock() + pendingTasks[id] = task + pendingTasksLock.unlock() + + Task { [weak self] in + await task.value + guard let self else { return } + self.pendingTasksLock.lock() + self.pendingTasks.removeValue(forKey: id) + self.pendingTasksLock.unlock() + } + } + + // MARK: - Callback Handlers + + private func handleIncomingCall( + conversationId: String, + shouldRing: Bool, + isVideoCall: Bool + ) { + WireLogger.calling.info( + "CallKitReportingCoordinator: onIncomingCall fired, conversationId=\(conversationId), shouldRing=\(shouldRing)", + attributes: .newNSE, .safePublic + ) + + guard let qualifiedID = QualifiedID(rawValue: conversationId) else { + WireLogger.calling.error( + "CallKitReportingCoordinator: invalid conversation ID: \(conversationId)", + attributes: .newNSE, .safePublic + ) + return + } + + let callKitContent: [String: Any] = [ + "accountID": accountID.uuidString, + "conversationID": qualifiedID.uuid.uuidString, + "shouldRing": shouldRing, + "hasVideo": isVideoCall, + "callerName": "" + ] + + didReportIncomingCall = shouldRing + + let task = Task { + await withCheckedContinuation { continuation in + CXProvider.reportNewIncomingVoIPPushPayload(callKitContent) { error in + if let error { + WireLogger.calling.error( + "CallKitReportingCoordinator: report incoming failed: \(error)", + attributes: .newNSE, .safePublic + ) + } else { + WireLogger.calling.info( + "CallKitReportingCoordinator: report incoming done", + attributes: .newNSE, .safePublic + ) + } + continuation.resume() + } + } + } + + registerPendingTask(task) + } + + private func handleCallClosed(reason: CallClosedReason, conversationId: String) { + WireLogger.calling.info( + "CallKitReportingCoordinator: onCallClosed fired, reason=\(reason)", + attributes: .newNSE, .safePublic + ) + guard let qualifiedID = QualifiedID(rawValue: conversationId) else { + WireLogger.calling.error( + "CallKitReportingCoordinator: invalid conversation ID: \(conversationId)", + attributes: .newNSE, .safePublic + ) + return + } + guard didReportIncomingCall else { + WireLogger.calling.debug( + "CallKitReportingCoordinator: skipping close report (no incoming reported)", + attributes: .newNSE, .safePublic + ) + return + } + + // Keep previous behavior: use raw conversationId on close. + let callKitContent: [String: Any] = [ + "accountID": accountID.uuidString, + "conversationID": qualifiedID.uuid.uuidString, + "shouldRing": false + ] + + didReportIncomingCall = false + + let task = Task { + do { + try await CXProvider.reportNewIncomingVoIPPushPayload(callKitContent) + WireLogger.calling.info( + "CallKitReportingCoordinator: stop ring done", + attributes: .newNSE, .safePublic + ) + } catch { + WireLogger.calling.error( + "CallKitReportingCoordinator: stop ring failed: \(error)", + attributes: .newNSE, .safePublic + ) + } + } + + registerPendingTask(task) + } +} diff --git a/WireDomain/Sources/WireDomain/Notifications/Components/NSEClientScope.swift b/WireDomain/Sources/WireDomain/Notifications/Components/NSEClientScope.swift index 0690f2a5046..8d47a0c281c 100644 --- a/WireDomain/Sources/WireDomain/Notifications/Components/NSEClientScope.swift +++ b/WireDomain/Sources/WireDomain/Notifications/Components/NSEClientScope.swift @@ -19,8 +19,10 @@ import Foundation import NeedleFoundation import WireDataModel +//import GenericMessageProtocol import WireLogging import WireNetwork +//import CallKit protocol NSEClientScopeDependency: Dependency { @@ -60,6 +62,117 @@ final class NSEClientScope: Component { private let pushChannelCoordinator: AppExtensionPushChannelCoordinator private var currentTask: Task? private var monitoringTask: Task? +// private lazy var processCallingEventsUseCase: ProcessCallingEventsUseCase = ProcessCallingEventsUseCase( +// callingService: callingService, +// clientID: clientID, +// conversationLocalStore: conversationLocalStore, +// isFederationEnabled: isFederationEnabled, +// accountID: dependency.accountID +// ) + private var processCallingEventsUseCase: ProcessCallingEventsUseCase { + shared { + ProcessCallingEventsUseCase( + callingService: callingService, + clientID: clientID, + conversationLocalStore: conversationLocalStore, + isFederationEnabled: isFederationEnabled, + accountID: dependency.accountID + ) + } + } + private lazy var callKitReportingCoordinator: CallKitReportingCoordinator = { + WireLogger.calling.debug( + "NSEClientScope: creating CallKitReportingCoordinator", + attributes: .newNSE, .safePublic + ) + return CallKitReportingCoordinator( + accountID: dependency.accountID, + avsService: callingService + ) + }() + + //private var callKitReportTask: Task? + +// private lazy var callingService: any AVSCallingEventServiceProtocol = { +// let service = AVSCallingEventService( +// userID: dependency.accountID.transportString(), +// clientID: clientID +// ) +// +// // Track whether we reported an incoming call to CallKit, +// // so onCallClosed knows whether it needs to stop ringing. +// var didReportIncomingCall = false +// +// service.onIncomingCall = { [weak self] conversationId, shouldRing, isVideoCall in +// WireLogger.calling.info( +// "gagaga onIncomingCall, conversationId: \(conversationId)", +// attributes: .newNSE, .safePublic +// ) +// +// guard let self, +// let qualifiedID = QualifiedID(rawValue: conversationId) else { +// return +// } +// let callKitContent: [String: Any] = [ +// "accountID": self.dependency.accountID.uuidString, +// "conversationID": qualifiedID.uuid.uuidString, +// "shouldRing": shouldRing, +// "hasVideo": isVideoCall, +// "callerName": "" +// ] +// WireLogger.calling.info( +// "gagaga onIncomingCall, callKitContent \(callKitContent)", +// attributes: .newNSE, .safePublic +// ) +// didReportIncomingCall = shouldRing +// +// self.callKitReportTask = Task { +// await withCheckedContinuation { continuation in +// CXProvider.reportNewIncomingVoIPPushPayload(callKitContent) { error in +// if let error { +// WireLogger.calling.error("gagaga reportNewIncomingVoIPPushPayload error: \(error)", attributes: .newNSE, .safePublic) +// } else { +// WireLogger.calling.info("gagaga reportNewIncomingVoIPPushPayload done", attributes: .newNSE, .safePublic) +// } +// } +// } +// } +// } +// +// service.onMissedCall = { conversationId, messageTime, isVideoCall in +// WireLogger.calling.info( +// "gagaga onMissedCall", +// attributes: .newNSE, .safePublic +// ) +// // Nothing to do here — the missed call text notification +// // is already built by ConversationCallingEventNotificationBuilder +// // from the same event in the event stream. +// WireLogger.calling.info( +// "AVS: missed call in conversation \(conversationId)", +// attributes: .newNSE, .safePublic +// ) +// } +// +// service.onCallClosed = { [weak self] reason, conversationId in +// WireLogger.calling.info( +// "gagaga onCallClosed", +// attributes: .newNSE, .safePublic +// ) +// guard let self, didReportIncomingCall else { return } +// // Stop ringing — only if we previously started it +// let callKitContent: [String: Any] = [ +// "accountID": self.dependency.accountID.uuidString, +// "conversationID": conversationId, +// "shouldRing": false +// ] +// didReportIncomingCall = false +// Task { +// try? await CXProvider.reportNewIncomingVoIPPushPayload(callKitContent) +// } +// } +// +// return service +// }() init( parent: any Scope, @@ -84,18 +197,20 @@ final class NSEClientScope: Component { super.init(parent: parent) } - + func processPayload( eventID: UUID, contentHandler: @escaping (UNNotificationContent) -> Void ) async throws { // Pull pending update events. - let eventStream: AsyncStream<[UpdateEvent]> +// let eventStream: AsyncStream<[UpdateEvent]> let publicKeys = try earService.fetchPublicKeys() + // 1. Pull pending update events. + let upstream: AsyncStream<[UpdateEvent]> if dependency.journal[.isConsumableNotificationsEnabled] { let (useCase, stream) = syncEventsUseCase() - eventStream = stream + upstream = stream // because we might be interrupted when in background, we wrap the sync in an expiringActivity that will // cancel the task (not keeping any file lock in suspend mode) @@ -148,24 +263,114 @@ final class NSEClientScope: Component { } } else { - eventStream = try await pullEventsUseCase.invoke(publicKeys: publicKeys) + upstream = try await pullEventsUseCase.invoke(publicKeys: publicKeys) + } + // 2. Buffer all events (stream is finite in NSE). + var eventBatches: [[UpdateEvent]] = [] + for await batch in upstream { + eventBatches.append(batch) + } + WireLogger.calling.info( + "WOW NSE: buffered \(eventBatches.count) batches, total events: \(eventBatches.flatMap { $0 }.count)", + attributes: .newNSE, .safePublic + ) + let isAvsReady = dependency.sharedUserDefaults.bool(forKey: "isAVSReady") + let isCallKitAvailable = dependency.sharedUserDefaults.bool(forKey: "isCallKitAvailable") + WireLogger.calling.info("WOW NSE state: isAvsReady=\(isAvsReady), isCallKitAvailable=\(isCallKitAvailable)", attributes: .newNSE, .safePublic) + + // 3. Process calling events via AVS — CallKit is reported internally via callingService callbacks. + _ = callKitReportingCoordinator + await processCallingEventsUseCase.invoke(eventBatches: eventBatches, callKitReportingCoordinator: callKitReportingCoordinator) + //await callKitReportingCoordinator.waitForCompletion() + + // 4. Generate notifications from events. + let eventStream = AsyncStream<[UpdateEvent]> { continuation in + for batch in eventBatches { continuation.yield(batch) } + continuation.finish() } - - // Generate notifications from events. let notifications = try await generateNotificationsUseCase( eventID: eventID ).invoke( updateEvents: eventStream ) - // Show notifications. + // 5. Show notifications. try await showNotificationsUseCase( contentHandler: contentHandler ).invoke( userNotifications: notifications ) +// _ = await callKitReportTask?.value + } + +// private func teeCallingEvents( +// from upstream: AsyncStream<[UpdateEvent]> +// ) -> AsyncStream<[UpdateEvent]> { +// AsyncStream<[UpdateEvent]> { continuation in +// // Ensure end() even if downstream cancels early +// continuation.onTermination = { _ in +// self.callingService.end() +// } +// +// // Start once, right before consumption begins +// callingService.start() +// +// Task { [weak self] in +// guard let self else { +// continuation.finish() +// return +// } +// +// for await batch in upstream { +// if Task.isCancelled { break } +// +// // Only process calling events +// for event in batch { +// if let param = await self.avsParameters(from: event) { +// callingService.process( +// data: param.data, +// currentTime: param.currentTime, +// serverTime: param.serverTime, +// conversationId: param.conversationId, +// userId: param.userId, +// clientId: clientID, +// conversationType: param.conversationType +// ) +// } +// } +// +// // Forward batch to downstream consumer (normal notifications) +// continuation.yield(batch) +// } +// +// continuation.finish() +// callingService.end() +// } +// } +// } +// + + // MARK: - Calling + + private var callingService: any AVSCallingEventServiceProtocol { + shared { + AVSCallingEventService( + userID: dependency.accountID.transportString(), + clientID: clientID + ) + } } +// private func processCallingEventsUseCase() -> ProcessCallingEventsUseCase { +// ProcessCallingEventsUseCase( +// callingService: callingService, +// clientID: clientID, +// conversationLocalStore: conversationLocalStore, +// isFederationEnabled: isFederationEnabled, +// accountID: dependency.accountID +// ) +// } + // MARK: - Pull events consumable notifications private func syncEventsUseCase() -> (SyncEventsUseCase, AsyncStream<[UpdateEvent]>) { @@ -662,3 +867,13 @@ final class NSEClientScope: Component { } } + +private struct AVSCallParams { + let data: Data + let currentTime: UInt32 + let serverTime: UInt32 + let conversationId: String + let userId: String + let clientId: String + let conversationType: Int32 + } diff --git a/WireDomain/Sources/WireDomain/Notifications/NotificationServiceExtension.swift b/WireDomain/Sources/WireDomain/Notifications/NotificationServiceExtension.swift index 2c70ec9bf1b..e22c75661fc 100644 --- a/WireDomain/Sources/WireDomain/Notifications/NotificationServiceExtension.swift +++ b/WireDomain/Sources/WireDomain/Notifications/NotificationServiceExtension.swift @@ -72,6 +72,7 @@ public final class NotificationServiceExtension: NotificationServiceProtocol { withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { + logger.info("🤡🤡", attributes: .newNSE, .safePublic) if onGoingTask != nil { logger.warn( "onGoingtask not null: a notification is already being processed", diff --git a/WireDomain/Sources/WireDomain/Notifications/ProcessCallingEventsUseCase.swift b/WireDomain/Sources/WireDomain/Notifications/ProcessCallingEventsUseCase.swift new file mode 100644 index 00000000000..4eb2c771521 --- /dev/null +++ b/WireDomain/Sources/WireDomain/Notifications/ProcessCallingEventsUseCase.swift @@ -0,0 +1,319 @@ +// +// Wire +// Copyright (C) 2026 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Foundation +import WireNetwork +import GenericMessageProtocol +import WireLogging +import CallKit +import WireDataModel + +final class ProcessCallingEventsUseCase { + + private let callingService: any AVSCallingEventServiceProtocol + private let clientID: String + private let conversationLocalStore: any ConversationLocalStoreProtocol + private let isFederationEnabled: Bool +// private var callKitReportTask: Task? + + public init( + callingService: any AVSCallingEventServiceProtocol, + clientID: String, + conversationLocalStore: any ConversationLocalStoreProtocol, isFederationEnabled: Bool, + accountID: UUID + ) { + self.callingService = callingService + self.clientID = clientID + self.conversationLocalStore = conversationLocalStore + self.isFederationEnabled = isFederationEnabled + +// var didReportIncomingCall = false + +// callingService.onIncomingCall = { conversationId, shouldRing, isVideoCall in +// WireLogger.calling.info("WOW NSE calling: onIncomingCall fired, conversationId=\(conversationId), shouldRing=\(shouldRing)", attributes: .newNSE, .safePublic) +// +// guard let qualifiedID = QualifiedID(rawValue: conversationId) else { return } +// +// let callKitContent: [String: Any] = [ +// "accountID": accountID.uuidString, +// "conversationID": qualifiedID.uuid.uuidString, +// "shouldRing": shouldRing, +// "hasVideo": isVideoCall, +// "callerName": "" +// ] +// +// didReportIncomingCall = shouldRing +// WireLogger.calling.error("12345 \(self == nil)", attributes: .newNSE, .safePublic) +// +// self.callKitReportTask = Task { +// await withCheckedContinuation { continuation in +// CXProvider.reportNewIncomingVoIPPushPayload(callKitContent) { error in +// if let error { +// WireLogger.calling.error( +// "reportNewIncomingVoIPPushPayload error: \(error)", +// attributes: .newNSE, .safePublic +// ) +// } else { +// WireLogger.calling.info( +// "reportNewIncomingVoIPPushPayload done", +// attributes: .newNSE, .safePublic +// ) +// } +// continuation.resume() +// } +// } +// } +// } +// +// callingService.onMissedCall = { conversationId, messageTime, isVideoCall in +// WireLogger.calling.info("WOW NSE calling: onMissedCall fired", attributes: .newNSE, .safePublic) +// // Nothing to do here — the missed call text notification +// // is already built by ConversationCallingEventNotificationBuilder +// // from the same event in the event stream. +// WireLogger.calling.info( +// "AVS: missed call in conversation \(conversationId)", +// attributes: .newNSE, .safePublic +// ) +// } + +// callingService.onCallClosed = { reason, conversationId in +// WireLogger.calling.info("WOW NSE calling: onCallClosed fired, reason=\(reason)", attributes: .newNSE, .safePublic) +// guard didReportIncomingCall else { return } +// +// let callKitContent: [String: Any] = [ +// "accountID": accountID.uuidString, +// "conversationID": conversationId, +// "shouldRing": false +// ] +// +// didReportIncomingCall = false +// +// self.callKitReportTask = Task { +// try? await CXProvider.reportNewIncomingVoIPPushPayload(callKitContent) +// } +// } + + } + +// func invoke(eventBatches: [[UpdateEvent]]) async { +// WireLogger.calling.info("WOW NSE calling: invoke started, batches=\(eventBatches.count)", attributes: .newNSE, .safePublic) +// callingService.start() +// WireLogger.calling.info("WOW NSE calling: start called", attributes: .newNSE, .safePublic) +// for batch in eventBatches { +// for event in batch { +// WireLogger.calling.info("WOW NSE calling: will process event \(event)", attributes: .newNSE, .safePublic) +// if let params = await avsParameters(from: event) { +// WireLogger.calling.info("WOW NSE calling: found AVS params,currentTime = \(params.currentTime), serverTime = \(params.serverTime), conversationId = \(params.conversationId), userId = \(params.userId), clientId = \(clientID), conversationType = \(params.conversationType)", attributes: .newNSE, .safePublic) +// callingService.process( +// data: params.data, +// currentTime: params.currentTime, +// serverTime: params.serverTime, +// conversationId: params.conversationId, +// userId: params.userId, +// clientId: clientID, +// conversationType: params.conversationType +// ) +// WireLogger.calling.info("WOW NSE calling: did process event \(event)", attributes: .newNSE, .safePublic) +// +// } +// } +// } +// WireLogger.calling.info("WOW NSE calling: about to call end()", attributes: .newNSE, .safePublic) +// callingService.end() +// WireLogger.calling.info("WOW NSE calling: did call end()", attributes: .newNSE, .safePublic) +// // Wait for CXProvider report to complete before returning — +// // without this the NSE may terminate before the report fires. +//// _ = await callKitReportTask?.value +// WireLogger.calling.info("WOW NSE calling: callKitReportTask awaited", attributes: .newNSE, .safePublic) +// } + + func invoke( + eventBatches: [[UpdateEvent]], + callKitReportingCoordinator: CallKitReportingCoordinator + ) async { + callingService.start() + + for batch in eventBatches { + for event in batch { + if let params = await avsParameters(from: event) { + callingService.process( + data: params.data, + currentTime: params.currentTime, + serverTime: params.serverTime, + conversationId: params.conversationId, + userId: params.userId, + clientId: clientID, + conversationType: params.conversationType + ) + } + } + } + + callingService.end() + + await callKitReportingCoordinator.waitForCompletion() + } + + + private func avsParameters(from event: UpdateEvent) async -> AVSCallParams? { + switch event { + case .conversation(.proteusMessageAdd(let e)): + WireLogger.calling.info( + "WOW this is proteus conversation", + attributes: .newNSE, .safePublic + ) + return await avsParametersForProteus(e).first + case .conversation(.mlsMessageAdd(let e)): + WireLogger.calling.info( + "WOW this is MLS conversation", + attributes: .newNSE, .safePublic + ) + return await avsParametersForMLS(e).first + default: + return nil + } + } + + private func avsParametersForProteus( + _ event: ConversationProteusMessageAddEvent + ) async -> [AVSCallParams] { + guard + let decryptedBase64 = event.message.decryptedMessage, + let payload = Data(base64Encoded: decryptedBase64), + let genericMessage = GenericMessage(from: payload, validate: false), + genericMessage.hasCalling, + let callingData = genericMessage.calling.content.data(using: .utf8) + else { return [] } + + let params = await buildParams( + callingData: callingData, + callingProto: genericMessage.calling, + fallbackConversationID: event.conversationID, + senderID: event.senderID, + senderClientID: event.messageSenderClientID, + timestamp: event.timestamp, + isMLS: false + ) + return params.map { [$0] } ?? [] + } + + private func avsParametersForMLS( + _ event: ConversationMLSMessageAddEvent + ) async -> [AVSCallParams] { + var result: [AVSCallParams] = [] + + for decryptedMessage in event.decryptedMessages { + WireLogger.calling.info( + "WOW decryptedMessage is not nil", + attributes: .newNSE, .safePublic + ) + guard + let payload = Data(base64Encoded: decryptedMessage.message), + let genericMessage = GenericMessage(from: payload, validate: false), + genericMessage.hasCalling, + let callingData = genericMessage.calling.content.data(using: .utf8, allowLossyConversion: false), + let clientID = decryptedMessage.senderClientID, + let timestamp = event.timestamp + else { continue } + WireLogger.calling.info( + "WOW hasCalling is true", + attributes: .newNSE, .safePublic + ) + WireLogger.calling.info( + "WOW callingContent: \(genericMessage.calling.content)", + attributes: .newNSE, .safePublic + ) + if let params = await buildParams( + callingData: callingData, + callingProto: genericMessage.calling, + fallbackConversationID: event.conversationID, + senderID: event.senderID, + senderClientID: clientID, + timestamp: timestamp, + isMLS: true + ) { + result.append(params) + } + } + + return result + } + + private func buildParams( + callingData: Data, + callingProto: Calling, + fallbackConversationID: ConversationID, + senderID: UserID, + senderClientID: String, + timestamp: Date, + isMLS: Bool + ) async -> AVSCallParams? { + // Prefer conversation ID embedded in the calling proto (mirrors WireCallCenterV3) + let callingConvID = callingProto.qualifiedConversationID + let conversationUUID: UUID + let conversationDomain: String? + if !callingConvID.id.isEmpty, let uuid = UUID(uuidString: callingConvID.id) { + conversationUUID = uuid + conversationDomain = callingConvID.domain.isEmpty ? nil : callingConvID.domain + } else { + conversationUUID = fallbackConversationID.id + conversationDomain = fallbackConversationID.domain + } + + func serialize(id: UUID, domain: String?) -> String { + if isFederationEnabled, let domain { return "\(id.transportString())@\(domain)" } + return id.transportString() + } + + let conversation = await conversationLocalStore.fetchOrCreateConversation( + id: conversationUUID, + domain: conversationDomain + ) + let isGroup = await conversationLocalStore.isGroupConversation(conversation) + + // WCALL_CONV_TYPE: 0 = oneToOne, 1 = group (Proteus), 3 = conference_mls + let conversationType: Int32 = if !isGroup { + 0 // WCALL_CONV_TYPE_ONEONONE + } else if isMLS { + 3 // WCALL_CONV_TYPE_CONFERENCE_MLS + } else { + 1 // WCALL_CONV_TYPE_GROUP + } + + return AVSCallParams( + data: callingData, + currentTime: UInt32(Date.now.timeIntervalSince1970), + serverTime: UInt32(timestamp.timeIntervalSince1970), + conversationId: serialize(id: conversationUUID, domain: conversationDomain), + userId: serialize(id: senderID.id, domain: senderID.domain), + clientId: senderClientID, + conversationType: conversationType + ) + } + +} + +private struct AVSCallParams { + let data: Data + let currentTime: UInt32 + let serverTime: UInt32 + let conversationId: String + let userId: String + let clientId: String + let conversationType: Int32 + } diff --git a/WireDomain/Sources/WireDomain/Notifications/ShowNotificationUseCase.swift b/WireDomain/Sources/WireDomain/Notifications/ShowNotificationUseCase.swift index b82341580e3..053fd353f5a 100644 --- a/WireDomain/Sources/WireDomain/Notifications/ShowNotificationUseCase.swift +++ b/WireDomain/Sources/WireDomain/Notifications/ShowNotificationUseCase.swift @@ -67,7 +67,11 @@ struct ShowNotificationUseCase: ShowNotificationUseCaseProtocol { attributes: .newNSE, .safePublic ) - try await CXProvider.reportNewIncomingVoIPPushPayload(callKitContent) + WireLogger.calling.info( + "WOW !!! callKitContent \(callKitContent)", + attributes: .newNSE, .safePublic + ) + // try await CXProvider.reportNewIncomingVoIPPushPayload(callKitContent) } catch { WireLogger.calling.error( "failed to wake up main app: \(String(describing: error))", diff --git a/wire-ios-sync-engine/Source/Notifications/VoIPPushManager.swift b/wire-ios-sync-engine/Source/Notifications/VoIPPushManager.swift index b41644427fb..2870ba94e4c 100644 --- a/wire-ios-sync-engine/Source/Notifications/VoIPPushManager.swift +++ b/wire-ios-sync-engine/Source/Notifications/VoIPPushManager.swift @@ -97,7 +97,10 @@ public final class VoIPPushManager: NSObject, PKPushRegistryDelegate { completion: @escaping () -> Void ) { Self.logger.debug("did receive incoming push: \(payload.safeForLoggingDescription)") - + WireLogger.calling.info( + "WOW did receive incoming push", + attributes: .newNSE, .safePublic + ) // We're only interested in voIP tokens. guard type == .voIP else { return completion() } @@ -111,20 +114,28 @@ public final class VoIPPushManager: NSObject, PKPushRegistryDelegate { payload: [AnyHashable: Any], completion: @escaping () -> Void ) { + WireLogger.calling.info( + "WOW did receive incoming push 1 payload: \(payload)", + attributes: .newNSE, .safePublic + ) guard let accountIDString = payload["accountID"] as? String, let accountID = UUID(uuidString: accountIDString), let conversationIDString = payload["conversationID"] as? String, let conversationID = UUID(uuidString: conversationIDString), let shouldRing = payload["shouldRing"] as? Bool, - let callerName = payload["callerName"] as? String, +// let callerName = payload["callerName"] as? String, let hasVideo = payload["hasVideo"] as? Bool else { Self.logger.critical("error: processing NSE push: invalid payload - \(payload)") completion() return } - + WireLogger.calling.info( + "did receive incoming push 2", + attributes: .newNSE, .safePublic + ) + let callerName = "Test name" let handle = CallHandle( accountID: accountID, conversationID: conversationID diff --git a/wire-ios-sync-engine/Source/SessionManager/SessionManager+CallKitManagerDelegate.swift b/wire-ios-sync-engine/Source/SessionManager/SessionManager+CallKitManagerDelegate.swift index 48419ba32a1..211a39e2a6d 100644 --- a/wire-ios-sync-engine/Source/SessionManager/SessionManager+CallKitManagerDelegate.swift +++ b/wire-ios-sync-engine/Source/SessionManager/SessionManager+CallKitManagerDelegate.swift @@ -63,7 +63,7 @@ extension SessionManager: CallKitManagerDelegate { let userSession = try await withSession(for: account) let conversation = try await fetchConversation(id: handle.conversationID, account: account) await requestCallConfigIfNeeded(for: userSession) - await userSession.processPendingCallEvents(only: false) + await userSession.processPendingCallEvents(only: false) // WireLogger.calling.info("did process call events, returning conversation...") completionHandler(.success(conversation))