mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
feat(healthchat): ring calls via gateway VoIP push
CallKit rejects the NSE hand-off without the filtering entitlement.
This commit is contained in:
@@ -113,7 +113,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
|
||||
userIndicatorController = UserIndicatorController()
|
||||
|
||||
elementCallService = ElementCallService()
|
||||
elementCallService = ElementCallService(appSettings: appSettings)
|
||||
|
||||
navigationRootCoordinator = NavigationRootCoordinator()
|
||||
|
||||
|
||||
@@ -30,6 +30,10 @@ nonisolated protocol CommonSettingsProtocol: AnyObject, Sendable {
|
||||
var enableOnlySignedDeviceIsolationMode: Bool { get }
|
||||
var threadsEnabled: Bool { get }
|
||||
var hideQuietNotificationAlerts: Bool { get }
|
||||
|
||||
var voipPusherAppID: String { get }
|
||||
var pushKitDeviceToken: String? { get }
|
||||
var voipPushGatewayNotifyURL: String? { get }
|
||||
}
|
||||
|
||||
nonisolated enum AppBuildType {
|
||||
@@ -244,6 +248,28 @@ final nonisolated class AppSettings: @unchecked Sendable {
|
||||
pushGatewayBaseURL.appending(path: "_matrix/push/v1/notify")
|
||||
}
|
||||
|
||||
// HealthChat: the NSE lacks the notification filtering entitlement, so it cannot hand ringing
|
||||
// calls to CallKit directly. Instead it asks the push gateway for a PushKit (VoIP) push, the
|
||||
// way the legacy element-ios app did. The app persists what the NSE needs for that request
|
||||
// here, because the NSE neither receives PushKit credentials nor runs the app settings hook.
|
||||
|
||||
/// The push gateway app ID that delivers PushKit pushes (`push_type: voip` in Sygnal).
|
||||
var voipPusherAppID: String {
|
||||
#if DEBUG
|
||||
InfoPlistReader.main.baseBundleIdentifier + ".ios.voip.dev"
|
||||
#else
|
||||
InfoPlistReader.main.baseBundleIdentifier + ".ios.voip.prod"
|
||||
#endif
|
||||
}
|
||||
|
||||
/// The device's PushKit token, base64 encoded like the APNs pushkey.
|
||||
@UserPreference
|
||||
var pushKitDeviceToken: String?
|
||||
|
||||
/// `pushGatewayNotifyEndpoint` as configured in the app, for the NSE.
|
||||
@UserPreference
|
||||
var voipPushGatewayNotifyURL: String?
|
||||
|
||||
@UserPreference(defaultValue: true)
|
||||
var enableNotifications: Bool
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
|
||||
private let callController = CXCallController()
|
||||
private let callProvider: CXProviderProtocol
|
||||
private let timeProvider: TimeProvider
|
||||
private let appSettings: AppSettings
|
||||
|
||||
private weak var clientProxy: ClientProxyProtocol? {
|
||||
didSet {
|
||||
@@ -67,10 +68,11 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
|
||||
|
||||
private var declineListenerHandle: TaskHandle?
|
||||
|
||||
init(callProvider: CXProviderProtocol? = nil, timeProvider: TimeProvider? = nil) {
|
||||
init(callProvider: CXProviderProtocol? = nil, timeProvider: TimeProvider? = nil, appSettings: AppSettings) {
|
||||
pushRegistry = PKPushRegistry(queue: nil)
|
||||
|
||||
self.timeProvider = timeProvider ?? TimeProvider(clock: ContinuousClock(), now: Date.init)
|
||||
self.appSettings = appSettings
|
||||
|
||||
if let callProvider {
|
||||
self.callProvider = callProvider
|
||||
@@ -157,7 +159,16 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
|
||||
|
||||
// MARK: - PKPushRegistryDelegate
|
||||
|
||||
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) { }
|
||||
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
|
||||
// HealthChat: stored for the NSE, which requests a VoIP push through the push gateway
|
||||
// for ringing calls as long as the notification filtering entitlement is missing.
|
||||
appSettings.pushKitDeviceToken = pushCredentials.token.base64EncodedString()
|
||||
appSettings.voipPushGatewayNotifyURL = appSettings.pushGatewayNotifyEndpoint.absoluteString
|
||||
}
|
||||
|
||||
func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
|
||||
appSettings.pushKitDeviceToken = nil
|
||||
}
|
||||
|
||||
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
|
||||
guard let roomID = payload.dictionaryPayload[ElementCallServiceNotificationKey.roomID.rawValue] as? String else {
|
||||
@@ -189,7 +200,7 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
|
||||
let callID = CallID(callKitID: UUID(), roomID: roomID, rtcNotificationID: rtcNotificationID, isVoiceCall: isVoiceCall)
|
||||
incomingCallID = callID
|
||||
|
||||
guard let expirationDate = (payload.dictionaryPayload[ElementCallServiceNotificationKey.expirationDate.rawValue] as? Date) else {
|
||||
guard let expirationDate = Self.expirationDate(from: payload.dictionaryPayload) else {
|
||||
MXLog.error("Something went wrong, missing expiration timestamp for incoming voip call: \(payload)")
|
||||
assertionFailure("Incoming voip push is missing the expiration timestamp")
|
||||
clearIncomingCallState()
|
||||
@@ -353,6 +364,19 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
|
||||
/// a call and immediately end it. `callerInfo` names the call after the room so the brief
|
||||
/// system UI and the Recents entry show the room rather than "Unknown", and the entry can be
|
||||
/// tapped to call back.
|
||||
/// CallKit hands the NSE's payload over as-is, so the expiration arrives as a `Date`. A VoIP push
|
||||
/// requested from the push gateway travels as JSON, where the same key is seconds since 1970.
|
||||
private static func expirationDate(from payload: [AnyHashable: Any]) -> Date? {
|
||||
switch payload[ElementCallServiceNotificationKey.expirationDate.rawValue] {
|
||||
case let date as Date:
|
||||
date
|
||||
case let secondsSince1970 as TimeInterval:
|
||||
Date(timeIntervalSince1970: secondsSince1970)
|
||||
default:
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
private func reportImmediatelyEndedCall(reason: CXCallEndedReason,
|
||||
callerInfo: (roomID: String, roomDisplayName: String?)? = nil,
|
||||
completion: @escaping () -> Void) {
|
||||
|
||||
Reference in New Issue
Block a user