mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-05 18:38:54 +00:00
* replace react-native-incall-manager with @mattermost/calls-native for audio management Removes react-native-incall-manager entirely and implements audio session lifecycle, route selection, and ringtone playback directly in calls-native on both iOS (AVAudioSession) and Android (AudioManager). Adds smart priority-based auto-routing (BT > WiredHeadset > Earpiece) with user-intent pinning. Unifies AudioDevice/AudioRoute types in calls-native as the single source of truth. * feedback review * feedback on tests * feedback review * have callkit use the ringtone selected by the user * feedback review * destroy db after test * route to wired headset on android * Android bring back vibration
115 lines
4.9 KiB
Swift
115 lines
4.9 KiB
Swift
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import CallKit
|
|
import Foundation
|
|
import Gekidou
|
|
import PushKit
|
|
|
|
/// Owns the single `PKPushRegistry` for VoIP pushes. The delegate callback
|
|
/// MUST call `reportNewIncomingCall` synchronously inside its body — iOS
|
|
/// terminates apps that don't on iOS 13+.
|
|
final class PushKitController: NSObject, PKPushRegistryDelegate {
|
|
private let registry: PKPushRegistry
|
|
private weak var bridge: CallsBridge?
|
|
|
|
init(bridge: CallsBridge) {
|
|
self.registry = PKPushRegistry(queue: .main)
|
|
self.bridge = bridge
|
|
super.init()
|
|
self.registry.delegate = self
|
|
self.registry.desiredPushTypes = [.voIP]
|
|
GekidouLogger.shared.log(.info, "PushKitController: initialized")
|
|
}
|
|
|
|
// MARK: - PKPushRegistryDelegate
|
|
|
|
func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) {
|
|
guard type == .voIP else { return }
|
|
let token = credentials.token.map { String(format: "%02x", $0) }.joined()
|
|
GekidouLogger.shared.log(.info, "PushKitController: VoIP token issued, length=\(token.count)")
|
|
bridge?.send(event: .VoIPTokenUpdated, body: ["token": token])
|
|
}
|
|
|
|
func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
|
|
guard type == .voIP else { return }
|
|
GekidouLogger.shared.log(.warning, "PushKitController: VoIP token invalidated by system")
|
|
bridge?.send(event: .VoIPTokenUpdated, body: ["token": ""])
|
|
}
|
|
|
|
func pushRegistry(_ registry: PKPushRegistry,
|
|
didReceiveIncomingPushWith payload: PKPushPayload,
|
|
for type: PKPushType,
|
|
completion: @escaping () -> Void) {
|
|
guard type == .voIP, let bridge = bridge else {
|
|
completion()
|
|
return
|
|
}
|
|
|
|
let dict = payload.dictionaryPayload
|
|
|
|
// Authenticate the push via Gekidou's VoIP-aware signature
|
|
// verification. Same JWT mechanism as the standard push path, but
|
|
// compares against the stored VoIP token (not the standard one).
|
|
guard Gekidou.PushNotification.default.verifyVoIPSignature(dict) else {
|
|
GekidouLogger.shared.log(.warning,
|
|
"PushKitController: VoIP push signature verification failed; reporting + ending")
|
|
bridge.callKitProvider.reportIncomingCall(
|
|
IncomingCallRequest(channelID: "", serverID: "",
|
|
postID: "", threadID: "",
|
|
callerID: "", callerName: "",
|
|
channelName: "", rawUserInfo: [:],
|
|
ringtoneName: nil),
|
|
pushCompletion: completion
|
|
)
|
|
return
|
|
}
|
|
|
|
let channelID = (dict["channel_id"] as? String) ?? ""
|
|
let serverID = (dict["server_id"] as? String) ?? ""
|
|
let postID = (dict["post_id"] as? String) ?? ""
|
|
let threadID = (dict["thread_id"] as? String) ?? ""
|
|
let senderID = (dict["sender_id"] as? String) ?? ""
|
|
let senderName = (dict["sender_name"] as? String) ?? ""
|
|
let channelName = (dict["channel_name"] as? String) ?? ""
|
|
|
|
GekidouLogger.shared.log(.info,
|
|
"PushKitController: VoIP push received channelID=\(channelID)")
|
|
|
|
// Resolve the user's ringtone preference synchronously from the
|
|
// per-server DB via Gekidou so CallKit plays the right sound.
|
|
let ringtoneName = Self.resolveRingtoneName(serverID: serverID)
|
|
|
|
// SYNCHRONOUS report — iOS 13+ enforces that this call happens
|
|
// before `pushRegistry:didReceiveIncomingPush:...` returns.
|
|
bridge.callKitProvider.reportIncomingCall(
|
|
IncomingCallRequest(
|
|
channelID: channelID,
|
|
serverID: serverID,
|
|
postID: postID,
|
|
threadID: threadID,
|
|
callerID: senderID,
|
|
callerName: senderName,
|
|
channelName: channelName,
|
|
rawUserInfo: dict,
|
|
ringtoneName: ringtoneName
|
|
),
|
|
pushCompletion: completion
|
|
)
|
|
}
|
|
|
|
// MARK: - Ringtone resolution
|
|
|
|
/// Resolve the user's preferred ringtone from the Gekidou DB.
|
|
/// Mirrors the JS `getRingtoneOrNone` mobile-only logic:
|
|
/// - If calls_mobile_sound is not "true" → nil (system default)
|
|
/// - Otherwise → "calls_<tone>" (e.g. "calls_calm")
|
|
/// Returns nil on any DB error so the system default is used as fallback.
|
|
private static func resolveRingtoneName(serverID: String) -> String? {
|
|
guard !serverID.isEmpty,
|
|
let serverUrl = try? Gekidou.Database.default.getServerUrlForServer(serverID)
|
|
else { return nil }
|
|
return Gekidou.Database.default.queryRingtoneForCurrentUser(serverUrl)
|
|
}
|
|
}
|