Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
85f560e
temp
KaterinaWire Feb 25, 2026
9866454
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire Mar 2, 2026
9e61dcd
fix conversation ID
KaterinaWire Mar 2, 2026
c850412
refactoring
KaterinaWire Mar 6, 2026
2df105d
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire Mar 10, 2026
1a1aad1
add more logs
KaterinaWire Mar 11, 2026
faff95c
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire Mar 24, 2026
46eaba2
added CallKitReportingCoordinator
KaterinaWire Apr 10, 2026
a383703
temp
KaterinaWire Apr 20, 2026
7ae5965
resolve merge conflicts
KaterinaWire Jun 5, 2026
8a352c8
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire Jun 8, 2026
b373fc3
temp add resolveCallerNameSync
KaterinaWire Jun 9, 2026
fed33ea
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire Jun 9, 2026
3c74fd6
temp - user name
KaterinaWire Jun 11, 2026
d4e1689
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire Jun 11, 2026
1da1fe5
clean up
KaterinaWire Jun 11, 2026
6b907d0
remove logs
KaterinaWire Jun 11, 2026
8048931
remove logs
KaterinaWire Jun 11, 2026
915beee
remove commented code from NSEClientScope
KaterinaWire Jun 11, 2026
1ede72e
clean up NSEClientScope
KaterinaWire Jun 11, 2026
ad4ffa2
remove commented code
KaterinaWire Jun 11, 2026
c2d885d
add and support enableNSEHelper developer flag
KaterinaWire Jun 12, 2026
ef86f7b
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire Jun 12, 2026
f14a446
add documentation
KaterinaWire Jun 15, 2026
f7b5a2e
add documentation
KaterinaWire Jun 15, 2026
3ba4e26
add comments and protocol to the ProcessCallingEventsUseCase
KaterinaWire Jun 15, 2026
0abefc2
add comments
KaterinaWire Jun 15, 2026
eafb317
add comments
KaterinaWire Jun 15, 2026
0d65146
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire Jun 15, 2026
fc22a59
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire Jun 15, 2026
8248d50
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire Jun 16, 2026
135afda
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire Jun 30, 2026
59a99e2
temp
KaterinaWire Jul 2, 2026
b8230f9
update the structure
KaterinaWire Jul 9, 2026
8901ca0
resolve merge conflict
KaterinaWire Jul 9, 2026
b533a35
temp
KaterinaWire Jul 10, 2026
38a22f0
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire Jul 10, 2026
428032d
remove some logs, and ProcessCallingEventsUseCase from the sepaarte file
KaterinaWire Jul 10, 2026
19e43c3
Update WireDomain/Sources/WireDomain/Notifications/ProcessCallingEven…
KaterinaWire Jul 10, 2026
3a36213
updated documentation
KaterinaWire Jul 10, 2026
a80d75e
clean up
KaterinaWire Jul 10, 2026
2a53328
add Task.checkCancellation()
KaterinaWire Jul 10, 2026
b221a4c
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire Jul 10, 2026
04184c9
update the return result of the avsParametersForProteus method
KaterinaWire Jul 10, 2026
f119bf2
implement onMissedCall
KaterinaWire Jul 10, 2026
03328aa
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire Jul 13, 2026
cdfe3dc
add ProcessCallingEventsUseCaseTests
KaterinaWire Jul 13, 2026
0eb765d
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire Jul 13, 2026
8d7e2d3
add tests
KaterinaWire Jul 13, 2026
f7dc7d5
add logs
KaterinaWire Jul 13, 2026
ea9cc0a
use params.clientId instead of clientId
KaterinaWire Jul 13, 2026
f6f8154
clean up
KaterinaWire Jul 13, 2026
1f25518
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire Jul 13, 2026
2b16407
fix test
KaterinaWire Jul 13, 2026
e8ee5dc
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire Jul 15, 2026
b220ea9
Merge branch 'develop' into feat/avs_for_nse
KaterinaWire Jul 16, 2026
adb0ec6
resolve merge conflicts
KaterinaWire Jul 17, 2026
3fc9a82
move CallClosedReason to the separate file
KaterinaWire Jul 17, 2026
b86dcc7
remove securityDegraded call end reason from the NSE
KaterinaWire Jul 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions WireDomain/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,17 @@ let package = Package(
.package(path: "../WireNetwork"),
.package(path: "../WireFoundation"),
.package(path: "../WireLogging"),
.package(path: "../WirePlugins")
.package(path: "../WirePlugins"),
.package(path: "../WireAVS")
],
targets: [
.target(
name: "WireDomainPackage",
dependencies: [
"WireNetwork",
"WireLogging",
"WireFoundation"
"WireFoundation",
"WireAVS"
]
),
.target(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,16 @@ 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
)
if DeveloperFlag.enableNSEHelper.isOn {
return nil
} else {
return await buildCallKitNotification(
callContent: callContent,
accountID: accountID,
conversationID: resolvedConversationID,
senderID: senderID
)
}

} else if displayCallNotification {
// If not, try to return a regular call notification.
Expand Down
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 {

Copy link
Copy Markdown
Collaborator

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 AVSCallingEventService is tied to a userID & clientID, the processInstance might have a different userID & clientID from that requested. Is that a problem?

return existing
}
let instance = AVSCallingEventService(userID: userID, clientID: clientID)
processInstance = instance
Comment on lines +80 to +81

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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
)
}
}
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
}
}
}
Loading
Loading