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