AppDelegate.swift 11.4 KB
//
//  AppDelegate.swift
//  Runner
//
//  Created by 权海 on 2026/6/11.
//

import Foundation
import UIKit
import Flutter
import UserNotifications
import AppTrackingTransparency
import AdSupport
import GoogleSignIn

typealias FlutterBridgeMethod = ((_ params: Any?, _ result: FlutterResult) -> Void)


@Observable
class AppDelegate: NSObject, UIApplicationDelegate {
    var window: UIWindow?

    private(set) var enginInitError: String?

    enum FlutterBridgeMethodName: String{
        case handleFlutterUrl
        case uploadHealthData
        case healthDataUpdated
    }
    private var methods: [String: FlutterBridgeMethod] = [:]

    var flutterEngine: FlutterEngine?
    var channel: FlutterMethodChannel?
    private var isFlutterEngineReady = false
    private var healthDataUploadObserver: NSObjectProtocol?
    private var healthDataUpdatedObserver: NSObjectProtocol?
    
    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        let configuration = UISceneConfiguration(
              name: nil,
              sessionRole: connectingSceneSession.role
            )
            configuration.delegateClass = FlutterSceneDelegate.self
            return configuration
    }

    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        setup()
        WeChatLoginBridge.shared.registerApp()
        cacheLaunchURLIfNeeded(launchOptions)
        return true
    }

    func application(
        _ app: UIApplication,
        open url: URL,
        options: [UIApplication.OpenURLOptionsKey: Any] = [:]
    ) -> Bool {
        if WeChatLoginBridge.shared.handleOpenURL(url) {
            return true
        }
        if GIDSignIn.sharedInstance.handle(url) {
            return true
        }
        handleFlutterUrl(url.absoluteString)
        return true
    }

    func application(
        _ application: UIApplication,
        continue userActivity: NSUserActivity,
        restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
    ) -> Bool {
        if userActivity.activityType == NSUserActivityTypeBrowsingWeb,
           WeChatLoginBridge.shared.handleUniversalLink(userActivity) {
            return true
        }
        guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
              let urlString = userActivity.webpageURL?.absoluteString else {
            return false
        }
        handleFlutterUrl(urlString)
        return true
    }
    
    func setup(){
        AppShared.shared.agent.genUA()
        AppShared.shared.payment.delegate = self
        _ = AppShared.shared.payment.listenForTransactions()

        UIApplication.shared.registerForRemoteNotifications()
        UNUserNotificationCenter.current().setBadgeCount(0)
        UNUserNotificationCenter.current().delegate = self
        requestIDFAAuthorization()

        WatchConnectivityService.shared.activate()
        observeHealthDataNotificationsIfNeeded()
        HealthKitService.shared.startBackgroundObserversIfNeeded()
    }

    func warmUpFlutterEngineIfNeeded() {
        guard !isFlutterEngineReady else {
            enginInitError = nil
            return
        }
        flutterEngine = FlutterEngine(name: "main_flutter_engine")
        guard let flutterEngine else{
            enginInitError = "engin 初始化失败"
            return
        }
        isFlutterEngineReady = true
        flutterEngine.run()
        GeneratedPluginRegistrant.register(with: flutterEngine)
        NativePigeonRegistrar.register(binaryMessenger: flutterEngine.binaryMessenger)
        channel = FlutterMethodChannel(name: "doublefeel_flutter_main_channel", binaryMessenger: flutterEngine.binaryMessenger)
        methods = defaultMethods()

        enginInitError = nil
    }

    func application(_ application: UIApplication,
                     didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) }
        let token = tokenParts.joined()
        AppGroupConstants.defaults?.set(token, forKey: AppGroupConstants.Key.appDeviceToken)
        Task{
            await AppShared.shared.reportDeviceInfo()
        }
    }

    func application(_ application: UIApplication,
                     didFailToRegisterForRemoteNotificationsWithError error: Error) {
        print("❌ Failed to register: \(error.localizedDescription)")
    }
}

extension AppDelegate{
    private func defaultMethods() -> [String: FlutterBridgeMethod]{
        var methods: [String: FlutterBridgeMethod] = [:]
        methods[FlutterBridgeMethodName.handleFlutterUrl.rawValue] = { params, result in
            result(params)
        }
        methods[FlutterBridgeMethodName.healthDataUpdated.rawValue] = { params, result in
            result(params)
        }

        return methods
    }

    func invoke(method: FlutterBridgeMethodName, arguments: Any?, result: @escaping FlutterResult){
        channel?.invokeMethod(method.rawValue, arguments: arguments, result: result)
    }

