You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
233 lines
8.5 KiB
Swift
233 lines
8.5 KiB
Swift
import UIKit
|
|
import Flutter
|
|
import Firebase
|
|
import PushKit
|
|
import CallKit
|
|
import AVFAudio
|
|
import flutter_callkit_incoming
|
|
|
|
@main
|
|
@objc class AppDelegate: FlutterAppDelegate, PKPushRegistryDelegate, CallkitIncomingAppDelegate {
|
|
|
|
// Expose the Flutter engine for SceneDelegate to use
|
|
lazy var flutterEngine = FlutterEngine(name: "my flutter engine")
|
|
|
|
// Must be retained for the lifetime of the app, otherwise VoIP pushes stop.
|
|
private var voipRegistry: PKPushRegistry?
|
|
|
|
// Keeps the app alive briefly after a decline so Dart can notify the caller.
|
|
private var declineTask: UIBackgroundTaskIdentifier = .invalid
|
|
|
|
override func application(
|
|
_ application: UIApplication,
|
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
|
) -> Bool {
|
|
// Initialize Firebase
|
|
FirebaseApp.configure()
|
|
|
|
// Start running the Flutter engine
|
|
flutterEngine.run()
|
|
|
|
// Register plugins with the engine
|
|
GeneratedPluginRegistrant.register(with: self.flutterEngine)
|
|
|
|
|
|
// Register for VoIP pushes AFTER plugins, so flutter_callkit_incoming is ready.
|
|
let registry = PKPushRegistry(queue: .main)
|
|
registry.delegate = self
|
|
registry.desiredPushTypes = [.voIP]
|
|
voipRegistry = registry
|
|
|
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
|
}
|
|
|
|
// UIScene lifecycle support
|
|
override func application(
|
|
_ application: UIApplication,
|
|
configurationForConnecting connectingSceneSession: UISceneSession,
|
|
options: UIScene.ConnectionOptions
|
|
) -> UISceneConfiguration {
|
|
let sceneConfiguration = UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
|
|
sceneConfiguration.delegateClass = SceneDelegate.self
|
|
return sceneConfiguration
|
|
}
|
|
|
|
// MARK: - CallkitIncomingAppDelegate
|
|
// The plugin hands these actions to the app delegate; every one must be fulfilled
|
|
// (that is what the plugin does itself when the delegate does not conform).
|
|
|
|
func onAccept(_ call: Call, _ action: CXAnswerCallAction) {
|
|
action.fulfill()
|
|
}
|
|
|
|
func onDecline(_ call: Call, _ action: CXEndCallAction) {
|
|
action.fulfill()
|
|
// App launched in the background by a VoIP push (killed state): once the call is declined
|
|
// iOS may suspend the app almost immediately. Keep it alive briefly so the Dart decline
|
|
// handler (CallKitEvents in firebase_notification_manger.dart) can notify the caller.
|
|
beginDeclineBackgroundTask()
|
|
}
|
|
|
|
func onEnd(_ call: Call, _ action: CXEndCallAction) {
|
|
action.fulfill()
|
|
}
|
|
|
|
func onTimeOut(_ call: Call) {}
|
|
|
|
func didActivateAudioSession(_ audioSession: AVAudioSession) {}
|
|
|
|
func didDeactivateAudioSession(_ audioSession: AVAudioSession) {}
|
|
|
|
func providerDidReset() {}
|
|
|
|
private func beginDeclineBackgroundTask() {
|
|
endDeclineBackgroundTask()
|
|
declineTask = UIApplication.shared.beginBackgroundTask(withName: "CallDecline") { [weak self] in
|
|
self?.endDeclineBackgroundTask()
|
|
}
|
|
// Enough for the token request + SignalR connect + CallDeclinedAsync.
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 20) { [weak self] in
|
|
self?.endDeclineBackgroundTask()
|
|
}
|
|
}
|
|
|
|
private func endDeclineBackgroundTask() {
|
|
guard declineTask != .invalid else { return }
|
|
UIApplication.shared.endBackgroundTask(declineTask)
|
|
declineTask = .invalid
|
|
}
|
|
|
|
// MARK: - PushKit (VoIP)
|
|
|
|
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
|
|
guard type == .voIP else { return }
|
|
let token = pushCredentials.token.map { String(format: "%02x", $0) }.joined()
|
|
// Persisted by the plugin; read in Dart via FlutterCallkitIncoming.getDevicePushTokenVoIP().
|
|
SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP(token)
|
|
}
|
|
|
|
func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
|
|
guard type == .voIP else { return }
|
|
SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP("")
|
|
}
|
|
|
|
// iOS 13+: every VoIP push MUST be reported to CallKit before `completion` is called,
|
|
// or iOS terminates the app and eventually stops delivering VoIP pushes to it.
|
|
func pushRegistry(
|
|
_ registry: PKPushRegistry,
|
|
didReceiveIncomingPushWith payload: PKPushPayload,
|
|
for type: PKPushType,
|
|
completion: @escaping () -> Void
|
|
) {
|
|
guard type == .voIP, let plugin = SwiftFlutterCallkitIncomingPlugin.sharedInstance else {
|
|
completion()
|
|
return
|
|
}
|
|
|
|
let data = flutter_callkit_incoming.Data(args: AppDelegate.callKitArgs(from: payload.dictionaryPayload))
|
|
plugin.showCallkitIncoming(data, fromPushKit: true) {
|
|
completion()
|
|
}
|
|
}
|
|
|
|
/// Builds the same structure Dart's `CallKitParams.toJson()` produces in
|
|
/// `handleDataMessage` (firebase_notification_manger.dart), so both paths behave identically.
|
|
/// Accepts the call fields at the payload root, or nested under "data"/"extra"/"payload"
|
|
/// (as a dictionary or a JSON string).
|
|
private static func callKitArgs(from raw: [AnyHashable: Any]) -> [String: Any?] {
|
|
var p: [String: Any] = [:]
|
|
for (k, v) in raw { if let key = k as? String { p[key] = v } }
|
|
for key in ["data", "extra", "payload"] {
|
|
if let nested = p[key] as? [String: Any] {
|
|
p.merge(nested) { _, new in new }
|
|
} else if let json = p[key] as? String,
|
|
let d = json.data(using: .utf8),
|
|
let nested = (try? JSONSerialization.jsonObject(with: d)) as? [String: Any] {
|
|
p.merge(nested) { _, new in new }
|
|
}
|
|
}
|
|
|
|
func str(_ keys: String...) -> String? {
|
|
for k in keys {
|
|
if let v = p[k], !(v is NSNull) {
|
|
let s = "\(v)"
|
|
if !s.isEmpty { return s }
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
let callId = str("callId", "sessionId") ?? UUID().uuidString
|
|
// CallKit requires a real UUID; the backend id is kept in `extra.callId`.
|
|
let uuid = UUID(uuidString: callId)?.uuidString ?? UUID().uuidString
|
|
let callerId = str("callerId", "callerEmployeeNumber", "sourceUserId") ?? "unknown"
|
|
let callerName = str("callerName", "callerUserName", "userName") ?? "Unknown Caller"
|
|
let callerEmployeeNumber = str("callerEmployeeNumber") ?? callerId
|
|
let calleeEmployeeNumber = str("calleeEmployeeNumber") ?? callerId
|
|
let isVideoCall: Bool = {
|
|
if let b = p["isVideoCall"] as? Bool { return b }
|
|
if let s = p["isVideoCall"] as? String { return s.lowercased() == "true" }
|
|
return str("callType")?.lowercased() == "video"
|
|
}()
|
|
|
|
return [
|
|
"id": uuid,
|
|
"nameCaller": callerName,
|
|
"appName": "ATOMS",
|
|
"avatar": "",
|
|
"handle": callerEmployeeNumber,
|
|
"type": isVideoCall ? 1 : 0,
|
|
"duration": 30000,
|
|
"extra": [
|
|
"callId": callId,
|
|
"callerId": callerId,
|
|
"callerEmployeeNumber": callerEmployeeNumber,
|
|
"calleeEmployeeNumber": calleeEmployeeNumber,
|
|
"callerName": callerName,
|
|
"moduleId": str("moduleId", "applicationId") ?? "",
|
|
"referenceId": str("referenceId") ?? "",
|
|
"conversationId": str("conversationId") ?? "",
|
|
"isVideoCall": isVideoCall,
|
|
"timestamp": ISO8601DateFormatter().string(from: Date()),
|
|
] as [String: Any],
|
|
"missedCallNotification": [
|
|
"showNotification": true,
|
|
"isShowCallback": false,
|
|
"subtitle": "Missed call",
|
|
"callbackText": "Call back",
|
|
] as [String: Any],
|
|
"callingNotification": [
|
|
"showNotification": true,
|
|
"isShowCallback": true,
|
|
"subtitle": "Calling...",
|
|
"callbackText": "Hang Up",
|
|
] as [String: Any],
|
|
"ios": [
|
|
"iconName": "CallKitLogo",
|
|
"handleType": "generic",
|
|
"supportsVideo": true,
|
|
"maximumCallGroups": 1,
|
|
"maximumCallsPerCallGroup": 1,
|
|
// Don't let the plugin activate a playAndRecord audio session while ringing: while the app
|
|
// holds an active session, iOS routes the volume/side buttons to it instead of the CallKit
|
|
// call (no silence on press, no decline on double-press). CallKit activates audio on
|
|
// answer and flutter_webrtc configures it for the call.
|
|
"configureAudioSession": false,
|
|
"audioSessionMode": "default",
|
|
"audioSessionActive": true,
|
|
"audioSessionPreferredSampleRate": 44100.0,
|
|
"audioSessionPreferredIOBufferDuration": 0.005,
|
|
"supportsDTMF": true,
|
|
"supportsHolding": false,
|
|
"supportsGrouping": false,
|
|
"supportsUngrouping": false,
|
|
"ringtonePath": "system_ringtone_default",
|
|
] as [String: Any],
|
|
]
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|