Browse Source

修复 iOS Swift 编译错误

三类问题:
1. JieliEventListener 是 @objc protocol 但参数类型 [String: Any?] 无法
   桥接到 Obj-C → 改成纯 Swift protocol(去掉 @objc),同 module 内
   InternalCleanupListener 仍能正常实现
2. 内部类型(EventDispatcher / TranslationCoordinator / TranslationAudio
   Bridge / EventChannelAudioBridge / 各 Feature 类)声明了 public 但同
   时引用了 internal 类型 → 把 plugin 内部所有 public 全部降为 internal
   (Flutter entry JielihomePlugin / DevicePluginInterfacePlugin 仍保留
   public 因为 Flutter 注册需要)
3. JL_BLEMultiple.otaFunc(...) 的 Result 参数是 nullable closure
   (JL_OTA_RT __nullable),Swift 不接受给 nullable closure 用 trailing
   closure 语法 → 改成显式 closure 字面量赋给 cb 变量再传入

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
weicu
liwei1dao 5 months ago
parent
commit
5af13c0872
  1. 17
      local_plugins/device_jieli/ios/Classes/Bridge/EventDispatcher.swift
  2. 14
      local_plugins/device_jieli/ios/Classes/Bridge/MethodRouter.swift
  3. 34
      local_plugins/device_jieli/ios/Classes/Core/JieliHomeServer.swift
  4. 10
      local_plugins/device_jieli/ios/Classes/Feature/AssistantBridge.swift
  5. 2
      local_plugins/device_jieli/ios/Classes/Feature/ConnectFeature.swift
  6. 4
      local_plugins/device_jieli/ios/Classes/Feature/CustomCmdFeature.swift
  7. 6
      local_plugins/device_jieli/ios/Classes/Feature/DeviceInfoFeature.swift
  8. 8
      local_plugins/device_jieli/ios/Classes/Feature/DeviceRecordFeature.swift
  9. 17
      local_plugins/device_jieli/ios/Classes/Feature/OtaFeature.swift
  10. 18
      local_plugins/device_jieli/ios/Classes/Feature/ScanFeature.swift
  11. 28
      local_plugins/device_jieli/ios/Classes/Feature/SpeechFeature.swift
  12. 16
      local_plugins/device_jieli/ios/Classes/Feature/Translation/TranslationAudioBridge.swift
  13. 24
      local_plugins/device_jieli/ios/Classes/Feature/TranslationFeature.swift
  14. 6
      local_plugins/device_jieli/ios/Classes/Integration/JieliNativeDevicePlugin.swift

17
local_plugins/device_jieli/ios/Classes/Bridge/EventDispatcher.swift

