You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

107 lines
3.1 KiB

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
}
}