-
Notifications
You must be signed in to change notification settings - Fork 40
feat: NSE helper - WPB-22200 #4389
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
KaterinaWire
wants to merge
59
commits into
develop
Choose a base branch
from
feat/avs_for_nse
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
59 commits
Select commit
Hold shift + click to select a range
85f560e
temp
KaterinaWire 9866454
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire 9e61dcd
fix conversation ID
KaterinaWire c850412
refactoring
KaterinaWire 2df105d
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire 1a1aad1
add more logs
KaterinaWire faff95c
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire 46eaba2
added CallKitReportingCoordinator
KaterinaWire a383703
temp
KaterinaWire 7ae5965
resolve merge conflicts
KaterinaWire 8a352c8
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire b373fc3
temp add resolveCallerNameSync
KaterinaWire fed33ea
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire 3c74fd6
temp - user name
KaterinaWire d4e1689
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire 1da1fe5
clean up
KaterinaWire 6b907d0
remove logs
KaterinaWire 8048931
remove logs
KaterinaWire 915beee
remove commented code from NSEClientScope
KaterinaWire 1ede72e
clean up NSEClientScope
KaterinaWire ad4ffa2
remove commented code
KaterinaWire c2d885d
add and support enableNSEHelper developer flag
KaterinaWire ef86f7b
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire f14a446
add documentation
KaterinaWire f7b5a2e
add documentation
KaterinaWire 3ba4e26
add comments and protocol to the ProcessCallingEventsUseCase
KaterinaWire 0abefc2
add comments
KaterinaWire eafb317
add comments
KaterinaWire 0d65146
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire fc22a59
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire 8248d50
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire 135afda
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire 59a99e2
temp
KaterinaWire b8230f9
update the structure
KaterinaWire 8901ca0
resolve merge conflict
KaterinaWire b533a35
temp
KaterinaWire 38a22f0
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire 428032d
remove some logs, and ProcessCallingEventsUseCase from the sepaarte file
KaterinaWire 19e43c3
Update WireDomain/Sources/WireDomain/Notifications/ProcessCallingEven…
KaterinaWire 3a36213
updated documentation
KaterinaWire a80d75e
clean up
KaterinaWire 2a53328
add Task.checkCancellation()
KaterinaWire b221a4c
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire 04184c9
update the return result of the avsParametersForProteus method
KaterinaWire f119bf2
implement onMissedCall
KaterinaWire 03328aa
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire cdfe3dc
add ProcessCallingEventsUseCaseTests
KaterinaWire 0eb765d
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire 8d7e2d3
add tests
KaterinaWire f7dc7d5
add logs
KaterinaWire ea9cc0a
use params.clientId instead of clientId
KaterinaWire f6f8154
clean up
KaterinaWire 1f25518
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire 2b16407
fix test
KaterinaWire e8ee5dc
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire b220ea9
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire adb0ec6
resolve merge conflicts
KaterinaWire 3fc9a82
move CallClosedReason to the separate file
KaterinaWire b86dcc7
remove securityDegraded call end reason from the NSE
KaterinaWire File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
250 changes: 250 additions & 0 deletions
250
WireDomain/Sources/WireDomain/Notifications/Components/Calling/AVSCallingEventService.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,250 @@ | ||
| // | ||
| // 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 avs | ||
| import Foundation | ||
| import WireLogging | ||
|
|
||
| public protocol AVSCallingEventServiceProtocol: AnyObject { | ||
| var onIncomingCall: ((_ conversationId: String, _ userId: 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() | ||
| } | ||
|
|
||
| /// Bridges NSE calling-event processing to the AVS `wcall_event_*` API. | ||
| /// | ||
| /// The service owns the AVS calling-event handle created with `wcall_event_create()` | ||
| /// and reuses it for all subsequent batch-processing calls. It exposes Swift closures | ||
| /// for the AVS callback results so callers can react to incoming, missed, and closed | ||
| /// calls without dealing with C callback signatures directly. | ||
| /// | ||
| /// A typical NSE flow is: | ||
| /// - call `start()` before processing synchronized notification events | ||
| /// - call `process(...)` for each call-related event | ||
| /// - call `end()` after synchronization is complete, allowing AVS to evaluate the | ||
| /// batch and invoke the registered callbacks | ||
| public final class AVSCallingEventService: AVSCallingEventServiceProtocol { | ||
|
|
||
| // MARK: - Closure properties (set by the caller in NSEClientScope) | ||
|
|
||
| public var onIncomingCall: ((_ conversationId: String, _ userId: 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 | ||
|
|
||
| // AVS registers C-level callbacks with a handle created in init. Calling | ||
| // wcall_event_create a second time returns the same handle, so callbacks | ||
| // always fire through the first instance's contextRef. We keep one instance | ||
| // per process so the handle and contextRef are stable across NSE invocations. | ||
| private nonisolated(unsafe) static var processInstance: AVSCallingEventService? | ||
|
|
||
| public static func shared(userID: String, clientID: String) -> AVSCallingEventService { | ||
| if let existing = processInstance { | ||
| return existing | ||
| } | ||
| let instance = AVSCallingEventService(userID: userID, clientID: clientID) | ||
| processInstance = instance | ||
|
Comment on lines
+80
to
+81
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue: the instance is associated with a specific user and client id, but it's stored statically as a single instance per process. I think this would be a problem when there are multiple accounts, as one user A may obtain the instance for user B. |
||
| return instance | ||
| } | ||
|
|
||
| public init(userID: String, clientID: String) { | ||
| self.handle = 0 | ||
| wcall_set_log_handler({ _, 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 | ||
| ) | ||
| } | ||
|
|
||
| deinit { | ||
| if let ptr = contextPointer { | ||
| Unmanaged<AVSCallingEventService>.fromOpaque(ptr).release() | ||
| } | ||
| } | ||
|
|
||
| // MARK: - AVSCallingEventServiceProtocol | ||
|
|
||
| public func start() { | ||
| WireLogger.calling.debug("AVS: wcall_event_start", attributes: .newNSE, .safePublic) | ||
| 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() { | ||
| WireLogger.calling.debug("AVS: wcall_event_end (evaluating batch)", attributes: .newNSE, .safePublic) | ||
| 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<Int8>?, // conversationId | ||
| UInt32, // messageTime | ||
| UnsafePointer<Int8>?, // userId | ||
| UnsafePointer<Int8>?, // clientId | ||
| Int32, // isVideoCall (1 = true) | ||
| Int32, // shouldRing (1 = true) | ||
| Int32, // conversationType | ||
| UnsafeMutableRawPointer? // contextRef → self | ||
| ) -> Void = { conversationIdPtr, _, userIdPtr, _, isVideoCallFlag, shouldRingFlag, _, contextRef in | ||
| guard | ||
| let contextRef, | ||
| let conversationId = conversationIdPtr.flatMap({ String(cString: $0) }), | ||
| let userId = userIdPtr.flatMap({ String(cString: $0) }) | ||
| else { | ||
| WireLogger.calling.warn( | ||
| "AVS callback: incoming call dropped (missing context or IDs)", | ||
| attributes: .newNSE, .safePublic | ||
| ) | ||
| return | ||
| } | ||
|
|
||
| WireLogger.calling.info( | ||
| "AVS callback: incoming call (shouldRing: \(shouldRingFlag == 1), video: \(isVideoCallFlag == 1))", | ||
| attributes: .newNSE, .safePublic | ||
| ) | ||
| let service = Unmanaged<AVSCallingEventService>.fromOpaque(contextRef).takeUnretainedValue() | ||
| service.onIncomingCall?( | ||
| conversationId, | ||
| userId, | ||
| shouldRingFlag == 1, | ||
| isVideoCallFlag == 1 | ||
| ) | ||
| } | ||
|
|
||
| private static let missedCallHandler: @convention(c) ( | ||
| UnsafePointer<Int8>?, // conversationId | ||
| UInt32, // messageTime | ||
| UnsafePointer<Int8>?, // userId | ||
| UnsafePointer<Int8>?, // 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 { | ||
| WireLogger.calling.warn( | ||
| "AVS callback: missed call dropped (missing context or ID)", | ||
| attributes: .newNSE, .safePublic | ||
| ) | ||
| return | ||
| } | ||
|
|
||
| WireLogger.calling.info( | ||
| "AVS callback: missed call", | ||
| attributes: .newNSE, .safePublic | ||
| ) | ||
| let service = Unmanaged<AVSCallingEventService>.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<Int8>?, // conversationId | ||
| UInt32, // messageTime | ||
| UnsafePointer<Int8>?, // userId | ||
| UnsafePointer<Int8>?, // clientId | ||
| UnsafeMutableRawPointer? // contextRef → self | ||
| ) -> Void = { reasonCode, conversationIdPtr, _, _, _, contextRef in | ||
| guard | ||
| let contextRef, | ||
| let conversationId = conversationIdPtr.flatMap({ String(cString: $0) }) | ||
| else { | ||
| WireLogger.calling.warn( | ||
| "AVS callback: call closed dropped (missing context or ID)", | ||
| attributes: .newNSE, .safePublic | ||
| ) | ||
| return | ||
| } | ||
|
|
||
| let reason = CallClosedReason(wcall_reason: reasonCode) | ||
| WireLogger.calling.info( | ||
| "AVS callback: call closed (reason: \(reason))", | ||
| attributes: .newNSE, .safePublic | ||
| ) | ||
| let service = Unmanaged<AVSCallingEventService>.fromOpaque(contextRef).takeUnretainedValue() | ||
| service.onCallClosed?( | ||
| reason, | ||
| conversationId | ||
| ) | ||
| } | ||
| } | ||
57 changes: 57 additions & 0 deletions
57
WireDomain/Sources/WireDomain/Notifications/Components/Calling/CallClosedReason.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| // | ||
| // 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 avs | ||
|
|
||
| public enum CallClosedReason: Int32, Sendable { | ||
| case normal | ||
| case canceled | ||
| case answeredElsewhere | ||
| case rejectedElsewhere | ||
| case timeout | ||
| case lostMedia | ||
| case internalError | ||
| case inputOutputError | ||
| case stillOngoing | ||
| case outdatedClient | ||
| case datachannel | ||
| case timeoutECONN | ||
| case noOneJoined | ||
| case everyoneLeft | ||
| case 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 | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (maybe): The NSE process can be long lived - there isn't a new process for each notification. As
AVSCallingEventServiceis tied to a userID & clientID, the processInstance might have a different userID & clientID from that requested. Is that a problem?