    private func observeHealthDataNotificationsIfNeeded() {
        guard healthDataUploadObserver == nil else { return }
        healthDataUploadObserver = NotificationCenter.default.addObserver(
            forName: .nativeHealthDataDidUpload,
            object: nil,
            queue: .main
        ) { [weak self] notification in
            guard let dataType = notification.userInfo?["dataType"] as? Int,
                  let timestamp = notification.userInfo?["timestamp"] as? Int64 else {
                return
            }
            self?.notifyFlutterHealthDataUploaded(dataType: dataType, timestamp: timestamp)
        }
        healthDataUpdatedObserver = NotificationCenter.default.addObserver(
            forName: .nativeHealthDataDidUpdate,
            object: nil,
            queue: .main
        ) { [weak self] notification in
            let dataTypes = notification.userInfo?["dataTypes"] as? [Int] ?? []
            self?.notifyFlutterHealthDataUpdated(dataTypes: dataTypes)
        }
    }

    private func notifyFlutterHealthDataUploaded(dataType: Int, timestamp: Int64) {
        guard channel != nil else {
            DebugLogger.log(desc: "uploadHealthData notification skipped because Flutter channel is not ready")
            return
        }
        invoke(
            method: .uploadHealthData,
            arguments: ["dataType": dataType, "timestamp": timestamp]
        ) { result in
            if let error = result as? FlutterError {
                DebugLogger.log(desc: "uploadHealthData callback error: \(error.message ?? error.code)")
            }
        }
    }

    func notifyFlutterHealthDataUpdated(dataTypes: [Int]) {
        warmUpFlutterEngineIfNeeded()
        guard channel != nil else {
            DebugLogger.log(desc: "healthDataUpdated notification skipped because Flutter channel is not ready")
            return
        }

        invoke(
            method: .healthDataUpdated,
            arguments: [
                "dataTypes": dataTypes,
                "timestamp": Int64(Date().timeIntervalSince1970.rounded()),
            ]
        ) { result in
            if let error = result as? FlutterError {
                DebugLogger.log(desc: "healthDataUpdated callback error: \(error.message ?? error.code)")
            }
        }
    }

    private func cacheLaunchURLIfNeeded(_ launchOptions: [UIApplication.LaunchOptionsKey: Any]?) {
        if let url = launchOptions?[.url] as? URL {
            AppShared.shared.unhandedUrl = url.absoluteString
            return
        }

        if let userActivityDictionary = launchOptions?[.userActivityDictionary] as? [AnyHashable: Any] {
            for value in userActivityDictionary.values {
                guard let userActivity = value as? NSUserActivity,
                      userActivity.activityType == NSUserActivityTypeBrowsingWeb,
                      let urlString = userActivity.webpageURL?.absoluteString else {
                    continue
                }
                AppShared.shared.unhandedUrl = urlString
                return
            }
        }

        if let userInfo = launchOptions?[.remoteNotification] as? [AnyHashable: Any],
           let urlString = Self.flutterURLString(from: userInfo) {
            AppShared.shared.unhandedUrl = urlString
        }
    }

    func handleFlutterUrl(_ urlString: String) {
        guard !urlString.isEmpty else {
            return
        }

        guard channel != nil else {
            AppShared.shared.unhandedUrl = urlString
            return
        }

        invoke(method: .handleFlutterUrl, arguments: [
            "url": urlString
        ]) { result in
            if let error = result as? FlutterError {
                DebugLogger.log(desc: "handleFlutterUrl error: \(error.message ?? error.code)")
            }
        }
    }

    private static func flutterURLString(from userInfo: [AnyHashable: Any]) -> String? {
        if let urlString = userInfo["url"] as? String {
            return urlString
        }

        guard let payload = userInfo["payload"] as? String,
              let data = payload.data(using: .utf8),
              let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
            return nil
        }

        return dict["url"] as? String
    }
}

extension Notification.Name {
    static let nativeHealthDataDidUpload = Notification.Name("nativeHealthDataDidUpload")
    static let nativeHealthDataDidUpdate = Notification.Name("nativeHealthDataDidUpdate")
}

extension AppDelegate: UNUserNotificationCenterDelegate{
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                willPresent notification: UNNotification,
                                withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        completionHandler([.banner, .sound, .badge])
    }

    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        let userInfo = response.notification.request.content.userInfo

        // 从通知中获取 URL
        if let urlString = Self.flutterURLString(from: userInfo) {
            handleFlutterUrl(urlString)
        }
        completionHandler()
    }
}

extension AppDelegate {
    private func requestIDFAAuthorization() {
        if ATTrackingManager.trackingAuthorizationStatus == .notDetermined {
            DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
                ATTrackingManager.requestTrackingAuthorization { status in
                    switch status {
                    case .authorized:
                        let idfa = ASIdentifierManager.shared().advertisingIdentifier.uuidString
                        print("✅ IDFA: \(idfa)")
                    case .denied:
                        print("❌ 用户拒绝了IDFA授权")
                    case .restricted:
                        print("⚠️ IDFA受限制,无法获取")
                    case .notDetermined:
                        print("⏳ 用户尚未做出选择")
                    @unknown default:
                        print("未知的IDFA授权状态")
                    }
//                    ReyunUtil.initSDK()
                }
            }
        } else {
//            ReyunUtil.initSDK()
        }
    }
}