@ -4,7 +4,10 @@ import Foundation
/// Native-side event subscriber. Mirrors Android `JieliEventListener`. /// Native-side event subscriber. Mirrors Android `JieliEventListener`.
/// Implementers receive the same `payload` dictionary that gets pushed to /// Implementers receive the same `payload` dictionary that gets pushed to
/// the Flutter EventChannel, so native and Dart subscribers stay symmetric. /// the Flutter EventChannel, so native and Dart subscribers stay symmetric.
@objc public protocol JieliEventListener: AnyObject { ///
/// Pure-Swift protocol (not `@objc`) because the payload's `[String: Any?]`
/// cannot be bridged to Obj-C (Obj-C dictionary values can't be nullable).
protocol JieliEventListener: AnyObject {
func onEvent(payload: [String: Any?]) func onEvent(payload: [String: Any?])
} }
@ -14,7 +17,7 @@ import Foundation
/// originating queue — fits high-frequency audio frames). /// originating queue — fits high-frequency audio frames).
/// ///
/// Mirrors Android `bridge/EventDispatcher.kt`. /// Mirrors Android `bridge/EventDispatcher.kt`.
public class EventDispatcher: NSObject, FlutterStreamHandler { class EventDispatcher: NSObject, FlutterStreamHandler {
private let lock = NSLock() private let lock = NSLock()
private var sink: FlutterEventSink? private var sink: FlutterEventSink?
@ -22,7 +25,7 @@ public class EventDispatcher: NSObject, FlutterStreamHandler {
// MARK: - FlutterStreamHandler // MARK: - FlutterStreamHandler
public func onListen( func onListen(
withArguments arguments: Any?, withArguments arguments: Any?,
eventSink events: @escaping FlutterEventSink eventSink events: @escaping FlutterEventSink
) -> FlutterError? { ) -> FlutterError? {
@ -31,7 +34,7 @@ public class EventDispatcher: NSObject, FlutterStreamHandler {
return nil return nil
} }
public func onCancel(withArguments arguments: Any?) -> FlutterError? { func onCancel(withArguments arguments: Any?) -> FlutterError? {
lock.lock(); defer { lock.unlock() } lock.lock(); defer { lock.unlock() }
sink = nil sink = nil
return nil return nil
@ -39,14 +42,14 @@ public class EventDispatcher: NSObject, FlutterStreamHandler {
// MARK: - Native listeners // MARK: - Native listeners
public func addNativeListener(_ listener: JieliEventListener) { func addNativeListener(_ listener: JieliEventListener) {
lock.lock(); defer { lock.unlock() } lock.lock(); defer { lock.unlock() }
if !listeners.contains(where: { $0 === listener }) { if !listeners.contains(where: { $0 === listener }) {
listeners.append(listener) listeners.append(listener)
} }
} }
public func removeNativeListener(_ listener: JieliEventListener) { func removeNativeListener(_ listener: JieliEventListener) {
lock.lock(); defer { lock.unlock() } lock.lock(); defer { lock.unlock() }
listeners.removeAll { $0 === listener } listeners.removeAll { $0 === listener }
} }
@ -57,7 +60,7 @@ public class EventDispatcher: NSObject, FlutterStreamHandler {
/// ///
/// Payload schema: identical to Android. `type` is the discriminator key /// Payload schema: identical to Android. `type` is the discriminator key
/// that the Dart `Jielihome._parseEvent(...)` switches on. /// that the Dart `Jielihome._parseEvent(...)` switches on.
public func send(_ payload: [String: Any?]) { func send(_ payload: [String: Any?]) {
let snapshot: [JieliEventListener] let snapshot: [JieliEventListener]
let sinkRef: FlutterEventSink? let sinkRef: FlutterEventSink?
lock.lock() lock.lock()

14
local_plugins/device_jieli/ios/Classes/Bridge/MethodRouter.swift

@ -8,15 +8,15 @@ import UIKit
/// argument keys. Where the iOS JieLi SDK does not yet have a wired /// argument keys. Where the iOS JieLi SDK does not yet have a wired
/// implementation, the handler returns a `NOT_IMPLEMENTED_IOS` error so the /// implementation, the handler returns a `NOT_IMPLEMENTED_IOS` error so the
/// Dart layer can downgrade gracefully (instead of crashing). /// Dart layer can downgrade gracefully (instead of crashing).
public class MethodRouter { class MethodRouter {
private let server: JieliHomeServer private let server: JieliHomeServer
public init(server: JieliHomeServer) { init(server: JieliHomeServer) {
self.server = server self.server = server
} }
public func handle(call: FlutterMethodCall, result: @escaping FlutterResult) { func handle(call: FlutterMethodCall, result: @escaping FlutterResult) {
let args = call.arguments as? [String: Any] ?? [:] let args = call.arguments as? [String: Any] ?? [:]
do { do {
switch call.method { switch call.method {
@ -317,10 +317,10 @@ public class MethodRouter {
/// Local plugin error wrapping that maps cleanly onto FlutterError. Used by /// Local plugin error wrapping that maps cleanly onto FlutterError. Used by
/// feature classes when they need to surface a typed error code without /// feature classes when they need to surface a typed error code without
/// throwing an opaque NSError. /// throwing an opaque NSError.
public struct PluginError: Error { struct PluginError: Error {
public let code: String let code: String
public let message: String let message: String
public init(_ code: String, _ message: String) { init(_ code: String, _ message: String) {
self.code = code self.code = code
self.message = message self.message = message
} }

34
local_plugins/device_jieli/ios/Classes/Core/JieliHomeServer.swift

@ -13,14 +13,14 @@ import JL_BLEKit
/// ///
/// Initialization is idempotent — calling [initialize] twice returns /// Initialization is idempotent — calling [initialize] twice returns
/// immediately on the second call. /// immediately on the second call.
public class JieliHomeServer { class JieliHomeServer {
public static let shared = JieliHomeServer() static let shared = JieliHomeServer()
private(set) public var initialized: Bool = false private(set) public var initialized: Bool = false
/// Event bus shared by Flutter EventChannel + native listeners. /// Event bus shared by Flutter EventChannel + native listeners.
public let dispatcher: EventDispatcher = EventDispatcher() let dispatcher: EventDispatcher = EventDispatcher()
// MARK: - Underlying SDK references // MARK: - Underlying SDK references
// The JieLi iOS SDK exposes BLE plumbing via `JL_BLEMultiple` and per-device // The JieLi iOS SDK exposes BLE plumbing via `JL_BLEMultiple` and per-device
@ -34,36 +34,36 @@ public class JieliHomeServer {
/// `JLTranslationManager`. Holds the active translation sink (call /// `JLTranslationManager`. Holds the active translation sink (call
/// translation / assistant / device record); modules switch sinks as they /// translation / assistant / device record); modules switch sinks as they
/// enter/exit modes — only one mode at a time. /// enter/exit modes — only one mode at a time.
public private(set) lazy var translationCoordinator: TranslationCoordinator = { private(set) lazy var translationCoordinator: TranslationCoordinator = {
TranslationCoordinator(server: self) TranslationCoordinator(server: self)
}() }()
// MARK: - Features (lazy-attached after initialize) // MARK: - Features (lazy-attached after initialize)
public private(set) lazy var scanFeature: ScanFeature = { private(set) lazy var scanFeature: ScanFeature = {
ScanFeature(dispatcher: dispatcher) ScanFeature(dispatcher: dispatcher)
}() }()
public private(set) lazy var connectFeature: ConnectFeature = { private(set) lazy var connectFeature: ConnectFeature = {
ConnectFeature(server: self) ConnectFeature(server: self)
}() }()
public private(set) lazy var deviceInfoFeature: DeviceInfoFeature = { private(set) lazy var deviceInfoFeature: DeviceInfoFeature = {
DeviceInfoFeature(server: self) DeviceInfoFeature(server: self)
}() }()
public private(set) lazy var customCmdFeature: CustomCmdFeature = { private(set) lazy var customCmdFeature: CustomCmdFeature = {
CustomCmdFeature(server: self) CustomCmdFeature(server: self)
}() }()
public private(set) lazy var translationFeature: TranslationFeature = { private(set) lazy var translationFeature: TranslationFeature = {
TranslationFeature(server: self) TranslationFeature(server: self)
}() }()
public private(set) lazy var speechFeature: SpeechFeature = { private(set) lazy var speechFeature: SpeechFeature = {
SpeechFeature(server: self) SpeechFeature(server: self)
}() }()
public private(set) lazy var otaFeature: OtaFeature = { private(set) lazy var otaFeature: OtaFeature = {
OtaFeature(server: self) OtaFeature(server: self)
}() }()
public private(set) lazy var deviceRecordFeature: DeviceRecordFeature = { private(set) lazy var deviceRecordFeature: DeviceRecordFeature = {
DeviceRecordFeature(server: self) DeviceRecordFeature(server: self)
}() }()
public private(set) lazy var assistantBridge: AssistantBridge = { private(set) lazy var assistantBridge: AssistantBridge = {
AssistantBridge(server: self) AssistantBridge(server: self)
}() }()
@ -71,7 +71,7 @@ public class JieliHomeServer {
// MARK: - Lifecycle // MARK: - Lifecycle
public func initialize( func initialize(
multiDevice: Bool, multiDevice: Bool,
skipNoNameDev: Bool, skipNoNameDev: Bool,
enableLog: Bool enableLog: Bool
@ -103,7 +103,7 @@ public class JieliHomeServer {
]) ])
} }
public func shutdown() { func shutdown() {
if !initialized { return } if !initialized { return }
otaFeature.cancel() otaFeature.cancel()
deviceRecordFeature.stop() deviceRecordFeature.stop()
@ -116,11 +116,11 @@ public class JieliHomeServer {
// MARK: - Native event subscription helpers // MARK: - Native event subscription helpers
public func addEventListener(_ listener: JieliEventListener) { func addEventListener(_ listener: JieliEventListener) {
dispatcher.addNativeListener(listener) dispatcher.addNativeListener(listener)
} }
public func removeEventListener(_ listener: JieliEventListener) { func removeEventListener(_ listener: JieliEventListener) {
dispatcher.removeNativeListener(listener) dispatcher.removeNativeListener(listener)
} }

10
local_plugins/device_jieli/ios/Classes/Feature/AssistantBridge.swift

@ -24,7 +24,7 @@ import JL_BLEKit
/// re-routes through the connected A2DP earphone. iOS equivalent is /// re-routes through the connected A2DP earphone. iOS equivalent is
/// `AVAudioEngine`/`AudioQueue`; the orchestrator can drive this directly /// `AVAudioEngine`/`AudioQueue`; the orchestrator can drive this directly
/// or call back through `assistantPlayback` (TODO once needed). /// or call back through `assistantPlayback` (TODO once needed).
public class AssistantBridge { class AssistantBridge {
weak var server: JieliHomeServer? weak var server: JieliHomeServer?
init(server: JieliHomeServer) { self.server = server } init(server: JieliHomeServer) { self.server = server }
@ -36,9 +36,9 @@ public class AssistantBridge {
private var sinkWrapper: AssistantSinkWrapper? private var sinkWrapper: AssistantSinkWrapper?
#endif #endif
public func isRunning() -> Bool { running } func isRunning() -> Bool { running }
public func start() -> Bool { func start() -> Bool {
if running { return true } if running { return true }
#if canImport(JL_BLEKit) #if canImport(JL_BLEKit)
guard let server = server else { return false } guard let server = server else { return false }
@ -118,7 +118,7 @@ public class AssistantBridge {
#endif #endif
} }
public func stop() { func stop() {
if !running { return } if !running { return }
running = false running = false
cleanup() cleanup()
@ -128,7 +128,7 @@ public class AssistantBridge {
]) ])
} }
public func shutdown() { stop() } func shutdown() { stop() }
// MARK: - Internal // MARK: - Internal

2
local_plugins/device_jieli/ios/Classes/Feature/ConnectFeature.swift

@ -12,7 +12,7 @@ import JL_BLEKit
/// (0=disconnect, 1=ok, 2=connecting). RCSP-init success surfaces as a /// (0=disconnect, 1=ok, 2=connecting). RCSP-init success surfaces as a
/// separate `rcspInit` event when the JieLi SDK confirms `cmdTargetFeatureResult` /// separate `rcspInit` event when the JieLi SDK confirms `cmdTargetFeatureResult`
/// returns successfully — same flow Android publishes. /// returns successfully — same flow Android publishes.
public class ConnectFeature: NSObject { class ConnectFeature: NSObject {
weak var server: JieliHomeServer? weak var server: JieliHomeServer?
init(server: JieliHomeServer) { self.server = server } init(server: JieliHomeServer) { self.server = server }

4
local_plugins/device_jieli/ios/Classes/Feature/CustomCmdFeature.swift

@ -12,12 +12,12 @@ import JL_BLEKit
/// argument, so we prepend the opCode byte to the payload and call that. /// argument, so we prepend the opCode byte to the payload and call that.
/// The reply comes back through the result block. This matches the shape /// The reply comes back through the result block. This matches the shape
/// the Dart caller expects: a `[Int]` response. /// the Dart caller expects: a `[Int]` response.
public class CustomCmdFeature { class CustomCmdFeature {
weak var server: JieliHomeServer? weak var server: JieliHomeServer?
init(server: JieliHomeServer) { self.server = server } init(server: JieliHomeServer) { self.server = server }
public func send( func send(
address: String, address: String,
opCode: UInt8, opCode: UInt8,
payload: Data, payload: Data,

6
local_plugins/device_jieli/ios/Classes/Feature/DeviceInfoFeature.swift

@ -10,12 +10,12 @@ import JL_BLEKit
/// caller. The Android schema (`name`, `firmwareVersion`, `battery`, ...) is /// caller. The Android schema (`name`, `firmwareVersion`, `battery`, ...) is
/// preserved; iOS-specific extras (`bleAddr`, `btAddr`, `pidvid`) are added /// preserved; iOS-specific extras (`bleAddr`, `btAddr`, `pidvid`) are added
/// for completeness — Dart consumers ignore unknown keys. /// for completeness — Dart consumers ignore unknown keys.
public class DeviceInfoFeature { class DeviceInfoFeature {
weak var server: JieliHomeServer? weak var server: JieliHomeServer?
init(server: JieliHomeServer) { self.server = server } init(server: JieliHomeServer) { self.server = server }
public func snapshot(address: String) -> [String: Any?]? { func snapshot(address: String) -> [String: Any?]? {
#if canImport(JL_BLEKit) #if canImport(JL_BLEKit)
guard let entity = server?.connectFeature.currentEntity() as? JL_EntityM, guard let entity = server?.connectFeature.currentEntity() as? JL_EntityM,
entity.mUUID == address else { entity.mUUID == address else {
@ -39,7 +39,7 @@ public class DeviceInfoFeature {
#endif #endif
} }
public func queryTargetInfo( func queryTargetInfo(
address: String, address: String,
mask: Int, mask: Int,
completion: @escaping ([String: Any?]?, Error?) -> Void completion: @escaping ([String: Any?]?, Error?) -> Void

8
local_plugins/device_jieli/ios/Classes/Feature/DeviceRecordFeature.swift

@ -21,7 +21,7 @@ import JL_BLEKit
/// ///
/// Mutually exclusive with TranslationFeature — both use the shared /// Mutually exclusive with TranslationFeature — both use the shared
/// `JLTranslationManager`. The MethodRouter stops the other before starting. /// `JLTranslationManager`. The MethodRouter stops the other before starting.
public class DeviceRecordFeature { class DeviceRecordFeature {
weak var server: JieliHomeServer? weak var server: JieliHomeServer?
init(server: JieliHomeServer) { self.server = server } init(server: JieliHomeServer) { self.server = server }
@ -36,9 +36,9 @@ public class DeviceRecordFeature {
private var sinkWrapper: RecordSinkWrapper? private var sinkWrapper: RecordSinkWrapper?
#endif #endif
public func isRecording() -> Bool { working } func isRecording() -> Bool { working }
public func start(args: [String: Any]) throws { func start(args: [String: Any]) throws {
if working { throw PluginError("DEVICE_RECORD_ERR", "already recording") } if working { throw PluginError("DEVICE_RECORD_ERR", "already recording") }
#if canImport(JL_BLEKit) #if canImport(JL_BLEKit)
guard let server = server else { guard let server = server else {
@ -101,7 +101,7 @@ public class DeviceRecordFeature {
#endif #endif
} }
public func stop() { func stop() {
if !working { return } if !working { return }
working = false working = false
let addr = deviceAddress let addr = deviceAddress

17
local_plugins/device_jieli/ios/Classes/Feature/OtaFeature.swift

@ -15,16 +15,16 @@ import JL_OTALib
/// progress callback into the same `otaState` / `otaError` event shapes /// progress callback into the same `otaState` / `otaError` event shapes
/// the Android side publishes — Dart `_parseOtaState(...)` keeps working /// the Android side publishes — Dart `_parseOtaState(...)` keeps working
/// unchanged. /// unchanged.
public class OtaFeature { class OtaFeature {
weak var server: JieliHomeServer? weak var server: JieliHomeServer?
init(server: JieliHomeServer) { self.server = server } init(server: JieliHomeServer) { self.server = server }
private var running: Bool = false private var running: Bool = false
public func isRunning() -> Bool { running } func isRunning() -> Bool { running }
public func start( func start(
address: String?, address: String?,
firmwareFilePath: String, firmwareFilePath: String,
blockSize: Int, blockSize: Int,
@ -47,9 +47,16 @@ public class OtaFeature {
"state": "INQUIRING", "state": "INQUIRING",
"sent": 0, "total": 0, "percent": -1, "tsMs": Self.tsMs(), "sent": 0, "total": 0, "percent": -1, "tsMs": Self.tsMs(),
]) ])
server.bleMultiple.otaFunc(withEntityM: entity, withFilePath: firmwareFilePath) { [weak self] result, progress in // JL_OTA_RT 是 nullable closure (`JL_OTA_RT __nullable`),Swift 不接受
// trailing closure 给 nullable closure 参数 —— 显式拼出 closure 字面量。
let cb: JL_OTA_RT = { [weak self] result, progress in
self?.onOtaCallback(result: result, progress: progress) self?.onOtaCallback(result: result, progress: progress)
} }
server.bleMultiple.otaFunc(
withEntityM: entity,
withFilePath: firmwareFilePath,
result: cb
)
#else #else
server?.dispatcher.send([ server?.dispatcher.send([
"type": "otaError", "type": "otaError",
@ -59,7 +66,7 @@ public class OtaFeature {
#endif #endif
} }
public func cancel() { func cancel() {
if !running { return } if !running { return }
running = false running = false
server?.dispatcher.send([ server?.dispatcher.send([

18
local_plugins/device_jieli/ios/Classes/Feature/ScanFeature.swift

@ -25,7 +25,7 @@ import CoreBluetooth
/// exact entry point is wired we leave them null and rely on the connection /// exact entry point is wired we leave them null and rely on the connection
/// path's BLE-only fallback (matches the Android "fallback when SDK can't /// path's BLE-only fallback (matches the Android "fallback when SDK can't
/// parse" behavior). /// parse" behavior).
public class ScanFeature: NSObject, CBCentralManagerDelegate { class ScanFeature: NSObject, CBCentralManagerDelegate {
private weak var server: JieliHomeServer? private weak var server: JieliHomeServer?
private weak var dispatcher: EventDispatcher? private weak var dispatcher: EventDispatcher?
@ -44,26 +44,26 @@ public class ScanFeature: NSObject, CBCentralManagerDelegate {
// Track which addresses have been published this scan to dedupe. // Track which addresses have been published this scan to dedupe.
private var publishedIds: Set<String> = [] private var publishedIds: Set<String> = []
public init(dispatcher: EventDispatcher) { init(dispatcher: EventDispatcher) {
self.dispatcher = dispatcher self.dispatcher = dispatcher
super.init() super.init()
} }
public func attach(server: JieliHomeServer) { func attach(server: JieliHomeServer) {
self.server = server self.server = server
if centralManager == nil { if centralManager == nil {
centralManager = CBCentralManager(delegate: self, queue: .main) centralManager = CBCentralManager(delegate: self, queue: .main)
} }
} }
public func detach() { func detach() {
stopScan() stopScan()
centralManager = nil centralManager = nil
} }
// MARK: - Public API (mirrors Android signatures) // MARK: - Public API (mirrors Android signatures)
public func startScan( func startScan(
timeoutMs: Int, timeoutMs: Int,
nameList: [String], nameList: [String],
uuidList: [String], uuidList: [String],
@ -103,7 +103,7 @@ public class ScanFeature: NSObject, CBCentralManagerDelegate {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(timeoutMs), execute: work) DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(timeoutMs), execute: work)
} }
public func stopScan() { func stopScan() {
if let work = stopWorkItem { work.cancel(); stopWorkItem = nil } if let work = stopWorkItem { work.cancel(); stopWorkItem = nil }
if let central = centralManager, central.state == .poweredOn { if let central = centralManager, central.state == .poweredOn {
central.stopScan() central.stopScan()
@ -120,11 +120,11 @@ public class ScanFeature: NSObject, CBCentralManagerDelegate {
serviceFilters = [] serviceFilters = []
} }
public func isScanning() -> Bool { scanning } func isScanning() -> Bool { scanning }
// MARK: - CBCentralManagerDelegate // MARK: - CBCentralManagerDelegate
public func centralManagerDidUpdateState(_ central: CBCentralManager) { func centralManagerDidUpdateState(_ central: CBCentralManager) {
let on = central.state == .poweredOn let on = central.state == .poweredOn
dispatcher?.send([ dispatcher?.send([
"type": "adapterStatus", "type": "adapterStatus",
@ -134,7 +134,7 @@ public class ScanFeature: NSObject, CBCentralManagerDelegate {
if !on && scanning { stopScan() } if !on && scanning { stopScan() }
} }
public func centralManager( func centralManager(
_ central: CBCentralManager, _ central: CBCentralManager,
didDiscover peripheral: CBPeripheral, didDiscover peripheral: CBPeripheral,
advertisementData: [String: Any], advertisementData: [String: Any],

28
local_plugins/device_jieli/ios/Classes/Feature/SpeechFeature.swift

@ -14,27 +14,27 @@ import Foundation
/// ///
/// The MethodChannel surface is preserved so the Dart layer (which already /// The MethodChannel surface is preserved so the Dart layer (which already
/// expects errors here on missing devices) sees a clean `SPEECH_START_ERR`. /// expects errors here on missing devices) sees a clean `SPEECH_START_ERR`.
public class SpeechFeature { class SpeechFeature {
public static let voiceTypePcm = 0 static let voiceTypePcm = 0
public static let voiceTypeSpeex = 1 static let voiceTypeSpeex = 1
public static let voiceTypeOpus = 2 static let voiceTypeOpus = 2
public static let sampleRate8k = 8 static let sampleRate8k = 8
public static let sampleRate16k = 16 static let sampleRate16k = 16
public static let vadWayDevice = 0 static let vadWayDevice = 0
public static let vadWaySdk = 1 static let vadWaySdk = 1
public static let reasonNormal = 0 static let reasonNormal = 0
public static let reasonStop = 1 static let reasonStop = 1
weak var server: JieliHomeServer? weak var server: JieliHomeServer?
init(server: JieliHomeServer) { self.server = server } init(server: JieliHomeServer) { self.server = server }
public func isRecording(address: String?) -> Bool { false } func isRecording(address: String?) -> Bool { false }
public func start( func start(
address: String?, address: String?,
voiceType: Int, voiceType: Int,
sampleRate: Int, sampleRate: Int,
@ -45,7 +45,7 @@ public class SpeechFeature {
completion(false, "speech.start not yet implemented on iOS") completion(false, "speech.start not yet implemented on iOS")
} }
public func stop( func stop(
address: String?, address: String?,
reason: Int, reason: Int,
completion: @escaping (Bool, String?) -> Void completion: @escaping (Bool, String?) -> Void
@ -53,7 +53,7 @@ public class SpeechFeature {
completion(false, "speech.stop not yet implemented on iOS") completion(false, "speech.stop not yet implemented on iOS")
} }
public func detach() { func detach() {
// No resources held while stubbed. // No resources held while stubbed.
} }
} }

16
local_plugins/device_jieli/ios/Classes/Feature/Translation/TranslationAudioBridge.swift

@ -3,7 +3,7 @@ import Foundation
/// Bridge between a TranslationModeHandler and the outside world (Flutter /// Bridge between a TranslationModeHandler and the outside world (Flutter
/// EventChannel by default; can be replaced by host code for native-direct /// EventChannel by default; can be replaced by host code for native-direct
/// orchestration). Mirrors Android `feature/translation/TranslationAudioBridge.kt`. /// orchestration). Mirrors Android `feature/translation/TranslationAudioBridge.kt`.
public protocol TranslationAudioBridge: AnyObject { protocol TranslationAudioBridge: AnyObject {
/// Plugin → outside: one captured PCM frame from the device. /// Plugin → outside: one captured PCM frame from the device.
func emitAudioFrame( func emitAudioFrame(
@ -47,14 +47,14 @@ public protocol TranslationAudioBridge: AnyObject {
/// Default bridge backed by the EventDispatcher → Flutter EventChannel /// Default bridge backed by the EventDispatcher → Flutter EventChannel
/// path. Mirrors Android `feature/translation/EventChannelAudioBridge.kt`. /// path. Mirrors Android `feature/translation/EventChannelAudioBridge.kt`.
public final class EventChannelAudioBridge: TranslationAudioBridge { final class EventChannelAudioBridge: TranslationAudioBridge {
private weak var dispatcher: EventDispatcher? private weak var dispatcher: EventDispatcher?
/// Closure invoked when feedTranslatedAudio is called. The default impl /// Closure invoked when feedTranslatedAudio is called. The default impl
/// routes back into TranslationFeature — same as Android's `injector`. /// routes back into TranslationFeature — same as Android's `injector`.
private let injector: (Int, String, Data, TranslationAudioFormat, Bool) -> Bool private let injector: (Int, String, Data, TranslationAudioFormat, Bool) -> Bool
public init( init(
dispatcher: EventDispatcher, dispatcher: EventDispatcher,
injector: @escaping (Int, String, Data, TranslationAudioFormat, Bool) -> Bool injector: @escaping (Int, String, Data, TranslationAudioFormat, Bool) -> Bool
) { ) {
@ -62,7 +62,7 @@ public final class EventChannelAudioBridge: TranslationAudioBridge {
self.injector = injector self.injector = injector
} }
public func emitAudioFrame( func emitAudioFrame(
modeId: Int, modeId: Int,
streamId: String, streamId: String,
pcm: Data, pcm: Data,
@ -85,7 +85,7 @@ public final class EventChannelAudioBridge: TranslationAudioBridge {
]) ])
} }
public func emitTranslationResult( func emitTranslationResult(
modeId: Int, modeId: Int,
srcLang: String?, srcLang: String?,
srcText: String?, srcText: String?,
@ -104,7 +104,7 @@ public final class EventChannelAudioBridge: TranslationAudioBridge {
]) ])
} }
public func emitLog(modeId: Int, content: String) { func emitLog(modeId: Int, content: String) {
dispatcher?.send([ dispatcher?.send([
"type": "translationLog", "type": "translationLog",
"modeId": modeId, "modeId": modeId,
@ -112,7 +112,7 @@ public final class EventChannelAudioBridge: TranslationAudioBridge {
]) ])
} }
public func emitError(modeId: Int, code: Int, message: String?) { func emitError(modeId: Int, code: Int, message: String?) {
dispatcher?.send([ dispatcher?.send([
"type": "translationError", "type": "translationError",
"modeId": modeId, "modeId": modeId,
@ -122,7 +122,7 @@ public final class EventChannelAudioBridge: TranslationAudioBridge {
} }
@discardableResult @discardableResult
public func feedTranslatedAudio( func feedTranslatedAudio(
modeId: Int, modeId: Int,
outputStreamId: String, outputStreamId: String,
pcm: Data, pcm: Data,

24
local_plugins/device_jieli/ios/Classes/Feature/TranslationFeature.swift

@ -9,7 +9,7 @@ import Foundation
/// ///
/// The `feedTranslatedAudio` / `feedTranslationResult` paths route to the /// The `feedTranslatedAudio` / `feedTranslationResult` paths route to the
/// active handler / bridge, exactly like Android. /// active handler / bridge, exactly like Android.
public class TranslationFeature: NSObject { class TranslationFeature: NSObject {
weak var server: JieliHomeServer? weak var server: JieliHomeServer?
@ -38,7 +38,7 @@ public class TranslationFeature: NSObject {
/// Replace the active bridge — used by host code that wants to keep /// Replace the active bridge — used by host code that wants to keep
/// audio inside native (mirrors Android `setTranslationBridge`). /// audio inside native (mirrors Android `setTranslationBridge`).
public func setBridge(_ bridge: TranslationAudioBridge) { func setBridge(_ bridge: TranslationAudioBridge) {
self.bridge = bridge self.bridge = bridge
guard let server = server else { return } guard let server = server else { return }
handlers = [ handlers = [
@ -53,19 +53,19 @@ public class TranslationFeature: NSObject {
// MARK: - Public API // MARK: - Public API
public func isWorking() -> Bool { current?.isWorking ?? false } func isWorking() -> Bool { current?.isWorking ?? false }
public func currentModeId() -> Int? { func currentModeId() -> Int? {
guard let h = current, h.isWorking else { return nil } guard let h = current, h.isWorking else { return nil }
return h.modeId return h.modeId
} }
public func currentInputStreams() -> [String] { func currentInputStreams() -> [String] {
current?.isWorking == true ? (current?.inputStreams ?? []) : [] current?.isWorking == true ? (current?.inputStreams ?? []) : []
} }
public func currentOutputStreams() -> [String] { func currentOutputStreams() -> [String] {
current?.isWorking == true ? (current?.outputStreams ?? []) : [] current?.isWorking == true ? (current?.outputStreams ?? []) : []
} }
public func start(modeId: Int, args: [String: Any]) throws { func start(modeId: Int, args: [String: Any]) throws {
// Match Android logic — auto-upgrade mode=3 + OPUS to mode=6 stereo when // Match Android logic — auto-upgrade mode=3 + OPUS to mode=6 stereo when
// the device supports it (and caller did not opt out). // the device supports it (and caller did not opt out).
let bypassStereo = (args["bypassStereoUpgrade"] as? Bool) ?? false let bypassStereo = (args["bypassStereoUpgrade"] as? Bool) ?? false
@ -91,13 +91,13 @@ public class TranslationFeature: NSObject {
current = handler current = handler
} }
public func stop() { func stop() {
current?.stop() current?.stop()
current = nil current = nil
} }
@discardableResult @discardableResult
public func feedTranslatedAudio( func feedTranslatedAudio(
streamId: String, streamId: String,
pcm: Data, pcm: Data,
sampleRate: Int, sampleRate: Int,
@ -119,7 +119,7 @@ public class TranslationFeature: NSObject {
) )
} }
public func feedTranslationResult( func feedTranslationResult(
srcLang: String?, srcText: String?, srcLang: String?, srcText: String?,
destLang: String?, destText: String?, destLang: String?, destText: String?,
requestId: String? requestId: String?
@ -139,7 +139,7 @@ public class TranslationFeature: NSObject {
/// `JLDeviceConfigTws.isSupportOpusStereo` flag when available. As a /// `JLDeviceConfigTws.isSupportOpusStereo` flag when available. As a
/// pragmatic default we just return `trIsPlayWithA2dp()`, matching the /// pragmatic default we just return `trIsPlayWithA2dp()`, matching the
/// demo's `setupModeForCall` heuristic. /// demo's `setupModeForCall` heuristic.
public func isSupportCallTranslationWithStereo(address: String?) -> Bool { func isSupportCallTranslationWithStereo(address: String?) -> Bool {
#if canImport(JL_BLEKit) #if canImport(JL_BLEKit)
guard let server = server else { return false } guard let server = server else { return false }
return server.translationCoordinator.manager?.trIsPlayWithA2dp() ?? false return server.translationCoordinator.manager?.trIsPlayWithA2dp() ?? false
@ -152,5 +152,5 @@ public class TranslationFeature: NSObject {
/// dedicated handler maps to MODE_AUDIO_TRANSLATION which the demo drives /// dedicated handler maps to MODE_AUDIO_TRANSLATION which the demo drives
/// by starting recording-and-play. Returns `false` until implemented to /// by starting recording-and-play. Returns `false` until implemented to
/// keep the Dart contract intact. /// keep the Dart contract intact.
public func feedAudioFilePcm(pcm: Data, sampleRate: Int) -> Bool { false } func feedAudioFilePcm(pcm: Data, sampleRate: Int) -> Bool { false }
} }

6
local_plugins/device_jieli/ios/Classes/Integration/JieliNativeDevicePlugin.swift

@ -19,11 +19,11 @@ import Foundation
/// 3. Hand both to the registry. /// 3. Hand both to the registry.
/// ///
/// Until then `register()` is a no-op so the plugin still loads cleanly. /// Until then `register()` is a no-op so the plugin still loads cleanly.
public enum JieliNativeDevicePlugin { enum JieliNativeDevicePlugin {
public static let kVendor = "jieli" static let kVendor = "jieli"
public static func register() { static func register() {
// TODO: when device_plugin_interface iOS gains a NativeDevicePluginRegistry, // TODO: when device_plugin_interface iOS gains a NativeDevicePluginRegistry,
// wire the JieLi factory here, mirroring Android's // wire the JieLi factory here, mirroring Android's
// `JieliNativeDevicePlugin.register(context)`. // `JieliNativeDevicePlugin.register(context)`.

Loading…
Cancel
Save