import AVFAudio import CallKit import Flutter import PushKit import UIKit import UserNotifications import YandexLoginSDK import flutter_callkit_incoming import flutter_foreground_task @main @objc class AppDelegate: FlutterAppDelegate, PKPushRegistryDelegate, CallkitIncomingAppDelegate { private var voipRegistry: PKPushRegistry? private var voipDiagnosticsChannel: FlutterMethodChannel? private let voipDiagnosticsStorageKey = "EatvioVoipDiagnostics" private let pendingAcceptedVoipCallKey = "EatvioPendingAcceptedVoipCall" private let voipDiagnosticsChannelName = "com.eatvioapp/voip_diagnostics" private let appUrlScheme = "eatvio" override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) SwiftFlutterForegroundTaskPlugin.setPluginRegistrantCallback { registry in GeneratedPluginRegistrant.register(with: registry) } let mainQueue = DispatchQueue.main voipRegistry = PKPushRegistry(queue: mainQueue) voipRegistry?.delegate = self voipRegistry?.desiredPushTypes = [PKPushType.voIP] if #available(iOS 10.0, *) { UNUserNotificationCenter.current().delegate = self } let launched = super.application(application, didFinishLaunchingWithOptions: launchOptions) if let controller = window?.rootViewController as? FlutterViewController { let channel = FlutterMethodChannel( name: voipDiagnosticsChannelName, binaryMessenger: controller.binaryMessenger ) voipDiagnosticsChannel = channel channel.setMethodCallHandler { [weak self] call, result in guard let self else { result(nil) return } switch call.method { case "drainVoipDiagnostics": result(self.drainVoipDiagnostics()) case "drainPendingAcceptedVoipCall": result(self.drainPendingAcceptedVoipCall()) default: result(FlutterMethodNotImplemented) } } } return launched } @available(iOS 8.0, *) override func application( _ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void ) -> Bool { if YXLSdk.shared.processUserActivity(userActivity) { return true } return super.application( application, continue: userActivity, restorationHandler: restorationHandler ) } override public func application( _ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:] ) -> Bool { if url.scheme == appUrlScheme { let components = URLComponents(url: url, resolvingAgainstBaseURL: false) let queryItems = components?.queryItems ?? [] let conversationUuid = queryItems.first { $0.name == "conversationUuid" }?.value? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" let callKitId = queryItems.first { $0.name == "callKitId" }?.value? .trimmingCharacters(in: .whitespacesAndNewlines) recordVoipDiagnostic( "accepted_call_url_opened", data: [ "callKitId": callKitId ?? "", "hasCallKitId": callKitId != nil, "conversationUuid": conversationUuid, "url": url.absoluteString, ] ) if let callKitId, !callKitId.isEmpty, !conversationUuid.isEmpty { voipDiagnosticsChannel?.invokeMethod( "acceptedVoipCall", arguments: [ "id": callKitId, "handle": conversationUuid, "extra": [ "conversationUuid": conversationUuid ], ] ) } return true } return YXLSdk.shared.handleOpen( url, sourceApplication: options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String ) } private func conversationUuid(fromActionUrl actionUrl: String?) -> String? { guard let actionUrl = actionUrl, let components = URLComponents(string: actionUrl) else { return nil } let queryItems = components.queryItems ?? [] for key: String in ["uuid", "conversationUuid", "conversation_uuid"] { if let value = queryItems.first(where: { $0.name == key })?.value, !value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return value.trimmingCharacters(in: .whitespacesAndNewlines) } } let segments = components.path.split(separator: "/").map(String.init) for index in segments.indices where segments[index] == "video-call" { let nextIndex = segments.index(after: index) if segments.indices.contains(nextIndex), !segments[nextIndex].isEmpty { return segments[nextIndex] } } return nil } private func conversationUuid(fromCall call: Call) -> String? { let extra = call.data.extra as? [String: Any] ?? [:] for key in ["conversationUuid", "conversation_uuid", "conversation_id", "chat_uuid", "uuid"] { if let value = extra[key] as? String { let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines) if !trimmed.isEmpty { return trimmed } } } let handle = call.data.handle.trimmingCharacters(in: .whitespacesAndNewlines) if !handle.isEmpty { return handle } return nil } private func storePendingAcceptedCall(_ call: Call) { let payload = sanitizedDiagnosticValue(call.data.toJSON()) as? [String: Any] ?? [:] UserDefaults.standard.set(payload, forKey: pendingAcceptedVoipCallKey) } private func drainPendingAcceptedVoipCall() -> [String: Any]? { let defaults = UserDefaults.standard guard let payload = defaults.dictionary(forKey: pendingAcceptedVoipCallKey) else { return nil } defaults.removeObject(forKey: pendingAcceptedVoipCallKey) return payload } private func bringAppToForeground() { DispatchQueue.main.async { for scene in UIApplication.shared.connectedScenes { guard let windowScene = scene as? UIWindowScene else { continue } for window in windowScene.windows { window.makeKeyAndVisible() } } self.window?.makeKeyAndVisible() } } private func openAppForAcceptedCall(_ call: Call) { guard let conversationUuid = conversationUuid(fromCall: call), !conversationUuid.isEmpty else { recordVoipDiagnostic( "accepted_call_missing_conversation_uuid", data: ["callKitId": call.data.uuid] ) return } var components = URLComponents() components.scheme = appUrlScheme components.host = "video-call" components.queryItems = [ URLQueryItem(name: "callKitId", value: call.data.uuid), URLQueryItem(name: "conversationUuid", value: conversationUuid) ] guard let url = components.url else { recordVoipDiagnostic( "accepted_call_url_build_failed", data: [ "callKitId": call.data.uuid, "conversationUuid": conversationUuid, ] ) return } recordVoipDiagnostic( "accepted_call_open_app_requested", data: [ "callKitId": call.data.uuid, "conversationUuid": conversationUuid, "url": url.absoluteString, "appState": UIApplication.shared.applicationState.rawValue, ] ) bringAppToForeground() UIApplication.shared.open(url, options: [:]) { [weak self] success in self?.recordVoipDiagnostic( success ? "accepted_call_open_app_succeeded" : "accepted_call_open_app_failed", data: [ "callKitId": call.data.uuid, "conversationUuid": conversationUuid, "url": url.absoluteString, ] ) } } private func sanitizedDiagnosticValue(_ value: Any?) -> Any { guard let value = value else { return "" } if let string = value as? String { return string } if let number = value as? NSNumber { return number } if let bool = value as? Bool { return bool } if let dictionary = value as? [AnyHashable: Any] { return dictionary.reduce(into: [String: Any]()) { result, entry in result[String(describing: entry.key)] = sanitizedDiagnosticValue(entry.value) } } if let array = value as? [Any] { return array.map { sanitizedDiagnosticValue($0) } } return String(describing: value) } private func recordVoipDiagnostic(_ name: String, data: [String: Any]) { let defaults = UserDefaults.standard var events = defaults.array(forKey: voipDiagnosticsStorageKey) as? [[String: Any]] ?? [] var event: [String: Any] = [ "name": name, "timestamp": ISO8601DateFormatter().string(from: Date()), "appState": UIApplication.shared.applicationState.rawValue, ] event["data"] = sanitizedDiagnosticValue(data) events.append(event) if events.count > 20 { events = Array(events.suffix(20)) } defaults.set(events, forKey: voipDiagnosticsStorageKey) voipDiagnosticsChannel?.invokeMethod("voipDiagnosticsUpdated", arguments: nil) } private func drainVoipDiagnostics() -> [[String: Any]] { let defaults = UserDefaults.standard let events = defaults.array(forKey: voipDiagnosticsStorageKey) as? [[String: Any]] ?? [] defaults.removeObject(forKey: voipDiagnosticsStorageKey) return events } func pushRegistry( _ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType ) { let deviceToken = credentials.token.map { String(format: "%02x", $0) }.joined() SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP(deviceToken) } func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) { SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP("") } func pushRegistry( _ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void ) { guard type == .voIP else { completion() return } let dict = payload.dictionaryPayload let pushMarkers = [ dict["type"] as? String, dict["notification_type"] as? String, dict["event"] as? String, dict["action"] as? String, dict["push_type"] as? String, dict["call_type"] as? String, ] .compactMap { $0?.lowercased().replacingOccurrences(of: "-", with: "_") } if pushMarkers.contains(where: { $0.contains("video_call") }) && pushMarkers.contains(where: { $0.contains("end") || $0.contains("cancel") }) { recordVoipDiagnostic( "voip_end_or_cancel_received", data: [ "markers": pushMarkers, "payload": dict, ] ) SwiftFlutterCallkitIncomingPlugin.sharedInstance?.endAllCalls() completion() return } let nameCaller = (dict["nameCaller"] as? String) ?? (dict["counterpartName"] as? String) ?? (dict["counterpart_name"] as? String) ?? (dict["caller_name"] as? String) ?? (dict["callerName"] as? String) ?? (dict["caller"] as? String) ?? "" let conversationUuid = (dict["conversation_uuid"] as? String) ?? (dict["conversationUuid"] as? String) ?? (dict["conversation_id"] as? String) ?? (dict["chat_uuid"] as? String) ?? (dict["uuid"] as? String) ?? conversationUuid(fromActionUrl: dict["action_url"] as? String) ?? (dict["handle"] as? String) ?? "" let callKitIdSource = [ (key: "call_id", value: dict["call_id"] as? String), (key: "id", value: dict["id"] as? String), (key: "message_id", value: dict["message_id"] as? String), ] .first { item in guard let trimmed = item.value?.trimmingCharacters(in: .whitespacesAndNewlines) else { return false } return !trimmed.isEmpty && UUID(uuidString: trimmed) != nil } let id = callKitIdSource?.value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? UUID().uuidString let callKitIdSourceName = callKitIdSource?.key ?? "generated" let isVideo = (dict["isVideo"] as? Bool) ?? (dict["is_video"] as? Bool) ?? true var extra: [String: Any] = [:] if !conversationUuid.isEmpty { extra["conversationUuid"] = conversationUuid } if let avatar = dict["caller_avatar"] as? String ?? dict["avatar"] as? String ?? dict["avatarUrl"] as? String ?? dict["avatar_url"] as? String ?? dict["counterpartAvatarUrl"] as? String ?? dict["counterpart_avatar_url"] as? String { extra["counterpartAvatarUrl"] = avatar } if let messageId = dict["message_id"] as? String { extra["messageId"] = messageId } if let roomName = dict["room_name"] as? String { extra["roomName"] = roomName } if let localName = dict["localParticipantName"] as? String ?? dict["local_participant_name"] as? String { extra["localParticipantName"] = localName } if let counterpartName = dict["counterpartName"] as? String { extra["counterpartName"] = counterpartName } else if let counterpartName = dict["counterpart_name"] as? String { extra["counterpartName"] = counterpartName } else if !nameCaller.isEmpty { extra["counterpartName"] = nameCaller } let data = flutter_callkit_incoming.Data( id: id, nameCaller: nameCaller, handle: conversationUuid.isEmpty ? nameCaller : conversationUuid, type: isVideo ? 1 : 0 ) data.extra = extra as NSDictionary NSLog( "[EatvioVoIP] report incoming call id=\(id) conversation=\(conversationUuid) state=\(UIApplication.shared.applicationState.rawValue) markers=\(pushMarkers.joined(separator: ",")) payload=\(dict)" ) recordVoipDiagnostic( "voip_incoming_received", data: [ "callKitId": id, "callKitIdSource": callKitIdSourceName, "conversationUuid": conversationUuid, "nameCaller": nameCaller, "isVideo": isVideo, "markers": pushMarkers, "payload": dict, ] ) guard let callkitPlugin = SwiftFlutterCallkitIncomingPlugin.sharedInstance else { NSLog("[EatvioVoIP] CallKit plugin missing id=\(id)") recordVoipDiagnostic( "callkit_plugin_missing", data: [ "callKitId": id, "conversationUuid": conversationUuid, ] ) completion() return } callkitPlugin.showCallkitIncoming( data, fromPushKit: true ) { let activeCalls = callkitPlugin.activeCalls() let activeIds = activeCalls.compactMap { $0["id"] as? String }.joined(separator: ",") NSLog( "[EatvioVoIP] report incoming completed id=\(id) activeCallIds=\(activeIds)" ) self.recordVoipDiagnostic( "callkit_report_completed", data: [ "callKitId": id, "callKitIdSource": callKitIdSourceName, "conversationUuid": conversationUuid, "activeCallIds": activeIds, "activeCallsCount": activeCalls.count, ] ) DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { let delayedActiveCalls = callkitPlugin.activeCalls() let delayedActiveIds = delayedActiveCalls.compactMap { $0["id"] as? String }.joined(separator: ",") self.recordVoipDiagnostic( "callkit_active_calls_delayed", data: [ "callKitId": id, "callKitIdSource": callKitIdSourceName, "conversationUuid": conversationUuid, "activeCallIds": delayedActiveIds, "activeCallsCount": delayedActiveCalls.count, ] ) } completion() } } func onAccept(_ call: Call, _ action: CXAnswerCallAction) { storePendingAcceptedCall(call) recordVoipDiagnostic( "callkit_accept_native", data: [ "callKitId": call.data.uuid, "conversationUuid": conversationUuid(fromCall: call) ?? "", "data": call.data.toJSON(), ] ) voipDiagnosticsChannel?.invokeMethod("acceptedVoipCall", arguments: call.data.toJSON()) DispatchQueue.main.async { [weak self] in self?.bringAppToForeground() self?.openAppForAcceptedCall(call) } action.fulfill() } func onDecline(_ call: Call, _ action: CXEndCallAction) { action.fulfill() } func onEnd(_ call: Call, _ action: CXEndCallAction) { action.fulfill() } func onTimeOut(_ call: Call) {} func didActivateAudioSession(_ audioSession: AVAudioSession) {} func didDeactivateAudioSession(_ audioSession: AVAudioSession) {} func providerDidReset() {} }