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.
cloudsolutions-atoms/ios/Runner/AppDelegate.swift

233 lines
8.5 KiB
Swift

4 years ago
import UIKit
import Flutter
4 years ago
import Firebase
2 weeks ago
import PushKit
import CallKit
import AVFAudio
2 weeks ago
import flutter_callkit_incoming
4 years ago
@main
@objc class AppDelegate: FlutterAppDelegate, PKPushRegistryDelegate, CallkitIncomingAppDelegate {
// Expose the Flutter engine for SceneDelegate to use
lazy var flutterEngine = FlutterEngine(name: "my flutter engine")
2 weeks ago
// 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
4 years ago
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Initialize Firebase
4 years ago
FirebaseApp.configure()
// Start running the Flutter engine
flutterEngine.run()
// Register plugins with the engine
GeneratedPluginRegistrant.register(with: self.flutterEngine)
2 weeks ago
// 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
4 years ago
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
}
2 weeks ago
// 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
}
2 weeks ago
// 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,
2 weeks ago
"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],
]
}
4 years ago
}