import Flutter import Foundation /// Native-side event subscriber. Mirrors Android `JieliEventListener`. /// Implementers receive the same `payload` dictionary that gets pushed to /// the Flutter EventChannel, so native and Dart subscribers stay symmetric. @objc public protocol JieliEventListener: AnyObject { func onEvent(payload: [String: Any?]) } /// Event bus. All forwarders / features call [send]; this fans out to: /// - Flutter side: through the EventChannel sink (on main queue); /// - Native side: through registered [JieliEventListener]s (sync, on the /// originating queue — fits high-frequency audio frames). /// /// Mirrors Android `bridge/EventDispatcher.kt`. public class EventDispatcher: NSObject, FlutterStreamHandler { private let lock = NSLock() private var sink: FlutterEventSink? private var listeners: [JieliEventListener] = [] // MARK: - FlutterStreamHandler public func onListen( withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink ) -> FlutterError? { lock.lock(); defer { lock.unlock() } sink = events return nil } public func onCancel(withArguments arguments: Any?) -> FlutterError? { lock.lock(); defer { lock.unlock() } sink = nil return nil } // MARK: - Native listeners public func addNativeListener(_ listener: JieliEventListener) { lock.lock(); defer { lock.unlock() } if !listeners.contains(where: { $0 === listener }) { listeners.append(listener) } } public func removeNativeListener(_ listener: JieliEventListener) { lock.lock(); defer { lock.unlock() } listeners.removeAll { $0 === listener } } // MARK: - Send /// Broadcast a single event payload. /// /// Payload schema: identical to Android. `type` is the discriminator key /// that the Dart `Jielihome._parseEvent(...)` switches on. public func send(_ payload: [String: Any?]) { let snapshot: [JieliEventListener] let sinkRef: FlutterEventSink? lock.lock() snapshot = listeners sinkRef = sink lock.unlock() for l in snapshot { l.onEvent(payload: payload) } guard let s = sinkRef else { return } let sanitized = sanitize(payload) if Thread.isMainThread { s(sanitized) } else { DispatchQueue.main.async { s(sanitized) } } } /// Replace `nil` values with `NSNull()` and re-emit `Data` payloads as /// `FlutterStandardTypedData.bytes(...)` so Dart receives `Uint8List` /// rather than an opaque blob (matches Android's ByteArray → Uint8List /// auto-conversion). private func sanitize(_ raw: [String: Any?]) -> [String: Any] { var out = [String: Any]() out.reserveCapacity(raw.count) for (k, v) in raw { out[k] = sanitize(value: v) } return out } private func sanitize(value: Any?) -> Any { guard let v = value else { return NSNull() } if let data = v as? Data { return FlutterStandardTypedData(bytes: data) } if let arr = v as? [Any?] { return arr.map { sanitize(value: $0) } } if let map = v as? [String: Any?] { return sanitize(map) } return v } }