From 668c8ac3fad6ce7a598ac21d2aee1acdcdf9f6c1 Mon Sep 17 00:00:00 2001 From: wolfplus Date: Fri, 2 May 2025 18:16:49 +0100 Subject: [PATCH] add --- android/app/src/main/AndroidManifest.xml | 23 - .../deepsound/ClassicBluetoothHelper.kt | 383 ++++++++++++++++ .../deepsound/ClassicBluetoothPlugin.kt | 197 ++++++++ .../yunqiinnovation/deepsound/MainActivity.kt | 24 +- lib/core/bindings/initial_binding.dart | 17 +- .../services/classic_bluetooth_service.dart | 214 ++++----- .../home/controllers/home_controller.dart | 253 +++------- lib/modules/home/views/home_view.dart | 2 +- .../controllers/pairing_controller.dart | 274 ++++++----- .../agent_service/android/build.gradle.kts | 2 +- .../agent_service/android/settings.gradle.kts | 4 +- .../agent_service/AgentService.kt | 4 +- local_plugins/agent_service/pubspec.yaml | 4 +- .../ble_service/android/build.gradle.kts | 2 +- .../ble_service/android/settings.gradle.kts | 1 + .../android/src/main/AndroidManifest.xml | 3 +- .../yunqiinnovation/ble_service/BleConst.kt | 12 +- .../ble_service/BleForegroundService.kt | 376 ++++++--------- .../ble_service/BleManagerWrapper.kt | 58 +-- .../yunqiinnovation/ble_service/BleService.kt | 434 +++++++++--------- .../ble_service/BleServicePlugin.kt | 19 - .../ble_service/BleWakeReceiver.kt | 7 +- .../android/build.gradle.kts | 40 -- .../android/settings.gradle.kts | 1 - .../android/src/main/AndroidManifest.xml | 17 - .../ClassicBluetoothHelper.kt | 266 ----------- .../ClassicBluetoothPlugin.kt | 186 -------- .../lib/classic_bluetooth.dart | 119 ----- local_plugins/classic_bluetooth/pubspec.yaml | 23 - .../.gradle/8.10/fileHashes/fileHashes.lock | Bin 17 -> 17 bytes .../buildOutputCleanup.lock | Bin 17 -> 17 bytes .../buildOutputCleanup/cache.properties | 2 +- pubspec.yaml | 6 +- test.json | 231 +--------- 34 files changed, 1371 insertions(+), 1833 deletions(-) create mode 100644 android/app/src/main/kotlin/com/yunqiinnovation/deepsound/ClassicBluetoothHelper.kt create mode 100644 android/app/src/main/kotlin/com/yunqiinnovation/deepsound/ClassicBluetoothPlugin.kt create mode 100644 local_plugins/ble_service/android/settings.gradle.kts delete mode 100644 local_plugins/classic_bluetooth/android/build.gradle.kts delete mode 100644 local_plugins/classic_bluetooth/android/settings.gradle.kts delete mode 100644 local_plugins/classic_bluetooth/android/src/main/AndroidManifest.xml delete mode 100644 local_plugins/classic_bluetooth/android/src/main/kotlin/com/yunqiinnovation/classic_bluetooth/ClassicBluetoothHelper.kt delete mode 100644 local_plugins/classic_bluetooth/android/src/main/kotlin/com/yunqiinnovation/classic_bluetooth/ClassicBluetoothPlugin.kt delete mode 100644 local_plugins/classic_bluetooth/lib/classic_bluetooth.dart delete mode 100644 local_plugins/classic_bluetooth/pubspec.yaml diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 941f7665f..6a87534b7 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -67,29 +67,6 @@ - - - - - - - - - - - - - - - - - = Build.VERSION_CODES.S) { + ContextCompat.checkSelfPermission( + context, + Manifest.permission.BLUETOOTH_CONNECT + ) == PackageManager.PERMISSION_GRANTED + } else { + true + } + } + + /** + * 检查蓝牙是否启用 + * @return 蓝牙是否启用 + */ + fun isBluetoothEnabled(): Boolean { + try { + val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() + if (bluetoothAdapter == null) { + return false + } + + return bluetoothAdapter.isEnabled + } catch (e: Exception) { + Log.e(TAG, "检查蓝牙状态失败", e) + return false + } + } + + /** + * 获取指定配置文件的已连接蓝牙设备 + * @param profile 蓝牙配置文件类型,如 BluetoothProfile.A2DP 或 BluetoothProfile.HEADSET + * @param callback 回调接口,返回设备列表或错误信息 + */ + fun getConnectedBluetoothDevices(profile: Int, callback: BluetoothDevicesCallback) { + val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() + if (bluetoothAdapter == null) { + callback.onError("NO_ADAPTER", "没有找到蓝牙适配器") + return + } + + // 检查蓝牙权限 + if (!hasBluetoothConnectPermission()) { + callback.onError("PERMISSION_DENIED", "缺少蓝牙连接权限") + return + } + + try { + // 检查是否已经有缓存的代理对象 + val cachedProxy = when (profile) { + BluetoothProfile.A2DP -> a2dpProxy + BluetoothProfile.HEADSET -> headsetProxy + else -> null + } + + if (cachedProxy != null) { + // 使用已缓存的代理对象 + processDeviceList(cachedProxy, profile, callback) + return + } + + // 使用原子布尔值确保回调只被调用一次 + val callbackCalled = AtomicBoolean(false) + + // 使用Handler处理超时 + val handler = Handler(Looper.getMainLooper()) + val timeoutRunnable = Runnable { + if (callbackCalled.compareAndSet(false, true)) { + callback.onError("TIMEOUT", "获取蓝牙设备超时") + } + } + + // 设置3秒超时 + handler.postDelayed(timeoutRunnable, 3000) + + // 通过 getProfileProxy 获取指定 Profile (A2DP/HEADSET) 的连接状态 + bluetoothAdapter.getProfileProxy(context, + object : BluetoothProfile.ServiceListener { + override fun onServiceConnected(profileId: Int, proxy: BluetoothProfile) { + // 先移除超时处理 + handler.removeCallbacks(timeoutRunnable) + + if (profileId == profile) { + // 缓存代理对象 + when (profileId) { + BluetoothProfile.A2DP -> a2dpProxy = proxy + BluetoothProfile.HEADSET -> headsetProxy = proxy + } + + if (callbackCalled.compareAndSet(false, true)) { + try { + processDeviceList(proxy, profileId, callback) + } catch (e: Exception) { + callback.onError("DEVICE_ERROR", "处理设备信息时出错") + } + } + } + } + + override fun onServiceDisconnected(profileId: Int) { + // 清除对应的缓存代理 + when (profileId) { + BluetoothProfile.A2DP -> a2dpProxy = null + BluetoothProfile.HEADSET -> headsetProxy = null + } + } + }, profile + ) + } catch (e: Exception) { + callback.onError("BLUETOOTH_ERROR", "获取蓝牙设备失败") + } + } + + /** + * 处理设备列表 + */ + private fun processDeviceList(proxy: BluetoothProfile, profileId: Int, callback: BluetoothDevicesCallback) { + // 获取已连接设备 + val connectedDevices = proxy.connectedDevices + + // 转换为设备信息列表 + val devicesList = connectedDevices.map { device -> + mapOf( + "name" to (device.name ?: "未知设备"), + "address" to device.address + ) + } + + // 返回设备列表(不关闭代理) + callback.onSuccess(devicesList) + } + + /** + * 检查是否有蓝牙权限 + */ + fun hasBluetoothPermission(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + ContextCompat.checkSelfPermission( + context, + Manifest.permission.BLUETOOTH_CONNECT + ) == PackageManager.PERMISSION_GRANTED + } else { + ContextCompat.checkSelfPermission( + context, + Manifest.permission.BLUETOOTH + ) == PackageManager.PERMISSION_GRANTED + } + } + + + /** + * 开始监听蓝牙状态变化 + * @param callback 蓝牙状态回调接口 + */ + fun startBluetoothStateMonitoring(callback: BluetoothStateCallback) { + this.stateCallback = callback + + // 注册蓝牙广播接收器(合并状态变化和设备连接/断开监听) + if (bluetoothReceiver == null) { + bluetoothReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + when (intent?.action) { + // 监听蓝牙状态变化 + BluetoothAdapter.ACTION_STATE_CHANGED -> { + val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR) + when (state) { + BluetoothAdapter.STATE_ON -> { + stateCallback?.onBluetoothStateChanged(BluetoothState.ON) + } + BluetoothAdapter.STATE_OFF -> { + stateCallback?.onBluetoothStateChanged(BluetoothState.OFF) + } + BluetoothAdapter.STATE_TURNING_ON -> { + stateCallback?.onBluetoothStateChanged(BluetoothState.TURNING_ON) + } + BluetoothAdapter.STATE_TURNING_OFF -> { + stateCallback?.onBluetoothStateChanged(BluetoothState.TURNING_OFF) + } + } + } + // 监听设备连接 + BluetoothDevice.ACTION_ACL_CONNECTED -> { + val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java) + } else { + @Suppress("DEPRECATION") + intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) + } + + if (device != null) { + stateCallback?.onDeviceConnected( + mapOf( + "name" to (device.name ?: "未知设备"), + "address" to device.address + ) + ) + } + } + // 监听设备断开 + BluetoothDevice.ACTION_ACL_DISCONNECTED -> { + val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java) + } else { + @Suppress("DEPRECATION") + intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) + } + + if (device != null) { + stateCallback?.onDeviceDisconnected( + mapOf( + "name" to (device.name ?: "未知设备"), + "address" to device.address + ) + ) + } + } + } + } + } + + // 创建IntentFilter并添加所有需要监听的动作 + val filter = IntentFilter().apply { + addAction(BluetoothAdapter.ACTION_STATE_CHANGED) + addAction(BluetoothDevice.ACTION_ACL_CONNECTED) + addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED) + } + + context.registerReceiver(bluetoothReceiver, filter) + // 立即返回当前状态 + reportCurrentBluetoothState() + } + } + + /** + * 报告当前蓝牙状态 + */ + private fun reportCurrentBluetoothState() { + val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() + if (bluetoothAdapter != null) { + val state = when (bluetoothAdapter.state) { + BluetoothAdapter.STATE_ON -> BluetoothState.ON + BluetoothAdapter.STATE_OFF -> BluetoothState.OFF + BluetoothAdapter.STATE_TURNING_ON -> BluetoothState.TURNING_ON + BluetoothAdapter.STATE_TURNING_OFF -> BluetoothState.TURNING_OFF + else -> BluetoothState.UNKNOWN + } + stateCallback?.onBluetoothStateChanged(state) + } + } + + /** + * 停止监听蓝牙状态变化 + */ + fun stopBluetoothStateMonitoring() { + // 注销蓝牙广播接收器 + if (bluetoothReceiver != null) { + try { + context.unregisterReceiver(bluetoothReceiver) + } catch (e: Exception) { + Log.e(TAG, "注销蓝牙接收器失败", e) + } + bluetoothReceiver = null + } + + // 清除回调 + stateCallback = null + } + + /** + * 释放资源 + */ + fun dispose() { + val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() ?: return + + // 停止蓝牙状态监听 + stopBluetoothStateMonitoring() + + // 释放A2DP代理 + a2dpProxy?.let { + try { + bluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP, it) + } catch (e: Exception) { + Log.e(TAG, "关闭A2DP代理失败", e) + } + a2dpProxy = null + } + + // 释放HEADSET代理 + headsetProxy?.let { + try { + bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, it) + } catch (e: Exception) { + Log.e(TAG, "关闭HEADSET代理失败", e) + } + headsetProxy = null + } + } + + /** + * 蓝牙设备回调接口 + */ + interface BluetoothDevicesCallback { + fun onSuccess(devices: List>) + fun onError(code: String, message: String) + } + + /** + * 蓝牙状态回调接口 + */ + interface BluetoothStateCallback { + fun onBluetoothStateChanged(state: BluetoothState) + fun onDeviceConnected(device: Map) + fun onDeviceDisconnected(device: Map) + } + + /** + * 蓝牙状态枚举 + */ + enum class BluetoothState { + UNKNOWN, + OFF, + ON, + TURNING_ON, + TURNING_OFF + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/ClassicBluetoothPlugin.kt b/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/ClassicBluetoothPlugin.kt new file mode 100644 index 000000000..e4c064b40 --- /dev/null +++ b/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/ClassicBluetoothPlugin.kt @@ -0,0 +1,197 @@ +package com.yunqiinnovation.deepsound + +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothProfile +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.util.Log +import androidx.annotation.NonNull +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result +import com.yunqiinnovation.deepvoice_utils.FileLogger + +/** ClassicBluetoothPlugin */ +class ClassicBluetoothPlugin: FlutterPlugin, MethodCallHandler { + private val TAG = "ClassicBluetoothPlugin" + private lateinit var channel: MethodChannel + private lateinit var eventChannel: EventChannel + private lateinit var context: Context + private lateinit var bluetoothHelper: ClassicBluetoothHelper + private var eventSink: EventChannel.EventSink? = null + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + context = flutterPluginBinding.applicationContext + bluetoothHelper = ClassicBluetoothHelper(context) + + // 设置方法通道 + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.classic_bluetooth/methods") + channel.setMethodCallHandler(this) + + // 设置事件通道(用于蓝牙状态监控) + eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.classic_bluetooth/events") + eventChannel.setStreamHandler(object : EventChannel.StreamHandler { + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + eventSink = events + startBluetoothStateMonitoring() + reportCurrentBluetoothState() + } + + override fun onCancel(arguments: Any?) { + eventSink = null + bluetoothHelper.stopBluetoothStateMonitoring() + } + }) + } + + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + when (call.method) { + "getConnectedDevices" -> { + getConnectedDevices(result) + } + "isBluetoothEnabled" -> { + result.success(bluetoothHelper.isBluetoothEnabled()) + } + "hasBluetoothPermission" -> { + result.success(bluetoothHelper.hasBluetoothPermission()) + } + else -> { + result.notImplemented() + } + } + } + + /** + * 获取所有已连接的蓝牙设备 + */ + private fun getConnectedDevices(result: Result) { + if (!bluetoothHelper.isBluetoothEnabled()) { + result.success(emptyList>()) + return + } + + // 使用HEADSET配置文件获取设备 + bluetoothHelper.getConnectedBluetoothDevices(BluetoothProfile.HEADSET, object : ClassicBluetoothHelper.BluetoothDevicesCallback { + override fun onSuccess(devices: List>) { + result.success(devices) + } + + override fun onError(code: String, message: String) { + result.error(code, message, null) + } + }) + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + eventChannel.setStreamHandler(null) + + // 确保停止蓝牙状态监听 + bluetoothHelper.stopBluetoothStateMonitoring() + bluetoothHelper.dispose() + } + + /** + * 报告当前蓝牙状态 + */ + private fun reportCurrentBluetoothState() { + try { + val adapter = BluetoothAdapter.getDefaultAdapter() + if (adapter != null) { + val stateString = when (adapter.state) { + BluetoothAdapter.STATE_ON -> "on" + BluetoothAdapter.STATE_OFF -> "off" + BluetoothAdapter.STATE_TURNING_ON -> "turningOn" + BluetoothAdapter.STATE_TURNING_OFF -> "turningOff" + else -> "unknown" + } + + // 发送当前状态 + val eventMap = mapOf( + "type" to "stateChanged", + "state" to stateString, + "timestamp" to System.currentTimeMillis() + ) + + sendEvent(eventMap) + } + } catch (e: Exception) { + FileLogger.e(TAG, "蓝牙状态获取失败", e) + } + } + + /** + * 开始监听蓝牙状态变化 + */ + private fun startBluetoothStateMonitoring() { + bluetoothHelper.startBluetoothStateMonitoring(object : ClassicBluetoothHelper.BluetoothStateCallback { + override fun onBluetoothStateChanged(state: ClassicBluetoothHelper.BluetoothState) { + val stateString = when (state) { + ClassicBluetoothHelper.BluetoothState.ON -> "on" + ClassicBluetoothHelper.BluetoothState.OFF -> "off" + ClassicBluetoothHelper.BluetoothState.TURNING_ON -> "turningOn" + ClassicBluetoothHelper.BluetoothState.TURNING_OFF -> "turningOff" + ClassicBluetoothHelper.BluetoothState.UNKNOWN -> "unknown" + } + + // 发送状态变化事件 + val eventMap = mapOf( + "type" to "stateChanged", + "state" to stateString, + "timestamp" to System.currentTimeMillis() + ) + + sendEvent(eventMap) + } + + override fun onDeviceConnected(device: Map) { + FileLogger.d(TAG, "设备连接: ${device["name"]} (${device["address"]})") + + // 发送设备连接事件 + val eventMap = mapOf( + "type" to "deviceConnected", + "device" to mapOf( + "name" to (device["name"] ?: "未知设备"), + "address" to (device["address"] ?: "") + ), + "timestamp" to System.currentTimeMillis() + ) + + sendEvent(eventMap) + } + + override fun onDeviceDisconnected(device: Map) { + FileLogger.d(TAG, "设备断开: ${device["name"]} (${device["address"]})") + + // 发送设备断开事件 + val eventMap = mapOf( + "type" to "deviceDisconnected", + "device" to mapOf( + "name" to (device["name"] ?: "未知设备"), + "address" to (device["address"] ?: "") + ), + "timestamp" to System.currentTimeMillis() + ) + + sendEvent(eventMap) + } + }) + } + + /** + * 发送事件到Flutter端 + */ + private fun sendEvent(event: Map) { + try { + eventSink?.success(event) + } catch (e: Exception) { + FileLogger.e(TAG, "事件发送失败", e) + } + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt b/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt index 2c69d1803..867a5215d 100644 --- a/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt +++ b/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt @@ -1,5 +1,8 @@ package com.yunqiinnovation.deepsound +import android.app.PendingIntent +import android.bluetooth.le.ScanFilter +import android.bluetooth.le.ScanSettings import android.content.Intent import android.os.Build import android.os.Bundle @@ -13,7 +16,6 @@ import com.yunqiinnovation.deepvoice_utils.FileLogger import com.yunqiinnovation.ble_service.BleForegroundService import com.yunqiinnovation.ble_service.BleManagerWrapper - class MainActivity: FlutterActivity() { private val TAG = "MainActivity" @@ -49,13 +51,10 @@ class MainActivity: FlutterActivity() { // 初始化FileLogger FileLogger.init(applicationContext) - // 初始化ble_service后台唤醒功能 - // initBleBackgroundScan() - - // 加载配置 - // loadConfigFromSecureStorage() + // 初始化BLE后台唤醒功能 + initBleBackgroundScan() - FileLogger.d(TAG, "MainActivity.onCreate 已初始化") + FileLogger.d(TAG, "初始化完成") } /** @@ -63,23 +62,20 @@ class MainActivity: FlutterActivity() { */ private fun loadConfigFromSecureStorage() { val data = Utils.loadAllSecureData(applicationContext) - - // 将所有数据加载到全局配置字典中 configMap.clear() configMap.putAll(data) - - FileLogger.d(TAG, "已从安全存储加载配置") } override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) GeneratedPluginRegistrant.registerWith(flutterEngine) - FileLogger.d(TAG, "Flutter 引擎配置完成") + // 注册经典蓝牙插件 + flutterEngine.plugins.add(ClassicBluetoothPlugin()) } override fun onDestroy() { - FileLogger.d(TAG, "onDestroy") + super.onDestroy() } @@ -101,7 +97,7 @@ class MainActivity: FlutterActivity() { FileLogger.d(TAG, "成功启动BLE后台唤醒功能") } catch (e: Exception) { - FileLogger.e(TAG, "启动BLE后台唤醒功能失败", e) + FileLogger.e(TAG, "BLE初始化失败", e) } } } diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart index 9a7a3abec..a3a096b3a 100644 --- a/lib/core/bindings/initial_binding.dart +++ b/lib/core/bindings/initial_binding.dart @@ -39,20 +39,19 @@ class InitialBinding extends Bindings { // 火山AI服务 Get.lazyPut(() => VolcanoAIService(), fenix: true); - // 经典蓝牙服务 - Get.lazyPut(() => ClassicBluetoothService(), - fenix: true); + // 经典蓝牙服务 - 立即初始化以监听蓝牙状态 + Get.lazyPut(() =>ClassicBluetoothService(), fenix: true); // 蓝牙媒体按钮服务 - Get.lazyPut( - () => BluetoothMediaButtonService(), - fenix: true); + // Get.lazyPut( + // () => BluetoothMediaButtonService(), + // fenix: true); - // 语音交互服务 - Get.lazyPut(() => VoiceInteractionService(), - fenix: true); + // // 语音交互服务 + // Get.lazyPut(() => VoiceInteractionService(), + // fenix: true); // 杰理蓝牙服务 diff --git a/lib/data/services/classic_bluetooth_service.dart b/lib/data/services/classic_bluetooth_service.dart index 1bd4e7363..09319daea 100644 --- a/lib/data/services/classic_bluetooth_service.dart +++ b/lib/data/services/classic_bluetooth_service.dart @@ -11,23 +11,32 @@ class BluetoothDevice { final String address; final String type; - BluetoothDevice( - {required this.name, required this.address, required this.type}); + BluetoothDevice({ + required this.name, + required this.address, + required this.type + }); + + factory BluetoothDevice.fromMap(Map map) { + return BluetoothDevice( + name: map['name'] ?? '未知设备', + address: map['address'] ?? '', + type: map['type'] ?? 'unknown', + ); + } @override - String toString() => - 'BluetoothDevice(name: $name, address: $address, type: $type)'; + String toString() => 'BluetoothDevice(name: $name, address: $address, type: $type)'; } /// 经典蓝牙服务 +/// 负责经典蓝牙设备的连接状态管理 class ClassicBluetoothService extends GetxService { static ClassicBluetoothService get to => Get.find(); // 通道名称 - final MethodChannel _channel = - const MethodChannel('com.deep_voice.classic_bluetooth'); - final EventChannel _eventChannel = - const EventChannel('com.deep_voice.classic_bluetooth_events'); + final _methodChannel = const MethodChannel('com.yunqiinnovation.classic_bluetooth/methods'); + final _eventChannel = const EventChannel('com.yunqiinnovation.classic_bluetooth/events'); /// 当前连接的设备 final currentDevice = Rx(null); @@ -35,19 +44,19 @@ class ClassicBluetoothService extends GetxService { /// 已连接耳机设备列表 final connectedDevices = [].obs; - /// 蓝牙设备状态监听 - StreamSubscription? _bluetoothStateListener; + /// 蓝牙事件监听 + StreamSubscription? _eventSubscription; /// 蓝牙开关状态 final bluetoothState = 'unknown'.obs; /// 蓝牙是否可用 - bool get isBluetoothOn => bluetoothState.value == 'on'; + bool get isBluetoothEnabled => bluetoothState.value == 'on'; /// 服务是否已初始化 bool _isInitialized = false; - /// 初始化蓝牙服务 - 单一入口点 + /// 初始化蓝牙服务 Future initialize() async { if (_isInitialized) { Logger.info('蓝牙服务已经初始化,跳过'); @@ -57,19 +66,22 @@ class ClassicBluetoothService extends GetxService { try { Logger.info('正在初始化蓝牙服务...'); - // 获取当前蓝牙状态 - final bool isEnabled = - await _channel.invokeMethod('isBluetoothEnabled') ?? false; + // 检查蓝牙是否启用 + final bool isEnabled = await _methodChannel.invokeMethod('isBluetoothEnabled') ?? false; bluetoothState.value = isEnabled ? 'on' : 'off'; Logger.info('初始蓝牙状态: ${bluetoothState.value}'); - await _channel.invokeMethod('initialize'); - // 设置事件监听 - _setupBluetoothStateListener(); + _setupEventListener(); _isInitialized = true; Logger.info('蓝牙服务初始化成功'); + + // 如果蓝牙已启用,自动获取已连接设备 + if (isEnabled) { + await refreshConnectedDevices(); + } + return true; } catch (e) { Logger.error('初始化蓝牙服务失败: $e'); @@ -77,147 +89,135 @@ class ClassicBluetoothService extends GetxService { } } - /// 检查蓝牙权限 - Future checkPermissions() async { - // 检查蓝牙相关权限 - Map statuses = await [ - Permission.bluetooth, - Permission.bluetoothConnect, - Permission.bluetoothScan, - Permission.microphone, - ].request(); - - return statuses[Permission.bluetoothConnect] == PermissionStatus.granted; + /// 设置事件监听 + void _setupEventListener() { + _eventSubscription?.cancel(); + _eventSubscription = _eventChannel.receiveBroadcastStream().listen((event) { + if (event is Map) { + Logger.info('收到蓝牙事件: $event'); + + // 根据事件类型处理 + final type = event['type'] as String?; + if (type == 'stateChanged') { + final state = event['state'] as String?; + if (state != null) { + _handleBluetoothStateChange(state); + } + } else if (type == 'deviceConnected') { + final deviceMap = event['device'] as Map?; + if (deviceMap != null) { + _handleDeviceConnected(deviceMap); + } + } else if (type == 'deviceDisconnected') { + final deviceMap = event['device'] as Map?; + if (deviceMap != null) { + _handleDeviceDisconnected(deviceMap); + } + } + } + }, onError: (e) { + Logger.error('蓝牙事件监听错误: $e'); + }); } - /// 刷新已连接的蓝牙耳机设备 + + /// 刷新已连接的蓝牙设备 Future refreshConnectedDevices() async { // 确保服务已初始化 if (!_isInitialized && !(await initialize())) { return false; } - // 如果蓝牙未开启,无法获取设备 - if (!isBluetoothOn) { - Logger.info('蓝牙未开启,无法获取设备'); + // 如果蓝牙未启用,无法获取设备 + if (!isBluetoothEnabled) { + Logger.info('蓝牙未启用,无法获取设备'); return false; } try { - // 只获取耳机设备 - final devices = await getConnectedHeadsetDevices(); + // 获取已连接的设备 + final devices = await getConnectedDevices(); // 更新设备列表 connectedDevices.value = devices; - // 更新当前设备 - if (connectedDevices.isNotEmpty && currentDevice.value == null) { - currentDevice.value = connectedDevices.first; + // 如果列表不为空且当前无选定设备,选择第一个 + if (devices.isNotEmpty && currentDevice.value == null) { + currentDevice.value = devices.first; } - return connectedDevices.isNotEmpty; + return devices.isNotEmpty; } catch (e) { Logger.error('刷新已连接设备失败: $e'); return false; } } - /// 获取已连接的耳机设备 - Future> getConnectedHeadsetDevices() async { + /// 获取已连接的蓝牙设备 + Future> getConnectedDevices() async { try { // 确保服务已初始化 if (!_isInitialized && !(await initialize())) { return []; } - final result = await _channel.invokeMethod('getConnectedHeadsetDevices'); + final result = await _methodChannel.invokeMethod('getConnectedDevices'); if (result is List) { - return result - .map((device) => BluetoothDevice( - name: device['name'] ?? '未知设备', - address: device['address'] ?? '', - type: 'headset')) - .toList(); + return result.map((device) => BluetoothDevice.fromMap(device)).toList(); } return []; } catch (e) { - Logger.error('获取已连接的耳机设备失败: $e'); + Logger.error('获取已连接设备失败: $e'); return []; } } - /// 设置蓝牙状态监听 - void _setupBluetoothStateListener() { - _bluetoothStateListener?.cancel(); - - _bluetoothStateListener = - _eventChannel.receiveBroadcastStream().listen((event) { - if (event is Map) { - Logger.info('蓝牙状态监听: $event'); - final type = event['type'] as String?; - - if (type == 'bluetoothStateChanged') { - final state = event['state'] as String?; - if (state != null) { - _handleBluetoothStateChange(state); - } - } else if (type == 'deviceConnected') { - _handleDeviceConnected(event); - } else if (type == 'deviceDisconnected') { - _handleDeviceDisconnected(event); - } - } - }, onError: (error) { - Logger.error('蓝牙状态监听错误: $error'); - }); + /// 检查蓝牙是否已开启 + Future checkBluetoothEnabled() async { + try { + return await _methodChannel.invokeMethod('isBluetoothEnabled') ?? false; + } catch (e) { + Logger.error('检查蓝牙状态失败: $e'); + return false; + } } /// 处理设备连接事件 - void _handleDeviceConnected(Map event) { - final deviceMap = event['device'] as Map?; - if (deviceMap != null) { - final device = BluetoothDevice( - name: deviceMap['name'] ?? '未知设备', - address: deviceMap['address'] ?? '', - type: deviceMap['type'] ?? 'unknown'); + void _handleDeviceConnected(Map deviceMap) { + final device = BluetoothDevice.fromMap(deviceMap); - // 更新设备列表 - if (!connectedDevices.any((d) => d.address == device.address)) { - connectedDevices.add(device); - } + // 更新设备列表 + if (!connectedDevices.any((d) => d.address == device.address)) { + connectedDevices.add(device); + } - // 更新当前设备 - currentDevice.value = device; + // 更新当前设备 + currentDevice.value = device; - Logger.info('设备已连接: ${device.name}'); - } + Logger.info('设备已连接: ${device.name}'); } /// 处理设备断开事件 - void _handleDeviceDisconnected(Map event) { - final deviceMap = event['device'] as Map?; - if (deviceMap != null) { - final address = deviceMap['address'] as String?; - - if (address != null) { - // 更新设备列表 - connectedDevices.removeWhere((d) => d.address == address); - - // 如果是当前设备,清除当前设备 - if (currentDevice.value?.address == address) { - currentDevice.value = - connectedDevices.isEmpty ? null : connectedDevices.first; - Logger.info('当前设备已断开连接'); - } + void _handleDeviceDisconnected(Map deviceMap) { + final address = deviceMap['address'] as String?; + + if (address != null) { + // 更新设备列表 + connectedDevices.removeWhere((d) => d.address == address); + + // 如果是当前设备,清除当前设备 + if (currentDevice.value?.address == address) { + currentDevice.value = connectedDevices.isEmpty ? null : connectedDevices.first; + Logger.info('当前设备已断开连接'); } } } /// 处理蓝牙状态变化 void _handleBluetoothStateChange(String state) { - Logger.info('----蓝牙状态变化: $state'); + Logger.info('蓝牙状态变化: $state'); // 更新蓝牙状态 bluetoothState.value = state; @@ -227,7 +227,7 @@ class ClassicBluetoothService extends GetxService { connectedDevices.clear(); currentDevice.value = null; } else if (state == 'on') { - // 蓝牙重新打开,延迟一小段时间后刷新设备 + // 蓝牙重新开启,延迟一小段时间后刷新设备 Future.delayed(const Duration(milliseconds: 1000), () { refreshConnectedDevices(); }); @@ -236,8 +236,8 @@ class ClassicBluetoothService extends GetxService { @override void onClose() { - // 取消蓝牙状态监听 - _bluetoothStateListener?.cancel(); + // 取消事件监听 + _eventSubscription?.cancel(); super.onClose(); } } diff --git a/lib/modules/home/controllers/home_controller.dart b/lib/modules/home/controllers/home_controller.dart index cfed9c72f..db2049bc9 100644 --- a/lib/modules/home/controllers/home_controller.dart +++ b/lib/modules/home/controllers/home_controller.dart @@ -2,7 +2,6 @@ import 'package:get/get.dart'; import 'dart:async'; import '../../../routes/app_routes.dart'; import '../../../data/services/classic_bluetooth_service.dart'; -import '../../../data/services/bluetooth_media_button_service.dart'; import '../../../core/utils/logger.dart'; import 'package:flutter/material.dart'; import '../../explore/controllers/explore_controller.dart'; @@ -20,17 +19,7 @@ class HomeController extends GetxController { final ExploreController exploreController = Get.find(); // 经典蓝牙服务 - late ClassicBluetoothService _bluetoothService; - - // 蓝牙媒体按钮服务 - late BluetoothMediaButtonService _mediaButtonService; - - // 媒体按钮订阅 - StreamSubscription? _mediaButtonSubscription; - - // 设备连接状态 - final _isConnected = false.obs; - bool get isConnected => _isConnected.value; + final ClassicBluetoothService _bluetoothService = Get.find(); // 连接状态 final _connectionStatus = ConnectionStatus.disconnected.obs; @@ -43,201 +32,120 @@ class HomeController extends GetxController { // 固件版本(模拟数据) final String firmwareVersion = 'v2.4.1'; - // 蓝牙设备信息 - final _deviceInfo = {}.obs; - Map get deviceInfo => _deviceInfo.value; - // 连接错误信息 final _connectionErrorMsg = ''.obs; String get connectionErrorMsg => _connectionErrorMsg.value; - // 当前连接的经典蓝牙设备 - List get connectedClassicDevices => - _bluetoothService.connectedDevices; - - // 蓝牙状态 + // 从蓝牙服务中直接获取的属性 + Rx get currentDevice => _bluetoothService.currentDevice; + List get connectedDevices => _bluetoothService.connectedDevices; RxString get bluetoothState => _bluetoothService.bluetoothState; - - // 状态更新标志,防止多次重复更新 - bool _isUpdatingStatus = false; + + // 蓝牙是否已连接 + bool get isConnected => currentDevice.value != null; @override void onInit() { super.onInit(); - // 初始化蓝牙服务和状态监控 - _initBluetoothService(); - - // 初始化蓝牙媒体按钮服务 - // _initMediaButtonService(); - - // 电池模拟更新(实际应用中可以通过蓝牙获取) + // 初始化 + _initBluetooth(); + + // 电池模拟更新 _simulateBatteryUpdate(); } - // 初始化蓝牙服务 - void _initBluetoothService() { + // 初始化蓝牙 + Future _initBluetooth() async { try { - // 获取经典蓝牙服务实例 - _bluetoothService = Get.find(); - - // 更新当前连接状态 - _updateConnectionStatus(); + // 确保蓝牙服务已初始化 + if (!(await _bluetoothService.initialize())) { + _connectionStatus.value = ConnectionStatus.failed; + _connectionErrorMsg.value = '初始化蓝牙服务失败'; + return; + } - // 监听蓝牙连接状态变化 + // 设置监听器 _setupBluetoothListeners(); + + // 更新初始状态 + _updateConnectionStatus(); + } catch (e) { - Logger.error('初始化蓝牙服务失败: $e'); + Logger.error('初始化蓝牙失败: $e'); _connectionStatus.value = ConnectionStatus.failed; + _connectionErrorMsg.value = e.toString(); } } - // 初始化蓝牙媒体按钮服务 - void _initMediaButtonService() { - try { - // 获取媒体按钮服务实例 - _mediaButtonService = Get.find(); - - // 初始化媒体按钮服务 - _mediaButtonService.initialize().then((success) { - if (success) { - // 启动按钮监听 - _mediaButtonService.startButtonListening(); - - // 设置按钮事件监听 - _setupMediaButtonListener(); - - Logger.info('蓝牙媒体按钮服务初始化成功'); - } else { - Logger.warning('蓝牙媒体按钮服务初始化失败'); - } - }); - } catch (e) { - Logger.error('初始化蓝牙媒体按钮服务失败: $e'); - } - } - - // 设置媒体按钮事件监听 - void _setupMediaButtonListener() { - // 取消之前的订阅 - _mediaButtonSubscription?.cancel(); - - // 设置新的订阅 - _mediaButtonSubscription = _mediaButtonService.buttonEvents.listen((event) { - Logger.info('收到媒体按钮事件: $event'); - - // 处理按钮事件 - if (event.buttonType == MediaButtonType.playPause) {} + // 设置蓝牙状态监听 + void _setupBluetoothListeners() { + // 监听当前设备变化 + ever(currentDevice, (_) { + _updateConnectionStatus(); }); - } - - // 电池电量模拟更新 - void _simulateBatteryUpdate() { - // 每5分钟更新一次电池电量(模拟) - Future.delayed(const Duration(seconds: 2), () { - if (isConnected) { - final timestamp = DateTime.now().millisecondsSinceEpoch; - _deviceBattery.value = 50 + (timestamp % 50); // 50-99的随机值 - } else { - _deviceBattery.value = null; - } - // 递归调用实现定时更新 - _simulateBatteryUpdate(); + // 监听蓝牙状态变化 + ever(bluetoothState, (_) { + _updateConnectionStatus(); }); } // 更新连接状态 void _updateConnectionStatus() { - // 如果正在更新,则跳过 - if (_isUpdatingStatus) return; - - _isUpdatingStatus = true; - - try { - // 检查当前设备 - final currentDevice = _bluetoothService.currentDevice.value; - - if (currentDevice != null) { - _isConnected.value = true; - _connectionStatus.value = ConnectionStatus.connected; - } else { - _isConnected.value = false; - _connectionStatus.value = ConnectionStatus.disconnected; - } - - update(); // 通知UI刷新 - } finally { - _isUpdatingStatus = false; - } - } - - // 设置蓝牙监听器 - void _setupBluetoothListeners() { - // 只监听当前设备和蓝牙状态 - - // 监听当前设备状态 - ever(_bluetoothService.currentDevice, (device) { - if (device != null) { - _isConnected.value = true; - _connectionStatus.value = ConnectionStatus.connected; - } else if (_isConnected.value) { - // 如果之前连接着设备但现在变为null,那么设备已断开 - _isConnected.value = false; - _connectionStatus.value = ConnectionStatus.disconnected; - _deviceBattery.value = null; - } + if (bluetoothState.value == 'off') { + _connectionStatus.value = ConnectionStatus.disconnected; + _deviceBattery.value = null; update(); - }); + return; + } - // 蓝牙状态变化监听 - ever(_bluetoothService.bluetoothState, (state) { - if (state == 'off') { - _isConnected.value = false; - _connectionStatus.value = ConnectionStatus.disconnected; - _deviceBattery.value = null; - update(); - } - }); + if (currentDevice.value != null) { + _connectionStatus.value = ConnectionStatus.connected; + } else { + _connectionStatus.value = ConnectionStatus.disconnected; + _deviceBattery.value = null; + } + + update(); } - // 刷新设备信息 - Future refreshDeviceInfo() async { - return; - // 如果正在更新,则跳过 - if (_isUpdatingStatus) return; - - _isUpdatingStatus = true; - + // 刷新设备连接 + Future refreshDeviceConnection() async { try { - // 设置状态为连接中 _connectionStatus.value = ConnectionStatus.connecting; update(); - - // 刷新设备列表 - final hasDevices = await _bluetoothService.refreshConnectedDevices(); - - // 更新状态 - _updateConnectionStatus(); - - // 根据结果更新状态 - if (!hasDevices) { - Logger.info('刷新设备:未找到设备'); + + final success = await _bluetoothService.refreshConnectedDevices(); + + if (!success) { _connectionErrorMsg.value = '未找到已连接的设备'; } else { _connectionErrorMsg.value = ''; - Logger.info('刷新设备:成功'); } } catch (e) { - Logger.error('刷新设备信息失败: $e'); - _connectionStatus.value = ConnectionStatus.failed; - _connectionErrorMsg.value = 'failedDeviceInformation'.tr; // 无法获取设备信息 - update(); + Logger.error('刷新设备连接失败: $e'); + _connectionErrorMsg.value = '无法刷新设备连接'; } finally { - _isUpdatingStatus = false; + _updateConnectionStatus(); } } + // 电池电量模拟更新 + void _simulateBatteryUpdate() { + Future.delayed(const Duration(seconds: 2), () { + if (isConnected) { + final timestamp = DateTime.now().millisecondsSinceEpoch; + _deviceBattery.value = 50 + (timestamp % 50); // 50-99的随机值 + } else { + _deviceBattery.value = null; + } + + // 递归调用实现定时更新 + _simulateBatteryUpdate(); + }); + } + // 重新扫描设备 void rescanDevices() { Get.toNamed(Routes.pairing); @@ -255,7 +163,6 @@ class HomeController extends GetxController { // 展示AI功能 void exploreAIFeatures() { - // 简单实现,直接跳转到AI聊天页面 Get.toNamed(Routes.chat); } @@ -277,7 +184,7 @@ class HomeController extends GetxController { // 获取连接状态对应的文本 String getConnectionStatusText() { // 如果蓝牙关闭,优先显示蓝牙状态 - if (_bluetoothService.bluetoothState.value == 'off') { + if (bluetoothState.value == 'off') { return 'bluetoothOff'.tr; // 蓝牙已关闭 } @@ -296,13 +203,12 @@ class HomeController extends GetxController { // 获取设备名称 String getDeviceName() { - final device = _bluetoothService.currentDevice.value; - return device?.name ?? 'unknownEquipment'.tr; // 未知设备 + return currentDevice.value?.name ?? 'unknownEquipment'.tr; // 未知设备 } // 获取信号强度(模拟数据) String getSignalStrength() { - if (!_isConnected.value) { + if (!isConnected) { return 'zeroSignal'.tr; // 无信号 } @@ -320,15 +226,6 @@ class HomeController extends GetxController { // 获取设备ID String getDeviceId() { - final device = _bluetoothService.currentDevice.value; - return device?.address ?? 'unknown'.tr; // 未知 - } - - @override - void onClose() { - // 取消按钮事件流订阅 - _mediaButtonSubscription?.cancel(); - - super.onClose(); + return currentDevice.value?.address ?? 'unknown'.tr; // 未知 } } diff --git a/lib/modules/home/views/home_view.dart b/lib/modules/home/views/home_view.dart index 7e0ebfe87..fee687b34 100644 --- a/lib/modules/home/views/home_view.dart +++ b/lib/modules/home/views/home_view.dart @@ -244,7 +244,7 @@ class HomeView extends GetView { // 添加刷新按钮 SizedBox(width: 8.w), InkWell( - onTap: controller.refreshDeviceInfo, + onTap: () => controller.refreshDeviceConnection(), child: Icon( Icons.refresh, size: 16.sp, diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index bcb4a1fd6..a8c1798ca 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -1,75 +1,143 @@ import 'package:app_settings/app_settings.dart'; import 'package:get/get.dart'; import 'dart:async'; +import 'package:permission_handler/permission_handler.dart'; import '../../../routes/app_routes.dart'; import '../../../data/services/classic_bluetooth_service.dart'; import '../../../core/utils/logger.dart'; import 'dart:io'; -import '../../../data/services/voice_interaction_service.dart'; -/// 简化的配对控制器,仅用于显示已连接经典蓝牙设备和处理基本配对流程 + +/// 配对控制器,负责显示已连接经典蓝牙设备和处理基本配对流程 class PairingController extends GetxController { static PairingController get to => Get.find(); - - // 经典蓝牙服务 - final ClassicBluetoothService _bluetoothService = - Get.find(); - - // 是否开始搜索 - final _isSearching = false.obs; - bool get isSearching => _isSearching.value; + final ClassicBluetoothService _bluetoothService = Get.find(); - // 设备连接状态 - final _isConnecting = false.obs; - bool get isConnecting => _isConnecting.value; + // 状态标志 + final isLoading = false.obs; + + // 权限是否已授予 + final _hasPermissions = false.obs; + bool get hasPermissions => _hasPermissions.value; // 蓝牙服务是否可用 final _isBluetoothServiceAvailable = true.obs; bool get isBluetoothServiceAvailable => _isBluetoothServiceAvailable.value; - // 当前连接设备 - Rx get currentDevice => _bluetoothService.currentDevice; - - // 连接设备列表 - RxList get connectedDevices => - _bluetoothService.connectedDevices; - - // 蓝牙状态 - RxString get bluetoothState => _bluetoothService.bluetoothState; - - // 错误信息 - final errorMessage = ''.obs; - // 状态描述(用于UI显示) - final statusDescription = 'initializing'.tr.obs; // 正在初始化... - - // 加载状态 - final isLoading = false.obs; + final statusDescription = 'initializing'.tr.obs; // 是否正在跳转到主页 bool _isNavigatingToHome = false; + // 当前连接设备和设备列表(直接从服务获取) + Rx get currentDevice => _bluetoothService.currentDevice; + RxList get connectedDevices => _bluetoothService.connectedDevices; + RxString get bluetoothState => _bluetoothService.bluetoothState; + @override void onInit() { super.onInit(); _initialize(); } - /// 设置状态变化监听 - void _setupStatusListeners() { - if (!_isBluetoothServiceAvailable.value) return; + /// 初始化流程 + Future _initialize() async { + try { + isLoading.value = true; + statusDescription.value = 'checkingPermissions'.tr; - // 监听蓝牙开关状态 - ever(_bluetoothService.bluetoothState, (state) { - _updateStatusDescription(); + // 检查并请求权限 + if (!await _checkAndRequestPermissions()) { + statusDescription.value = 'permissionDenied'.tr; + return; + } + + statusDescription.value = 'initializing'.tr; + + // 初始化蓝牙服务 + if (!await _bluetoothService.initialize()) { + Logger.error('蓝牙服务初始化失败'); + statusDescription.value = 'bluetoothInitializationFailure'.tr; + _isBluetoothServiceAvailable.value = false; + return; + } + + // 设置状态监听 + _setupStatusListeners(); + + // 刷新已连接设备 + statusDescription.value = 'searchingDevice'.tr; + await refreshConnectedDevices(); + } catch (e) { + Logger.error('初始化配对控制器失败: $e'); + statusDescription.value = 'initializationFailure'.tr; + _isBluetoothServiceAvailable.value = false; + } finally { + isLoading.value = false; + if (hasPermissions) { + _updateStatusDescription(); + } + } + } + + /// 检查并请求必要的蓝牙权限 + Future _checkAndRequestPermissions() async { + Map statuses; + List permissionsToRequest = []; + + if (Platform.isAndroid) { + // Android 12+ (SDK 31+) 需要 SCAN 和 CONNECT + // Android 6+ (SDK 23+) 需要 LOCATION + permissionsToRequest.add(Permission.locationWhenInUse); // 基本定位权限 + // Android 12及以上版本才需要请求蓝牙扫描和连接权限 + if (await _getAndroidSdkVersion() >= 31) { + permissionsToRequest.add(Permission.bluetoothScan); + permissionsToRequest.add(Permission.bluetoothConnect); + } + } else if (Platform.isIOS) { + permissionsToRequest.add(Permission.bluetooth); + } + + statuses = await permissionsToRequest.request(); + + bool allGranted = true; + statuses.forEach((permission, status) { + if (!status.isGranted) { + allGranted = false; + Logger.warning('权限 ${permission.toString()} 未被授予: $status'); + } }); + + _hasPermissions.value = allGranted; + return allGranted; + } + + // 辅助方法获取Android SDK版本 + Future _getAndroidSdkVersion() async { + if (Platform.isAndroid) { + // 这里需要原生代码支持来获取确切的SDK版本, + // 或者使用 device_info_plus 插件 + // 暂时返回一个较高值以触发新权限请求,如果需要精确控制则需要引入插件 + // 或者,如果你的minSdkVersion >= 31,可以直接请求新权限 + // 假设minSdkVersion < 31,我们需要一种方式判断当前运行设备的SDK版本 + // 为了简化,我们这里先假设需要请求新权限,如果你的目标SDK较低,需要调整 + return 31; // 假设运行在需要新权限的设备上 + } + return 0; + } - // 监听当前设备状态 - ever(_bluetoothService.currentDevice, (device) { - _updateStatusDescription(); + /// 设置状态变化监听 + void _setupStatusListeners() { + // 监听蓝牙状态变化 + ever(bluetoothState, (_) => _updateStatusDescription()); - // 如果有设备连接,自动跳转到主页 + // 监听当前设备变化 + ever(currentDevice, (device) { + _updateStatusDescription(); + + // 当设备连接时自动跳转 if (device != null) { _autoNavigateToHome(); } @@ -78,104 +146,66 @@ class PairingController extends GetxController { /// 更新状态描述 void _updateStatusDescription() { - if (_bluetoothService.bluetoothState.value == 'off') { - statusDescription.value = 'bluetoothTurnedOff'.tr;// 蓝牙已关闭, 请打开蓝牙 + if (!hasPermissions) { + statusDescription.value = 'permissionDenied'.tr; + return; + } + + if (bluetoothState.value == 'off') { + statusDescription.value = 'bluetoothTurnedOff'.tr; return; } - if (_bluetoothService.currentDevice.value != null) { - statusDescription.value = - '${'connectedDevice'.tr} ${_bluetoothService.currentDevice.value!.name}';// 已连接到设备: + if (currentDevice.value != null) { + statusDescription.value = '${'connectedDevice'.tr} ${currentDevice.value!.name}'; return; } - if (_bluetoothService.connectedDevices.isNotEmpty) { - statusDescription.value = - '${'findConnectedDevice'.tr} ${_bluetoothService.connectedDevices.first.name}';// 找到已连接的设备: + if (connectedDevices.isNotEmpty) { + statusDescription.value = '${'findConnectedDevice'.tr} ${connectedDevices.first.name}'; return; } if (isLoading.value) { - statusDescription.value = 'searchingDevice'.tr; // 正在搜索设备...; + statusDescription.value = 'searchingDevice'.tr; return; } - statusDescription.value = 'notFound'.tr;//未找到已连接的蓝牙设备 - } - - /// 初始化流程 - Future _initialize() async { - try { - - - isLoading.value = true; - statusDescription.value = 'initializing'.tr; // 正在初始化...; - - // 确保蓝牙服务已初始化 - if (!await _bluetoothService.initialize()) { - Logger.error('蓝牙服务初始化失败'); - statusDescription.value = - 'bluetoothInitializationFailure'.tr; // 蓝牙服务初始化失败 - _isBluetoothServiceAvailable.value = false; - isLoading.value = false; - return; - } - _setupStatusListeners(); - // 检查权限 - if (Platform.isAndroid) { - statusDescription.value = 'checkingPermissions'.tr; - final hasPermission = await _bluetoothService.checkPermissions(); - if (!hasPermission) { - statusDescription.value = '缺少蓝牙权限'; - isLoading.value = false; - return; - } - } - - // 刷新设备列表 - statusDescription.value = '正在搜索设备...'; - await refreshConnectedDevices(); - - // 更新状态描述 - _updateStatusDescription(); - } catch (e) { - Logger.error('初始化配对控制器失败: $e'); - statusDescription.value = 'initializationFailure'.tr; // 初始化失败 - _isBluetoothServiceAvailable.value = false; - } finally { - isLoading.value = false; - } + statusDescription.value = 'notFound'.tr; } - /// 刷新设备 + /// 刷新已连接设备 Future refreshConnectedDevices() async { + if (!_isBluetoothServiceAvailable.value) return; + + if (!await _checkAndRequestPermissions()) { + statusDescription.value = 'permissionDenied'.tr; + return; + } + try { isLoading.value = true; - statusDescription.value = 'searchingDevice'.tr; // 正在搜索设备... - - final hasDevices = await _bluetoothService.refreshConnectedDevices(); - - // 更新状态描述 - _updateStatusDescription(); + statusDescription.value = 'searchingDevice'.tr; + + await _bluetoothService.refreshConnectedDevices(); } catch (e) { Logger.error('刷新设备失败: $e'); - statusDescription.value = 'failedRefreshDevice'.tr; // 刷新设备失败 + statusDescription.value = 'failedRefreshDevice'.tr; } finally { isLoading.value = false; + if (hasPermissions) { + _updateStatusDescription(); + } } } /// 自动跳转到主页 void _autoNavigateToHome() { - // 如果已经在跳转或设备列表为空,则不执行 - if (_isNavigatingToHome || _bluetoothService.connectedDevices.isEmpty) - return; - - // 设置跳转标志 + if (_isNavigatingToHome || currentDevice.value == null) return; + _isNavigatingToHome = true; - - Logger.info('已找到连接的设备,自动跳转到主页'); - + Logger.info('已找到连接的设备 ${currentDevice.value?.name},自动跳转到主页'); + Future.delayed(const Duration(milliseconds: 500), () { Get.offAllNamed(Routes.home); }); @@ -184,21 +214,27 @@ class PairingController extends GetxController { /// 打开系统蓝牙设置 void openBluetoothSettings() { try { - // 使用app_settings插件打开蓝牙设置 - AppSettings.openAppSettings(type: AppSettingsType.bluetooth); + if (!hasPermissions) { + openAppSettings(); + } else if (Platform.isAndroid && bluetoothState.value == 'off') { + AppSettings.openAppSettings(type: AppSettingsType.bluetooth); + } else if (Platform.isIOS) { + openAppSettings(); + } else { + AppSettings.openAppSettings(type: AppSettingsType.bluetooth); + } } catch (e) { - Logger.error('打开蓝牙设置失败: $e'); + Logger.error('打开设置失败: $e'); } } - - /// 刷新设备(兼容旧代码) - Future refreshDevices() async { - await refreshConnectedDevices(); - } + + /// 打开应用设置 + Future openAppSettings() async { + await AppSettings.openAppSettings(); + } /// 跳过连接 void skipConnecting() { - // 设置跳转标志防止自动跳转再次触发 _isNavigatingToHome = true; Get.offAllNamed(Routes.home); } diff --git a/local_plugins/agent_service/android/build.gradle.kts b/local_plugins/agent_service/android/build.gradle.kts index ed613c41b..2bd9f5080 100644 --- a/local_plugins/agent_service/android/build.gradle.kts +++ b/local_plugins/agent_service/android/build.gradle.kts @@ -37,6 +37,6 @@ dependencies { // 依赖本地其他插件 add("compileOnly", project(":azure_speech")) add("compileOnly", project(":open_ai_service")) - add("compileOnly", project(":common_utils")) + add("compileOnly", project(":deepvoice_utils")) add("compileOnly", project(":chat_storage")) } \ No newline at end of file diff --git a/local_plugins/agent_service/android/settings.gradle.kts b/local_plugins/agent_service/android/settings.gradle.kts index efdbada26..6595de250 100644 --- a/local_plugins/agent_service/android/settings.gradle.kts +++ b/local_plugins/agent_service/android/settings.gradle.kts @@ -3,11 +3,11 @@ rootProject.name = "agent_service" // 包含项目依赖 include(":azure_speech") include(":open_ai_service") -include(":common_utils") +include(":deepvoice_utils") include(":chat_storage") // 设置项目路径 project(":azure_speech").projectDir = file("../../azure_speech/android") project(":open_ai_service").projectDir = file("../../open_ai_service/android") -project(":common_utils").projectDir = file("../../common_utils/android") +project(":deepvoice_utils").projectDir = file("../../deepvoice_utils/android") project(":chat_storage").projectDir = file("../../chat_storage/android") \ No newline at end of file diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index cc3bc2869..004dd807a 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -484,7 +484,7 @@ class AgentService( * 保存聊天消息 */ private fun saveChatMessage(userMessage: String, assistantMessage: String) { - CoroutineScope(Dispatchers.IO).launch { + launch(Dispatchers.IO) { try { // 保存用户消息 val userMessageId = chatStorageHelper.saveMessage( @@ -519,7 +519,7 @@ class AgentService( * 清除聊天历史 */ fun clearChatHistory(callback: (Boolean) -> Unit) { - CoroutineScope(Dispatchers.IO).launch { + launch(Dispatchers.IO) { try { // 清除指定会话的所有消息 val success = chatStorageHelper.deleteMessages(sessionId) diff --git a/local_plugins/agent_service/pubspec.yaml b/local_plugins/agent_service/pubspec.yaml index 867cae4a0..c925f5998 100644 --- a/local_plugins/agent_service/pubspec.yaml +++ b/local_plugins/agent_service/pubspec.yaml @@ -15,8 +15,8 @@ dependencies: path: ../open_ai_service azure_speech: path: ../azure_speech - common_utils: - path: ../common_utils + deepvoice_utils: + path: ../deepvoice_utils chat_storage: path: ../chat_storage diff --git a/local_plugins/ble_service/android/build.gradle.kts b/local_plugins/ble_service/android/build.gradle.kts index 4708e1a67..a76679c75 100644 --- a/local_plugins/ble_service/android/build.gradle.kts +++ b/local_plugins/ble_service/android/build.gradle.kts @@ -10,7 +10,7 @@ plugins { android { namespace = "com.yunqiinnovation.ble_service" - compileSdk = 34 + compileSdk = 35 defaultConfig { minSdk = 26 diff --git a/local_plugins/ble_service/android/settings.gradle.kts b/local_plugins/ble_service/android/settings.gradle.kts new file mode 100644 index 000000000..2f97aacd5 --- /dev/null +++ b/local_plugins/ble_service/android/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "ble_service" \ No newline at end of file diff --git a/local_plugins/ble_service/android/src/main/AndroidManifest.xml b/local_plugins/ble_service/android/src/main/AndroidManifest.xml index 461eac927..9b1eec458 100644 --- a/local_plugins/ble_service/android/src/main/AndroidManifest.xml +++ b/local_plugins/ble_service/android/src/main/AndroidManifest.xml @@ -27,7 +27,8 @@ + android:exported="true" + android:enabled="true"> diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt index 8da430404..b592c46e9 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt @@ -8,13 +8,19 @@ import java.util.* object BleConst { // ================== 协议相关UUID ================== /** 主服务UUID - 文档中定义为0000ABC0-0000-1111-2222-123456789ABC */ - val PRIMARY_SERVICE_UUID: UUID = UUID.fromString("0000fd7b-0000-1000-8000-00805f9b34fb") + val PRIMARY_SERVICE_UUID: UUID = UUID.fromString("0000abc0-0000-1111-2222-123456789abc") + + /** 音频服务UUID - 文档中定义为00001801-0000-1000-8000-00805f9b34fb */ + val AUDIO_SERVICE_UUID: UUID = UUID.fromString("0000ae00-0000-1000-8000-00805f9b34fb") + + /** 接收音频特征UUID - 文档中定义为0000ABC1-0000-1111-2222-123456789ABC */ + val RECEIVE_AUDIO_CHAR_UUID: UUID = UUID.fromString("0000ae02-0000-1000-8000-00805f9b34fb") /** 写入特征UUID - 文档中定义为0000ABC1-0000-1111-2222-123456789ABC */ - val WRITE_CHAR_UUID: UUID = UUID.fromString("0000fd7b-0000-1000-8000-00805f9b34fb") + val WRITE_CHAR_UUID: UUID = UUID.fromString("0000abc1-0000-1111-2222-123456789abc") /** 通知特征UUID - 文档中定义为0000ABC2-0000-1111-2222-123456789ABC */ - val NOTIFY_CHAR_UUID: UUID = UUID.fromString("0000fd7b-0000-1000-8000-00805f9b34fb") + val NOTIFY_CHAR_UUID: UUID = UUID.fromString("0000abc2-0000-1111-2222-123456789abc") /** 客户端特征配置描述符UUID */ val CLIENT_CHAR_CONFIG_UUID: UUID = UUID.fromString("0000fd7b-0000-1000-8000-00805f9b34fb") diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleForegroundService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleForegroundService.kt index 5034153d0..88a496745 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleForegroundService.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleForegroundService.kt @@ -1,311 +1,227 @@ package com.yunqiinnovation.ble_service import android.app.* -import android.bluetooth.BluetoothManager import android.content.Intent import android.content.pm.ServiceInfo import android.os.Build import android.os.IBinder -import android.os.Handler -import android.os.Looper import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import com.yunqiinnovation.deepvoice_utils.FileLogger import android.content.Context import android.content.SharedPreferences +import kotlinx.coroutines.* /** * BLE前台服务 - * 1. 在onCreate中启动前台服务并注册后台扫描 - * 2. 在onStartCommand中处理被BleWakeReceiver拉起的重连请求 - * 3. 在onDestroy中清理资源 - * 4. 自动扫描连接上一次连接的设备 - * 5. 响应唤醒命令,自动开启录音功能 + * 职责: + * 1. 维持前台服务状态,确保系统不会杀死应用 + * 2. 作为BLE被唤醒的服务入口 + * 3. 在被唤醒时检查连接状态,如果未连接则使用上次成功的连接重新建立连接 */ @RequiresApi(26) class BleForegroundService : Service(), BleService.Callback { + companion object { + private const val TAG = "BleForegroundService" + private const val RECONNECT_DELAY = 5000L + private const val CHANNEL_ID = "ble_service_channel" + private const val NOTIFICATION_ID = 1001 + } + private lateinit var bleManager: BleManagerWrapper - private val TAG = "BleForegroundService" - private val handler = Handler(Looper.getMainLooper()) - private lateinit var prefs: SharedPreferences - - // 扫描相关 - private var isAutoConnecting = false - private var scanRetryCount = 0 - private val MAX_SCAN_RETRY = 3 - private val SCAN_TIMEOUT = 10000L // 10秒 - private val AUTO_CONNECT_DELAY = 2000L // 2秒 - + // 简化协程作用域:使用SupervisorJob确保一个协程失败不会影响其他协程 + private val serviceScope = CoroutineScope(SupervisorJob()) + override fun onCreate() { super.onCreate() FileLogger.init(this) FileLogger.i(TAG, "BleForegroundService正在创建...") - - // 初始化偏好设置存储 - prefs = getSharedPreferences("ble_service_prefs", Context.MODE_PRIVATE) - - // 使用全局BleManagerWrapper单例,共享BleService实例 + + // 初始化BLE管理器 bleManager = BleManagerWrapper.instance.apply { init(this@BleForegroundService) - addCallback(this@BleForegroundService) // 注册回调 - } - - // 使用正确的前台服务类型启动 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { // Android 14+ - startForeground( - BleConst.FOREGROUND_ID, - buildNotification("BLE服务运行中..."), - ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE - ) - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // Android 10-13 - startForeground( - BleConst.FOREGROUND_ID, - buildNotification("BLE服务运行中..."), - ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION - ) - } else { // 较旧版本 - startForeground(BleConst.FOREGROUND_ID, buildNotification("BLE服务运行中...")) + addCallback(this@BleForegroundService) } - - // 启动自动连接流程 - startAutoConnect() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - // 若由Receiver带MAC拉起,则立即重连 - val mac = intent?.getStringExtra("mac") - FileLogger.d(TAG, "onStartCommand: $mac") + FileLogger.i(TAG, "服务onStartCommand: ${intent?.action}") - mac?.let { - FileLogger.i(TAG, "收到连接请求,正在连接设备: $it") - // 保存最后连接的设备地址 - saveLastConnectedDevice(it) - // 连接设备 - bleManager.connect(it) + // 1. 启动前台服务 + startForegroundServiceWithNotification() + + // 2. 注册后台扫描 + registerBackgroundScan() + + // 3. 处理连接请求 + val macAddress = intent?.getStringExtra("mac") + if (!macAddress.isNullOrEmpty()) { + // 检查当前连接状态,如果未连接则发起连接 + serviceScope.launch { + if (!bleManager.isConnected()) { + FileLogger.i(TAG, "开始连接到设备: $macAddress") + connectToDevice(macAddress) + } else { + FileLogger.i(TAG, "设备已连接,忽略连接请求") + } + } } return START_STICKY } + + // 简化协程启动,将注册逻辑提取为独立方法 + private fun registerBackgroundScan() { + serviceScope.launch { + try { + bleManager.registerBackgroundScan() + FileLogger.i(TAG, "已注册后台扫描") + } catch (e: Exception) { + FileLogger.e(TAG, "注册后台扫描失败", e) + } + } + } override fun onBind(i: Intent?): IBinder? = null override fun onDestroy() { FileLogger.i(TAG, "BleForegroundService正在销毁...") - // 移除回调 - bleManager.removeCallback(this) - // 取消所有待处理的操作 - handler.removeCallbacksAndMessages(null) + + // 清理BLE资源 + cleanupBleResources() + + // 取消所有协程 + serviceScope.cancel() + super.onDestroy() } - /** - * 启动自动连接流程 - */ - private fun startAutoConnect() { - // 如果已经连接则不需要自动连接 - if (bleManager.isConnected()) { - FileLogger.i(TAG, "已连接到设备,无需自动连接") - return - } - - // 获取上次连接的设备 - val lastDevice = getLastConnectedDevice() - if (lastDevice.isNullOrEmpty()) { - FileLogger.i(TAG, "无上次连接设备记录,开始扫描新设备") - startScanForDevices() - return - } - - // 尝试连接上次的设备 - FileLogger.i(TAG, "尝试连接上次设备: $lastDevice") - bleManager.connect(lastDevice) - - // 设置连接超时,如果超时则开始扫描 - handler.postDelayed({ - if (!bleManager.isConnected()) { - FileLogger.i(TAG, "连接上次设备超时,开始扫描新设备") - startScanForDevices() + // 封装资源清理逻辑 + private fun cleanupBleResources() { + serviceScope.launch { + try { + // 关闭蓝牙连接 + if (bleManager.isConnected()) { + bleManager.disconnect() + } + // 清理资源 + bleManager.cleanup() + FileLogger.i(TAG, "已清理BLE资源") + } catch (e: Exception) { + FileLogger.e(TAG, "清理BLE资源失败", e) + } finally { + // 移除回调 + bleManager.removeCallback(this@BleForegroundService) } - }, AUTO_CONNECT_DELAY) + } } - + /** - * 开始扫描设备 + * 启动前台服务 */ - private fun startScanForDevices() { - if (isAutoConnecting) return - - isAutoConnecting = true - scanRetryCount = 0 + private fun startForegroundServiceWithNotification() { + val notification = createNotification() - FileLogger.i(TAG, "开始扫描BLE设备...") - bleManager.startScan(SCAN_TIMEOUT) + // 根据Android版本使用正确的前台服务类型 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { // Android 14+ + startForeground( + NOTIFICATION_ID, + notification, + ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE + ) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // Android 10-13 + startForeground( + NOTIFICATION_ID, + notification, + ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION + ) + } else { // 较旧版本 + startForeground(NOTIFICATION_ID, notification) + } - // 设置扫描超时 - handler.postDelayed({ - if (isAutoConnecting && scanRetryCount < MAX_SCAN_RETRY) { - scanRetryCount++ - FileLogger.i(TAG, "扫描超时,重试 $scanRetryCount/$MAX_SCAN_RETRY") - bleManager.startScan(SCAN_TIMEOUT) - } else if (isAutoConnecting) { - FileLogger.i(TAG, "已达最大重试次数,停止自动连接") - isAutoConnecting = false - } - }, SCAN_TIMEOUT + 1000) // 比扫描超时多1秒 + FileLogger.i(TAG, "已启动前台服务") } /** - * 保存最后连接的设备 + * 创建通知 */ - private fun saveLastConnectedDevice(address: String) { - prefs.edit().putString("last_connected_device", address).apply() - } - - /** - * 获取最后连接的设备 - */ - private fun getLastConnectedDevice(): String? { - return prefs.getString("last_connected_device", null) - } + private fun createNotification(): Notification { + val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - /** 构建低优先级通知,满足5秒前台规则 */ - private fun buildNotification(msg: String): Notification { - val nm = getSystemService(NotificationManager::class.java) - if (nm.getNotificationChannel(BleConst.CHANNEL_ID) == null) { - nm.createNotificationChannel( - NotificationChannel( - BleConst.CHANNEL_ID, "BLE前台", NotificationManager.IMPORTANCE_LOW - ) + // Android 8.0+ 需要创建通知渠道 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + CHANNEL_ID, + "蓝牙连接服务", + NotificationManager.IMPORTANCE_LOW ) + manager.createNotificationChannel(channel) } - return NotificationCompat.Builder(this, BleConst.CHANNEL_ID) - .setSmallIcon(android.R.drawable.ic_dialog_info) + + // 点击通知打开MainActivity + val intent = packageManager.getLaunchIntentForPackage(packageName)?.apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + } + val pendingIntent = if (intent != null) { + PendingIntent.getActivity( + this, + 0, + intent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + } else { + null + } + + return NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle("DeepSound") - .setContentText(msg) + .setContentText("蓝牙连接服务正在后台运行") + .setSmallIcon(android.R.drawable.ic_dialog_info) + .apply { pendingIntent?.let { setContentIntent(it) } } .build() } - + /** - * 更新通知内容 + * 连接到指定设备 */ - private fun updateNotification(msg: String) { - val nm = getSystemService(NotificationManager::class.java) - nm.notify(BleConst.FOREGROUND_ID, buildNotification(msg)) - } - - // ============================== BleService.Callback 接口实现 ============================== - - override fun onScanResult(devices: List>) { - if (!isAutoConnecting || devices.isEmpty()) return - - // 自动连接第一个找到的设备 - val device = devices.firstOrNull() - device?.let { - val address = it["id"] as String - val name = it["name"] as String - - FileLogger.i(TAG, "扫描到设备: $name ($address),正在自动连接...") - saveLastConnectedDevice(address) - bleManager.connect(address) - - // 停止扫描 - bleManager.stopScan() - - // 延迟一段时间检查连接状态 - handler.postDelayed({ - if (!bleManager.isConnected()) { - FileLogger.i(TAG, "自动连接失败,继续扫描...") - if (scanRetryCount < MAX_SCAN_RETRY) { - startScanForDevices() - } else { - FileLogger.i(TAG, "已达最大重试次数,停止自动连接") - isAutoConnecting = false - } - } - }, AUTO_CONNECT_DELAY) + private fun connectToDevice(address: String) { + FileLogger.i(TAG, "开始连接设备: ${address.takeLast(4)}...") + + serviceScope.launch { + try { + bleManager.connect(address) + FileLogger.i(TAG, "设备连接成功: ${address.takeLast(4)}") + } catch (e: Exception) { + FileLogger.e(TAG, "连接设备失败", e) + } } } - + + // ============================== BleService.Callback 接口实现 ============================== + override fun onConnectionStateChanged(state: Int) { when (state) { BleConst.STATE_CONNECTED -> { FileLogger.i(TAG, "设备已连接") - updateNotification("已连接到BLE设备") - isAutoConnecting = false - - // 获取设备信息 - // bleManager.getDeviceVersion() - // bleManager.getBatteryInfo() } BleConst.STATE_DISCONNECTED -> { FileLogger.i(TAG, "设备已断开") - updateNotification("BLE设备已断开") - - // 如果不是主动断开,尝试重新连接 - // if (!isAutoConnecting) { - // handler.postDelayed({ - // startAutoConnect() - // }, 3000) // 延迟3秒重连 - // } } BleConst.STATE_ERROR -> { FileLogger.e(TAG, "设备连接错误") - updateNotification("BLE设备连接错误") - - // 连接错误,尝试重新扫描连接 - // if (!isAutoConnecting) { - // handler.postDelayed({ - // startAutoConnect() - // }, 5000) // 延迟5秒重连 - // } } } } - + override fun onWakeupSignalReceived() { - FileLogger.i(TAG, "收到唤醒信号,开启设备录音功能") - - - // 更新通知 - updateNotification("已唤醒,正在录音...") + FileLogger.i(TAG, "收到唤醒信号") } - - override fun onCommandResult(commandType: Int, success: Boolean) { - FileLogger.i(TAG, "命令结果:类型=$commandType, 成功=$success") - } - - override fun onProtocolCommandResult(commandType: Int, data: ByteArray?, success: Boolean) { - when (commandType) { - BleConst.CMD_CONTROL_CODEC -> { - if (success) { - FileLogger.i(TAG, "编解码控制命令执行成功") - } else { - FileLogger.e(TAG, "编解码控制命令执行失败") - } - } - } - } - - override fun onDeviceInfoReceived(infoType: Int, infoData: String) { - when (infoType) { - BleConst.CMD_GET_VERSION -> { - FileLogger.i(TAG, "设备版本: $infoData") - updateNotification("已连接:版本 $infoData") - } - BleConst.CMD_GET_BATTERY_INFO -> { - val parts = infoData.split("|") - val batteryLevel = parts[0] - val isCharging = parts.getOrNull(1) == "1" - - FileLogger.i(TAG, "电池电量: $batteryLevel%, 充电中: $isCharging") - updateNotification("已连接:电量 $batteryLevel%${if (isCharging) "(充电中)" else ""}") - } - } - } - + // 以下是必须实现但当前服务不需要处理的回调方法 - override fun onDataReceived(data: ByteArray) {} + override fun onScanResult(devices: List>) {} override fun onAudioDataReceived(data: ByteArray) {} + override fun onProtocolCommandResult(commandType: Int, data: ByteArray?, success: Boolean) {} + override fun onDeviceInfoReceived(infoType: Int, infoData: String) {} override fun onStatusUpdate(status: Map) {} -} \ No newline at end of file +} \ No newline at end of file diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleManagerWrapper.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleManagerWrapper.kt index 3129bf60d..e0f5b04b2 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleManagerWrapper.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleManagerWrapper.kt @@ -51,15 +51,6 @@ class BleManagerWrapper private constructor() { } } - override fun onDataReceived(data: ByteArray) { - for (callback in registeredCallbacks) { - try { - callback.onDataReceived(data) - } catch (e: Exception) { - FileLogger.e(TAG, "分发数据回调异常", e) - } - } - } override fun onAudioDataReceived(data: ByteArray) { for (callback in registeredCallbacks) { @@ -80,16 +71,7 @@ class BleManagerWrapper private constructor() { } } } - - override fun onCommandResult(commandType: Int, success: Boolean) { - for (callback in registeredCallbacks) { - try { - callback.onCommandResult(commandType, success) - } catch (e: Exception) { - FileLogger.e(TAG, "分发命令结果回调异常", e) - } - } - } + override fun onProtocolCommandResult(commandType: Int, data: ByteArray?, success: Boolean) { for (callback in registeredCallbacks) { @@ -136,7 +118,7 @@ class BleManagerWrapper private constructor() { FileLogger.d(TAG, "BLE服务初始化成功") } // 注册后台扫描,确保可被唤醒 - bleService?.registerBackgroundScan() + // bleService?.registerBackgroundScan() } catch (e: Exception) { FileLogger.e(TAG, "BLE服务初始化失败", e) @@ -178,6 +160,36 @@ class BleManagerWrapper private constructor() { fun startScan(timeout: Long = 10000): Boolean = bleService?.startScan(timeout) ?: false fun stopScan(): Boolean = bleService?.stopScan() ?: false + /** + * 注册后台持久扫描,APP被杀死后也能唤醒 + * @return 是否注册成功 + */ + fun registerBackgroundScan(): Boolean { + try { + bleService?.registerBackgroundScan() + FileLogger.i(TAG, "注册后台持久扫描") + return true + } catch (e: Exception) { + FileLogger.e(TAG, "注册后台持久扫描失败: ${e.message}") + return false + } + } + + /** + * 注销后台持久扫描 + * @return 是否注销成功 + */ + fun unregisterBackgroundScan(): Boolean { + try { + bleService?.unregisterBackgroundScan() + FileLogger.i(TAG, "注销后台持久扫描") + return true + } catch (e: Exception) { + FileLogger.e(TAG, "注销后台持久扫描失败: ${e.message}") + return false + } + } + // 连接相关 fun connect(address: String): Boolean { bleService?.connect(address) @@ -207,12 +219,6 @@ class BleManagerWrapper private constructor() { fun volumeUp(): Boolean = bleService?.volumeUp() ?: false fun volumeDown(): Boolean = bleService?.volumeDown() ?: false - // 数据传输 - fun sendAudioData(data: ByteArray?): Boolean { - if (data == null) return false - return bleService?.sendAudioData(data) ?: false - } - // 清理资源 fun cleanup() { bleService?.cleanup() diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt index 6c2520030..92cabf152 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt @@ -49,12 +49,10 @@ class BleService(private val context: Context) { fun onConnectionStateChanged(state: Int) // 数据相关回调 - fun onDataReceived(data: ByteArray) fun onAudioDataReceived(data: ByteArray) fun onWakeupSignalReceived() // 命令响应相关回调 - fun onCommandResult(commandType: Int, success: Boolean) fun onProtocolCommandResult(commandType: Int, data: ByteArray?, success: Boolean) // 设备信息相关回调 @@ -67,9 +65,6 @@ class BleService(private val context: Context) { // ====================================================================================================== // 成员变量 // ====================================================================================================== - // 日志与数据存储 - private var outputFile: File? = null - private var fos: FileOutputStream? = null // 蓝牙相关组件 private val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager @@ -80,6 +75,7 @@ class BleService(private val context: Context) { private var bluetoothGatt: BluetoothGatt? = null private var notifyChar: BluetoothGattCharacteristic? = null private var writeChar: BluetoothGattCharacteristic? = null + private var audioChar: BluetoothGattCharacteristic? = null // 扫描相关 private val scanHandler = Handler(Looper.getMainLooper()) @@ -114,12 +110,14 @@ class BleService(private val context: Context) { scanResults.clear() val filter = ScanFilter.Builder() - .setServiceUuid(ParcelUuid(BleConst.PRIMARY_SERVICE_UUID)) + // .setServiceUuid(ParcelUuid(BleConst.PRIMARY_SERVICE_UUID)) + .setManufacturerData(0xEFAB, byteArrayOf(0xA0.toByte(), 0x01)) .build() val settings = ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) - .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) // 使用ALL_MATCHES获取所有结果 + // .setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH) + .setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH) // 使用ALL_MATCHES获取所有结果 .setReportDelay(0) // 立即报告结果 .build() @@ -283,6 +281,7 @@ class BleService(private val context: Context) { bluetoothGatt = null notifyChar = null writeChar = null + audioChar = null } } @@ -321,51 +320,72 @@ class BleService(private val context: Context) { return } - val svc = g.getService(BleConst.PRIMARY_SERVICE_UUID) - notifyChar = svc?.getCharacteristic(BleConst.NOTIFY_CHAR_UUID) - writeChar = svc?.getCharacteristic(BleConst.WRITE_CHAR_UUID) + // 获取主服务特征 + val primarySvc = g.getService(BleConst.PRIMARY_SERVICE_UUID) + notifyChar = primarySvc?.getCharacteristic(BleConst.NOTIFY_CHAR_UUID) + writeChar = primarySvc?.getCharacteristic(BleConst.WRITE_CHAR_UUID) + // 获取音频服务特征 + val audioSvc = g.getService(BleConst.AUDIO_SERVICE_UUID) + audioChar = audioSvc?.getCharacteristic(BleConst.RECEIVE_AUDIO_CHAR_UUID) + if (notifyChar == null || writeChar == null) { - FileLogger.e(TAG, "未找到所需特征") + FileLogger.e(TAG, "未找到主服务所需特征") updateConnectionState(BleConst.STATE_ERROR) return } writeChar?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE - setupNotifications(g) + + // 设置主服务的通知 + setupNotifications(g, notifyChar) + + // 设置音频服务的通知(如果存在) + if (audioChar != null) { + setupNotifications(g, audioChar) + FileLogger.i(TAG, "音频服务特征找到并设置通知") + } else { + FileLogger.w(TAG, "音频服务特征未找到") + } } override fun onCharacteristicChanged(g: BluetoothGatt, c: BluetoothGattCharacteristic) { val data = c.value ?: return - // 通用数据回调 - callback?.onDataReceived(data) - - // 判断数据类型 - when { - // OPUS音频帧 - data.isNotEmpty() && (data[0].toInt() and 0xFF == 0x4B || - data[0].toInt() and 0xFF == 0x48) -> { - processOpusData(data) - } - // 设备响应 (0xBB开头) - data.isNotEmpty() && data[0] == BleConst.DEVICE_RESPONSE_HEADER -> { - processDeviceResponse(data) - } - // 设备主动上报 (0xCC开头) - data.isNotEmpty() && data[0] == BleConst.DEVICE_NOTIFY_HEADER -> { - processDeviceNotification(data) + // 根据特征UUID区分处理 + when (c.uuid) { + // 音频特征数据 + BleConst.RECEIVE_AUDIO_CHAR_UUID -> { + processAudioData(data) } - // 唤醒信号 (0x01开头) - data.isNotEmpty() && data[0].toInt() == 0x01 -> { - FileLogger.i(TAG, "收到唤醒信号") - callback?.onWakeupSignalReceived() - // 接收到唤醒信号,打开编码器 - openEncoder() + // 通知特征数据(命令和控制) + BleConst.NOTIFY_CHAR_UUID -> { + // 判断数据类型 + when { + // 设备响应 (0xBB开头) + data.isNotEmpty() && data[0] == BleConst.DEVICE_RESPONSE_HEADER -> { + processDeviceResponse(data) + } + // 设备主动上报 (0xCC开头) + data.isNotEmpty() && data[0] == BleConst.DEVICE_NOTIFY_HEADER -> { + processDeviceNotification(data) + } + // 唤醒信号 (0x01开头) + data.isNotEmpty() && data[0].toInt() == 0x01 -> { + FileLogger.i(TAG, "收到唤醒信号") + callback?.onWakeupSignalReceived() + // 接收到唤醒信号,打开编码器 + openEncoder() + } + // 其他未知数据 + else -> { + FileLogger.d(TAG, "收到未知数据: ${data.joinToString(", ") { "0x%02X".format(it) }}") + } + } } - // 其他未知数据 + // 其他未知特征 else -> { - FileLogger.d(TAG, "收到未知数据: ${data.joinToString(", ") { "0x%02X".format(it) }}") + FileLogger.d(TAG, "收到未知特征数据: ${c.uuid}, 数据=${data.joinToString(", ") { "0x%02X".format(it) }}") } } } @@ -380,174 +400,41 @@ class BleService(private val context: Context) { g: BluetoothGatt, d: BluetoothGattDescriptor, status: Int ) { if (status == BluetoothGatt.GATT_SUCCESS) - FileLogger.i(TAG, "通知描述符写入成功") + FileLogger.i(TAG, "通知描述符写入成功: ${d.characteristic.uuid}") } } /** - * 设置通知功能 + * 设置特征的通知功能 */ - private fun setupNotifications(g: BluetoothGatt) { - if (notifyChar?.properties?.and(BluetoothGattCharacteristic.PROPERTY_NOTIFY) == 0) return - g.setCharacteristicNotification(notifyChar, true) - val desc = notifyChar!!.getDescriptor(BleConst.CLIENT_CHAR_CONFIG_UUID) - desc.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE - g.writeDescriptor(desc) - } - - /** - * 处理OPUS音频数据 - */ - private fun processOpusData(data: ByteArray) { - try { - // 保存原始数据到文件 - fos?.write(data) - - // 判断是否为OPUS格式数据 - val isOpus = data.size > 2 && - (data[0].toInt() and 0xFF == 0x4B || - data[0].toInt() and 0xFF == 0x48) - - if (isOpus) { - // 回调给上层 - callback?.onAudioDataReceived(data) - } else { - FileLogger.w(TAG, "收到非OPUS格式数据") - } - } catch (e: Exception) { - FileLogger.e(TAG, "处理OPUS数据异常: ${e.message}", e) - } - } - - // ====================================================================================================== - // 后台扫描与唤醒 - // ====================================================================================================== - /** - * 注册后台持久扫描,APP被杀死后也能唤醒 - */ - fun registerBackgroundScan() { - val scanner = bluetoothLeScanner ?: return - try { - val filter = ScanFilter.Builder() - .setServiceUuid(ParcelUuid(BleConst.PRIMARY_SERVICE_UUID)) - .build() - - val settings = ScanSettings.Builder() - .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) - .setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH) - .build() - - val pi = PendingIntent.getBroadcast( - context, 0, - Intent(BleConst.ACTION_SCAN_RESULT) - .setClass(context, BleWakeReceiver::class.java), - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE - ) - scanner.startScan(listOf(filter), settings, pi) - FileLogger.i(TAG, "后台持久扫描已注册") - } catch (e: Exception) { - FileLogger.e(TAG, "注册后台扫描失败: ${e.message}", e) - } - } - - /** - * 注销后台持久扫描 - */ - fun unregisterBackgroundScan() { - val scanner = bluetoothLeScanner ?: return - try { - val pi = PendingIntent.getBroadcast( - context, 0, - Intent(BleConst.ACTION_SCAN_RESULT) - .setClass(context, BleWakeReceiver::class.java), - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE - ) - scanner.stopScan(pi) - FileLogger.i(TAG, "后台持久扫描已注销") - } catch (e: Exception) { - FileLogger.e(TAG, "注销后台扫描失败: ${e.message}", e) + private fun setupNotifications(g: BluetoothGatt, characteristic: BluetoothGattCharacteristic?) { + if (characteristic == null || characteristic.properties.and(BluetoothGattCharacteristic.PROPERTY_NOTIFY) == 0) { + FileLogger.w(TAG, "特征不支持通知功能: ${characteristic?.uuid}") + return } + + g.setCharacteristicNotification(characteristic, true) + val desc = characteristic.getDescriptor(BleConst.CLIENT_CHAR_CONFIG_UUID) + desc?.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + g.writeDescriptor(desc) } - // ====================================================================================================== - // 协议命令实现 - // ====================================================================================================== /** - * 发送命令到设备 - 通用方法 - * @param cmdType 命令类型 - * @param cmdData 命令数据 - * @return 是否成功发送 + * 处理音频数据 + * @param data 音频数据 */ - private fun sendCommand(cmdType: Byte, cmdData: ByteArray = byteArrayOf()): Boolean { - if (!checkConn()) { - FileLogger.e(TAG, "发送命令失败: 设备未连接") - return false - } - + private fun processAudioData(data: ByteArray) { try { - // 计算命令长度 - val cmdLength = cmdData.size - - // 创建命令包 - val cmdPacket = ByteArray(4 + cmdLength) // 帧头 + 命令 + 长度 + 数据 + CRC - - // 帧头 - cmdPacket[0] = BleConst.APP_REQUEST_HEADER - // 命令类型 - cmdPacket[1] = cmdType - // 命令长度 - cmdPacket[2] = cmdLength.toByte() - - // 填充命令数据(如果有) - if (cmdLength > 0) { - System.arraycopy(cmdData, 0, cmdPacket, 3, cmdLength) - } - - // 计算CRC - 对命令类型、长度和数据进行校验 - val crcData = ByteArray(2 + cmdLength) - System.arraycopy(cmdPacket, 1, crcData, 0, crcData.size) - val crc = crc8Maxim(crcData, crcData.size) - - // 设置CRC - cmdPacket[3 + cmdLength] = crc + // 记录日志 + FileLogger.d(TAG, "收到音频数据: ${data.size}字节") - // 记录命令详情 - FileLogger.d(TAG, "发送命令: CMD=0x${(cmdType.toInt() and 0xFF).toString(16)}, 长度=$cmdLength, " + - "数据=${cmdData.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}, " + - "CRC=0x${(crc.toInt() and 0xFF).toString(16)}") - - // 发送数据 - writeChar!!.value = cmdPacket - return bluetoothGatt?.writeCharacteristic(writeChar) ?: false + // 回调给上层 + callback?.onAudioDataReceived(data) } catch (e: Exception) { - FileLogger.e(TAG, "发送命令异常: ${e.message}", e) - return false - } - } - - /** - * 计算CRC8/MAXIM校验值 - */ - private fun crc8Maxim(data: ByteArray, length: Int): Byte { - var crc = 0 - val polynomial = 0x31 - - for (i in 0 until length) { - crc = crc xor (data[i].toInt() and 0xFF) - for (j in 0 until 8) { - if (crc and 0x80 != 0) { - crc = ((crc shl 1) xor polynomial) and 0xFF - } else { - crc = (crc shl 1) and 0xFF - } - } + FileLogger.e(TAG, "处理音频数据异常: ${e.message}", e) } - return crc.toByte() } - // ====================================================================================================== - // 协议解析 - // ====================================================================================================== /** * 处理设备响应数据 - 帧头为0xBB */ @@ -832,30 +719,6 @@ class BleService(private val context: Context) { FileLogger.i(TAG, "设备音量减小...") return sendCommand(BleConst.CMD_VOLUME_DOWN.toByte()) } - - /** - * 发送音频数据 - * @param data 音频数据 - */ - fun sendAudioData(data: ByteArray): Boolean { - if (!checkConn()) { - FileLogger.e(TAG, "发送音频数据失败: 设备未连接") - return false - } - - if (writeChar == null) { - FileLogger.e(TAG, "发送音频数据失败: 写特征为空") - return false - } - - try { - writeChar?.value = data - return bluetoothGatt?.writeCharacteristic(writeChar) ?: false - } catch (e: Exception) { - FileLogger.e(TAG, "发送音频数据异常: ${e.message}", e) - return false - } - } // ====================================================================================================== // 工具方法 @@ -888,6 +751,72 @@ class BleService(private val context: Context) { return bluetoothAdapter } + // ====================================================================================================== + // 后台扫描与唤醒 + // ====================================================================================================== + /** + * 注册后台持久扫描,APP被杀死后也能唤醒 + */ + fun registerBackgroundScan() { + val scanner = bluetoothLeScanner ?: return + try { + val filter = ScanFilter.Builder() + // .setServiceUuid(ParcelUuid(BleConst.PRIMARY_SERVICE_UUID)) + .setManufacturerData(0xEFAB, byteArrayOf(0xA0.toByte(), 0x01)) + .build() + + val settings = ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) + // .setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH) + .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) + .build() + + // 创建Intent并设置前台接收器标志 + val scanIntent = Intent(BleConst.ACTION_SCAN_RESULT) + .setClass(context, BleWakeReceiver::class.java) + .addFlags(Intent.FLAG_RECEIVER_FOREGROUND) + + val pi = PendingIntent.getBroadcast( + context, 0, scanIntent, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + } else { + PendingIntent.FLAG_UPDATE_CURRENT + } + ) + scanner.startScan(listOf(filter), settings, pi) + FileLogger.i(TAG, "后台持久扫描已注册") + } catch (e: Exception) { + FileLogger.e(TAG, "注册后台扫描失败: ${e.message}", e) + } + } + + /** + * 注销后台持久扫描 + */ + fun unregisterBackgroundScan() { + val scanner = bluetoothLeScanner ?: return + try { + // 创建Intent并设置前台接收器标志 + val scanIntent = Intent(BleConst.ACTION_SCAN_RESULT) + .setClass(context, BleWakeReceiver::class.java) + .addFlags(Intent.FLAG_RECEIVER_FOREGROUND) + + val pi = PendingIntent.getBroadcast( + context, 0, scanIntent, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + } else { + PendingIntent.FLAG_UPDATE_CURRENT + } + ) + scanner.stopScan(pi) + FileLogger.i(TAG, "后台持久扫描已注销") + } catch (e: Exception) { + FileLogger.e(TAG, "注销后台扫描失败: ${e.message}", e) + } + } + // ====================================================================================================== // 资源清理 // ====================================================================================================== @@ -898,8 +827,81 @@ class BleService(private val context: Context) { stopScan() scanHandler.removeCallbacksAndMessages(null) disconnectGatt() - fos?.close() - fos = null - outputFile = null + } + + // ====================================================================================================== + // 协议命令实现 + // ====================================================================================================== + /** + * 发送命令到设备 - 通用方法 + * @param cmdType 命令类型 + * @param cmdData 命令数据 + * @return 是否成功发送 + */ + private fun sendCommand(cmdType: Byte, cmdData: ByteArray = byteArrayOf()): Boolean { + if (!checkConn()) { + FileLogger.e(TAG, "发送命令失败: 设备未连接") + return false + } + + try { + // 计算命令长度 + val cmdLength = cmdData.size + + // 创建命令包 + val cmdPacket = ByteArray(4 + cmdLength) // 帧头 + 命令 + 长度 + 数据 + CRC + + // 帧头 + cmdPacket[0] = BleConst.APP_REQUEST_HEADER + // 命令类型 + cmdPacket[1] = cmdType + // 命令长度 + cmdPacket[2] = cmdLength.toByte() + + // 填充命令数据(如果有) + if (cmdLength > 0) { + System.arraycopy(cmdData, 0, cmdPacket, 3, cmdLength) + } + + // 计算CRC - 对命令类型、长度和数据进行校验 + val crcData = ByteArray(2 + cmdLength) + System.arraycopy(cmdPacket, 1, crcData, 0, crcData.size) + val crc = crc8Maxim(crcData, crcData.size) + + // 设置CRC + cmdPacket[3 + cmdLength] = crc + + // 记录命令详情 + FileLogger.d(TAG, "发送命令: CMD=0x${(cmdType.toInt() and 0xFF).toString(16)}, 长度=$cmdLength, " + + "数据=${cmdData.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}, " + + "CRC=0x${(crc.toInt() and 0xFF).toString(16)}") + + // 发送数据 + writeChar!!.value = cmdPacket + return bluetoothGatt?.writeCharacteristic(writeChar) ?: false + } catch (e: Exception) { + FileLogger.e(TAG, "发送命令异常: ${e.message}", e) + return false + } + } + + /** + * 计算CRC8/MAXIM校验值 + */ + private fun crc8Maxim(data: ByteArray, length: Int): Byte { + var crc = 0 + val polynomial = 0x31 + + for (i in 0 until length) { + crc = crc xor (data[i].toInt() and 0xFF) + for (j in 0 until 8) { + if (crc and 0x80 != 0) { + crc = ((crc shl 1) xor polynomial) and 0xFF + } else { + crc = (crc shl 1) and 0xFF + } + } + } + return crc.toByte() } } \ No newline at end of file diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt index 72834f77d..a6eb22e57 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt @@ -319,15 +319,6 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware, Plugin val success = bleManager.volumeDown() result.success(success) } - "sendAudioData" -> { - val data = call.argument("data") - if (data == null) { - result.error("INVALID_ARGUMENT", "Audio data cannot be null", null) - return - } - val success = bleManager.sendAudioData(data) - result.success(success) - } "cleanup" -> { bleManager.cleanup() result.success(true) @@ -378,9 +369,6 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware, Plugin sendStatusUpdate() } - override fun onDataReceived(data: ByteArray) { - sendEvent(dataEventSink, mapOf("type" to "rawData", "data" to data), "发送原始数据异常") - } override fun onAudioDataReceived(data: ByteArray) { sendEvent(dataEventSink, mapOf("type" to "audioData", "data" to data), "发送音频数据异常") @@ -390,13 +378,6 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware, Plugin sendEvent(dataEventSink, mapOf("type" to "wakeup"), "发送唤醒信号异常") } - override fun onCommandResult(commandType: Int, success: Boolean) { - sendEvent(connectionEventSink, mapOf( - "type" to "commandResult", - "commandType" to commandType, - "success" to success - ), "发送命令结果异常") - } override fun onProtocolCommandResult(commandType: Int, data: ByteArray?, success: Boolean) { sendEvent(connectionEventSink, mapOf( diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeReceiver.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeReceiver.kt index e9d441cf1..8b1e0a822 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeReceiver.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeReceiver.kt @@ -4,7 +4,7 @@ import android.content.* import android.bluetooth.le.* import android.os.Build import com.yunqiinnovation.deepvoice_utils.FileLogger - +import android.util.Log /** * BLE唤醒接收器 * 当耳机发送含特定UUID广播时,系统会触发此接收器 @@ -18,16 +18,19 @@ class BleWakeReceiver : BroadcastReceiver() { override fun onReceive(ctx: Context, intent: Intent) { FileLogger.init(ctx) + FileLogger.d(TAG, "onReceive — 收到唤醒广播: action=${intent.action}, ${BleConst.ACTION_SCAN_RESULT}") + if (intent.action != BleConst.ACTION_SCAN_RESULT) return val list = intent.getParcelableArrayListExtra( BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT - ) ?: return + )?: return val mac = list.first().device.address FileLogger.d(TAG, "被系统唤醒,发现设备 $mac") // 拉起前台服务并把MAC传过去 val svc = Intent(ctx, BleForegroundService::class.java).putExtra("mac", mac) + svc.action = BleConst.ACTION_SCAN_RESULT if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) ctx.startForegroundService(svc) else ctx.startService(svc) } diff --git a/local_plugins/classic_bluetooth/android/build.gradle.kts b/local_plugins/classic_bluetooth/android/build.gradle.kts deleted file mode 100644 index 526816e6b..000000000 --- a/local_plugins/classic_bluetooth/android/build.gradle.kts +++ /dev/null @@ -1,40 +0,0 @@ -plugins { - // Android Library 插件 - id("com.android.library") - // Kotlin Android 插件 - id("org.jetbrains.kotlin.android") - id("com.github.johnrengelman.shadow") version "8.1.1" -} - -android { - // 命名空间,对应你插件的包名(需与代码内包名保持一致) - namespace = "com.yunqiinnovation.classic_bluetooth" - - // 目标 SDK 版本 - compileSdk = 33 - - defaultConfig { - // 最低 SDK 版本 - minSdk = 21 - targetSdk = 33 - } - - // Java 语言级别兼容配置 - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - - // Kotlin 语言级别 - kotlinOptions { - jvmTarget = "11" - } - -} - - - -dependencies { - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0") - implementation("androidx.core:core-ktx:1.7.0") -} \ No newline at end of file diff --git a/local_plugins/classic_bluetooth/android/settings.gradle.kts b/local_plugins/classic_bluetooth/android/settings.gradle.kts deleted file mode 100644 index 22e118edf..000000000 --- a/local_plugins/classic_bluetooth/android/settings.gradle.kts +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = "classic_bluetooth" diff --git a/local_plugins/classic_bluetooth/android/src/main/AndroidManifest.xml b/local_plugins/classic_bluetooth/android/src/main/AndroidManifest.xml deleted file mode 100644 index 518c1e377..000000000 --- a/local_plugins/classic_bluetooth/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/local_plugins/classic_bluetooth/android/src/main/kotlin/com/yunqiinnovation/classic_bluetooth/ClassicBluetoothHelper.kt b/local_plugins/classic_bluetooth/android/src/main/kotlin/com/yunqiinnovation/classic_bluetooth/ClassicBluetoothHelper.kt deleted file mode 100644 index 6cad68441..000000000 --- a/local_plugins/classic_bluetooth/android/src/main/kotlin/com/yunqiinnovation/classic_bluetooth/ClassicBluetoothHelper.kt +++ /dev/null @@ -1,266 +0,0 @@ -package com.yunqiinnovation.classic_bluetooth - -import android.bluetooth.BluetoothAdapter -import android.bluetooth.BluetoothDevice -import android.bluetooth.BluetoothProfile -import android.content.Context -import android.content.pm.PackageManager -import android.content.BroadcastReceiver -import android.content.Intent -import android.content.IntentFilter -import android.os.Build -import android.Manifest -import androidx.core.content.ContextCompat -import android.util.Log - -/** - * 经典蓝牙助手类,集成蓝牙设备管理和状态监听功能 - */ -class ClassicBluetoothHelper(private val context: Context) { - - private val TAG = "ClassicBluetoothHelper" - - // 回调接口用于通知蓝牙事件 - private var bluetoothEventListener: BluetoothEventListener? = null - - // 蓝牙状态变化广播接收器 - private val bluetoothStateReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - when (intent?.action) { - BluetoothDevice.ACTION_ACL_CONNECTED -> { - val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java) - } else { - @Suppress("DEPRECATION") - intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) - } - - if (device != null) { - val deviceInfo = mapOf( - "name" to (device.name ?: "未知设备"), - "address" to device.address - ) - log("蓝牙设备已连接: ${deviceInfo["name"]} (${deviceInfo["address"]})") - notifyEvent("deviceConnected", deviceInfo) - } - } - BluetoothDevice.ACTION_ACL_DISCONNECTED -> { - val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java) - } else { - @Suppress("DEPRECATION") - intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) - } - - if (device != null) { - val deviceInfo = mapOf( - "name" to (device.name ?: "未知设备"), - "address" to device.address - ) - log("蓝牙设备已断开: ${deviceInfo["name"]} (${deviceInfo["address"]})") - notifyEvent("deviceDisconnected", deviceInfo) - } - } - BluetoothAdapter.ACTION_STATE_CHANGED -> { - val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR) - when (state) { - BluetoothAdapter.STATE_ON -> { - log("蓝牙已开启") - notifyEvent("bluetoothStateChanged", mapOf("state" to "on")) - } - BluetoothAdapter.STATE_OFF -> { - log("蓝牙已关闭") - notifyEvent("bluetoothStateChanged", mapOf("state" to "off")) - } - } - } - } - } - } - - init { - // 在构造函数中注册广播接收器 - registerBroadcastReceivers() - } - - /** - * 注册蓝牙广播接收器 - */ - private fun registerBroadcastReceivers() { - val bluetoothFilter = IntentFilter().apply { - addAction(BluetoothDevice.ACTION_ACL_CONNECTED) - addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED) - addAction(BluetoothAdapter.ACTION_STATE_CHANGED) - } - - if (Build.VERSION.SDK_INT >= 34) { // Android 14 (UPSIDE_DOWN_CAKE) - context.registerReceiver(bluetoothStateReceiver, bluetoothFilter, Context.RECEIVER_NOT_EXPORTED) - } else { - context.registerReceiver(bluetoothStateReceiver, bluetoothFilter) - } - - log("已注册蓝牙状态广播接收器") - } - - /** - * 通知蓝牙事件 - */ - private fun notifyEvent(type: String, data: Map) { - val event = mutableMapOf( - "type" to type, - "timestamp" to System.currentTimeMillis() - ) - - // 添加其他数据 - if (type == "deviceConnected" || type == "deviceDisconnected") { - event["device"] = data - } else { - event.putAll(data) - } - - // 通知监听器 - bluetoothEventListener?.onBluetoothEvent(event) - } - - /** - * 设置蓝牙事件监听器 - */ - fun setBluetoothEventListener(listener: BluetoothEventListener?) { - this.bluetoothEventListener = listener - } - - /** - * 检查蓝牙是否启用 - * @return 蓝牙是否启用 - */ - fun isBluetoothEnabled(): Boolean { - try { - val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() - if (bluetoothAdapter == null) { - log("设备不支持蓝牙", true) - return false - } - - return bluetoothAdapter.isEnabled - } catch (e: Exception) { - log("检查蓝牙状态失败: ${e.message}", true) - return false - } - } - - /** - * 获取所有连接的经典蓝牙设备 - * @return 设备信息列表,如果出错则返回空列表 - */ - fun getConnectedBluetoothDevices(): List> { - if (!hasBluetoothPermission()) { - log("缺少蓝牙权限", true) - return emptyList() - } - - val devices = mutableSetOf() - val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() ?: return emptyList() - - if (!bluetoothAdapter.isEnabled) { - return emptyList() - } - - try { - // 获取已配对设备 - val bondedDevices = bluetoothAdapter.bondedDevices ?: setOf() - - // 只保留已连接的设备 - devices.addAll(bondedDevices.filter { device -> - try { - val deviceClass = device.bluetoothClass - val majorDeviceClass = deviceClass.majorDeviceClass - // 检查设备是否为音频设备、穿戴设备等常见外设类型 - device.bondState == BluetoothDevice.BOND_BONDED && - (majorDeviceClass == BluetoothProfile.HEADSET || - majorDeviceClass == BluetoothProfile.A2DP || - majorDeviceClass == BluetoothProfile.GATT) - } catch (e: Exception) { - false - } - }) - - return devices.map { device -> - mapOf( - "name" to (device.name ?: "未知设备"), - "address" to device.address, - "type" to getDeviceTypeName(device) - ) - } - } catch (e: Exception) { - log("获取已连接蓝牙设备失败: ${e.message}", true) - return emptyList() - } - } - - /** - * 获取设备类型名称 - */ - private fun getDeviceTypeName(device: BluetoothDevice): String { - return try { - val deviceClass = device.bluetoothClass - when (deviceClass.majorDeviceClass) { - BluetoothProfile.HEADSET -> "耳机" - BluetoothProfile.A2DP -> "音频设备" - BluetoothProfile.GATT -> "低功耗设备" - else -> "其他设备" - } - } catch (e: Exception) { - "未知类型" - } - } - - /** - * 检查是否有蓝牙权限 - */ - fun hasBluetoothPermission(): Boolean { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - ContextCompat.checkSelfPermission( - context, - Manifest.permission.BLUETOOTH_CONNECT - ) == PackageManager.PERMISSION_GRANTED - } else { - ContextCompat.checkSelfPermission( - context, - Manifest.permission.BLUETOOTH - ) == PackageManager.PERMISSION_GRANTED - } - } - - /** - * 简单的日志函数 - */ - private fun log(message: String, isError: Boolean = false) { - if (isError) { - Log.e(TAG, message) - } else { - Log.d(TAG, message) - } - } - - /** - * 蓝牙事件回调接口 - */ - interface BluetoothEventListener { - fun onBluetoothEvent(event: Map) - } - - /** - * 释放资源 - */ - fun dispose() { - try { - context.unregisterReceiver(bluetoothStateReceiver) - log("已注销蓝牙状态广播接收器") - } catch (e: Exception) { - log("注销蓝牙状态广播接收器失败: ${e.message}", true) - } - - // 清除监听器 - bluetoothEventListener = null - } -} \ No newline at end of file diff --git a/local_plugins/classic_bluetooth/android/src/main/kotlin/com/yunqiinnovation/classic_bluetooth/ClassicBluetoothPlugin.kt b/local_plugins/classic_bluetooth/android/src/main/kotlin/com/yunqiinnovation/classic_bluetooth/ClassicBluetoothPlugin.kt deleted file mode 100644 index c723af77c..000000000 --- a/local_plugins/classic_bluetooth/android/src/main/kotlin/com/yunqiinnovation/classic_bluetooth/ClassicBluetoothPlugin.kt +++ /dev/null @@ -1,186 +0,0 @@ -package com.yunqiinnovation.classic_bluetooth - -import androidx.annotation.NonNull -import android.Manifest -import android.app.Activity -import android.content.Context -import android.content.pm.PackageManager -import android.os.Build -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat - -import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.embedding.engine.plugins.activity.ActivityAware -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.EventChannel.EventSink -import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener - -/** ClassicBluetoothPlugin */ -class ClassicBluetoothPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, RequestPermissionsResultListener, ClassicBluetoothHelper.BluetoothEventListener { - private lateinit var methodChannel : MethodChannel - private lateinit var eventChannel : EventChannel - private var eventSink: EventSink? = null - - private lateinit var context: Context - private var activity: Activity? = null - private lateinit var bluetoothHelper: ClassicBluetoothHelper - - private val REQUEST_BLUETOOTH_PERMISSIONS = 1003 - private var pendingResult: Result? = null - private var pendingMethodCall: MethodCall? = null - - override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - context = flutterPluginBinding.applicationContext - - // 设置方法通道 - methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.classic_bluetooth/methods") - methodChannel.setMethodCallHandler(this) - - // 设置事件通道 - eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.classic_bluetooth/events") - eventChannel.setStreamHandler(object : EventChannel.StreamHandler { - override fun onListen(arguments: Any?, events: EventSink?) { - eventSink = events - } - - override fun onCancel(arguments: Any?) { - eventSink = null - } - }) - - // 初始化蓝牙助手 - bluetoothHelper = ClassicBluetoothHelper(context) - bluetoothHelper.setBluetoothEventListener(this) - } - - override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { - when (call.method) { - "getConnectedDevices" -> { - if (!bluetoothHelper.hasBluetoothPermission()) { - if (activity != null) { - // 保存当前方法调用和结果,以便权限授予后继续执行 - pendingMethodCall = call - pendingResult = result - requestBluetoothPermissions() - } else { - result.error("PERMISSION_DENIED", "缺少必要的蓝牙权限,且无法请求权限", null) - } - return - } - - // 获取已连接的所有蓝牙设备 - val devices = bluetoothHelper.getConnectedBluetoothDevices() - result.success(devices) - } - "isBluetoothEnabled" -> { - val isEnabled = bluetoothHelper.isBluetoothEnabled() - result.success(isEnabled) - } - else -> { - result.notImplemented() - } - } - } - - /** - * 请求蓝牙权限 - */ - private fun requestBluetoothPermissions() { - if (activity == null) return - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - ActivityCompat.requestPermissions( - activity!!, - arrayOf( - Manifest.permission.BLUETOOTH_SCAN, - Manifest.permission.BLUETOOTH_CONNECT - ), - REQUEST_BLUETOOTH_PERMISSIONS - ) - } else { - ActivityCompat.requestPermissions( - activity!!, - arrayOf( - Manifest.permission.BLUETOOTH, - Manifest.permission.BLUETOOTH_ADMIN - ), - REQUEST_BLUETOOTH_PERMISSIONS - ) - } - } - - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray): Boolean { - if (requestCode == REQUEST_BLUETOOTH_PERMISSIONS) { - // 检查是否有足够的权限 - val granted = grantResults.isNotEmpty() && grantResults.all { it == PackageManager.PERMISSION_GRANTED } - - if (granted) { - // 权限已授予,继续执行挂起的方法调用 - pendingMethodCall?.let { call -> - pendingResult?.let { result -> - onMethodCall(call, result) - pendingMethodCall = null - pendingResult = null - } - } - } else { - // 权限被拒绝 - pendingResult?.error("PERMISSION_DENIED", "蓝牙权限被拒绝", null) - pendingMethodCall = null - pendingResult = null - } - - return true - } - - return false - } - - override fun onAttachedToActivity(binding: ActivityPluginBinding) { - activity = binding.activity - binding.addRequestPermissionsResultListener(this) - } - - override fun onDetachedFromActivityForConfigChanges() { - activity = null - } - - override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { - activity = binding.activity - binding.addRequestPermissionsResultListener(this) - } - - override fun onDetachedFromActivity() { - activity = null - } - - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { - methodChannel.setMethodCallHandler(null) - eventChannel.setStreamHandler(null) - - // 释放资源 - bluetoothHelper.setBluetoothEventListener(null) - bluetoothHelper.dispose() - } - - override fun onBluetoothEvent(event: Map) { - if (eventSink == null) return - - try { - activity?.runOnUiThread { - try { - eventSink?.success(event) - } catch (e: Exception) { - // 忽略异常 - } - } - } catch (e: Exception) { - // 忽略异常 - } - } -} \ No newline at end of file diff --git a/local_plugins/classic_bluetooth/lib/classic_bluetooth.dart b/local_plugins/classic_bluetooth/lib/classic_bluetooth.dart deleted file mode 100644 index a9389d6b9..000000000 --- a/local_plugins/classic_bluetooth/lib/classic_bluetooth.dart +++ /dev/null @@ -1,119 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/services.dart'; - -/// 经典蓝牙设备信息 -class BluetoothDevice { - /// 设备名称 - final String name; - - /// 设备地址 - final String address; - - /// 设备类型 - final String type; - - /// 构造函数 - BluetoothDevice({ - required this.name, - required this.address, - required this.type, - }); - - /// 从Map创建设备信息 - factory BluetoothDevice.fromMap(Map map) { - return BluetoothDevice( - name: map['name'] as String? ?? '未知设备', - address: map['address'] as String? ?? '', - type: map['type'] as String? ?? '未知类型', - ); - } - - /// 转换为Map - Map toMap() { - return { - 'name': name, - 'address': address, - 'type': type, - }; - } - - @override - String toString() => '$name ($address) [$type]'; -} - -/// 蓝牙事件 -class BluetoothEvent { - /// 事件类型 - final String type; - - /// 时间戳 - final int timestamp; - - /// 事件数据 - final Map data; - - /// 构造函数 - BluetoothEvent({ - required this.type, - required this.timestamp, - required this.data, - }); - - /// 从Map创建事件 - factory BluetoothEvent.fromMap(Map map) { - return BluetoothEvent( - type: map['type'] as String, - timestamp: map['timestamp'] as int, - data: Map.from(map) - ..remove('type') - ..remove('timestamp'), - ); - } - - /// 获取关联的设备(如果有) - BluetoothDevice? get device { - if (data.containsKey('device')) { - return BluetoothDevice.fromMap(Map.from(data['device'] as Map)); - } - return null; - } - - /// 获取蓝牙状态(如果是状态变化事件) - String? get bluetoothState { - if (type == 'bluetoothStateChanged') { - return data['state'] as String?; - } - return null; - } -} - -/// 经典蓝牙插件类 -class ClassicBluetooth { - static const MethodChannel _methodChannel = - MethodChannel('com.yunqiinnovation.classic_bluetooth/methods'); - static const EventChannel _eventChannel = - EventChannel('com.yunqiinnovation.classic_bluetooth/events'); - - static Stream? _eventStream; - - /// 获取蓝牙事件流 - static Stream get events { - _eventStream ??= _eventChannel.receiveBroadcastStream() - .map((dynamic event) => BluetoothEvent.fromMap(Map.from(event as Map))); - return _eventStream!; - } - - /// 获取当前连接的蓝牙设备 - static Future> getConnectedDevices() async { - final List devices = await _methodChannel.invokeMethod('getConnectedDevices'); - return devices - .map((device) => BluetoothDevice.fromMap(Map.from(device as Map))) - .toList(); - } - - /// 检查蓝牙是否启用 - static Future isBluetoothEnabled() async { - return await _methodChannel.invokeMethod('isBluetoothEnabled'); - } -} \ No newline at end of file diff --git a/local_plugins/classic_bluetooth/pubspec.yaml b/local_plugins/classic_bluetooth/pubspec.yaml deleted file mode 100644 index 7ab5fd1f3..000000000 --- a/local_plugins/classic_bluetooth/pubspec.yaml +++ /dev/null @@ -1,23 +0,0 @@ -name: classic_bluetooth -description: 经典蓝牙设备管理插件 -version: 1.0.0 -homepage: https://yunqiinnovation.com - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.20.0" - -dependencies: - flutter: - sdk: flutter - -dev_dependencies: - flutter_test: - sdk: flutter - -flutter: - plugin: - platforms: - android: - package: com.yunqiinnovation.classic_bluetooth - pluginClass: ClassicBluetoothPlugin \ No newline at end of file diff --git a/local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock b/local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock index a6fdf5ae2ab9cd81e316cdbd1c5bdf5732bdbd35..5daa4e92e075dff9f616090dc6d87cf35b3859c7 100644 GIT binary patch literal 17 UcmZQRo$b{-r|q*H0|dwb05?bkfB*mh literal 17 UcmZQRo$b{-r|q*H0|bZx05?JedH?_b diff --git a/local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock index ee70b56b581d54729bb4e58882287ca0d3c97fe1..bbd82acf731ad1e0c071b7874e67f48e088600cf 100644 GIT binary patch literal 17 TcmZQ}Dp$CADo{Lz0RmJ2DJKJD literal 17 TcmZQ}Dp$CADo{Lz0Rp4|DIEh^ diff --git a/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties b/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties index 9463138c9..60e8a4931 100644 --- a/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties +++ b/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties @@ -1,2 +1,2 @@ -#Wed Apr 30 15:54:50 IST 2025 +#Fri May 02 14:33:07 IST 2025 gradle.version=8.10 diff --git a/pubspec.yaml b/pubspec.yaml index e208e7a73..d410fbe5e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,8 +60,6 @@ dependencies: flutter_facebook_auth: ^7.1.1 flutter_image_compress: ^2.4.0 common_utils: ^2.1.0 - deepvoice_utils: - path: local_plugins/deepvoice_utils # image_cropper: ^9.0.0 dio: ^5.8.0+1 sqflite: ^2.4.2 @@ -82,8 +80,8 @@ dependencies: path: local_plugins/jl_opus ble_service: path: local_plugins/ble_service - classic_bluetooth: - path: local_plugins/classic_bluetooth + deepvoice_utils: + path: local_plugins/deepvoice_utils dev_dependencies: diff --git a/test.json b/test.json index 19f06549a..99bc3f2d6 100644 --- a/test.json +++ b/test.json @@ -1,225 +1,16 @@ -curl 'https: //ark.cn-beijing.volces.com/api/v3/bots/chat/completions' \ --H "Authorization: Bearer 168deb3d-fd0c-4912-b9f1-aaee5c6743e6" \ --H 'Content-Type: application/json' \ --d '{ - "model": "bot-20250405211523-l7c9r", - "stream": true, - "stream_options": { - "include_usage": true - }, - "messages": [ - { - "role": "system", - "content": "You are a helpful assistant." - }, - { - "role": "user", - "content": "Hello!" - } - ] -}' +intent={cmp=com.yunqiinnovation.deepsound/com.yunqiinnovation.ble_service.BleForegroundService} +com.yunqiinnovation.deepsound/com.yunqiinnovation.ble_service.BleWakeReceiver filter f42f2c2 +# 1. 启动前台 Service (API 26+ 推荐用 start-foreground-service) +adb shell am start-foreground-service \ + -n com.yunqiinnovation.deepsound/com.yunqiinnovation.ble_service.BleForegroundService -"amap-amap-sse": { - "url": "https://mcp.amap.com/sse?key=e5fdc9605eabdeb5626f18f5721f343d" -}, -"web-search": { - "url": "http://mcp.ideapsound.com:8000/sse" -}, -"deapsound-deapsound": { - "url": "https://www.deapsound.com/sse" -} +# 2. 发送广播到 BleWakeReceiver +adb shell am broadcast \ + -a com.yunqiinnovation.ble_service.ACTION_BLE_SCAN \ + -n com.yunqiinnovation.deepsound/com.yunqiinnovation.ble_service.BleWakeReceiver -curl 'https: //ark.cn-beijing.volces.com/api/v3/bots/chat/completions' \ --H 'Content-Type: application/json' \ --H 'Authorization: Bearer 168deb3d-fd0c-4912-b9f1-aaee5c6743e6' \ --d '{ - "model": "bot-20250405211523-l7c9r", - "stream": true, - "messages": [ - { - "role": "assistant", - "content": "你好呀!今天想和我聊些什么呢😄 是生活趣事、学习心得,还是其他好玩的话题?" - }, - { - "role": "assistant", - "content": "" - }, - { - "role": "assistant", - "content": "你好呀!今天过得怎么样?有什么需要我帮忙的吗?或者只是想聊聊天?😊" - }, - { - "role": "assistant", - "content": "明白啦!有什么需要我帮忙的吗?无论是查资料、聊天解闷,还是帮你处理手机上的任务,我随时待命哦~ 😊" - }, - { - "role": "assistant", - "content": "好的!随时准备陪你测试,有什么需要尽管说哦~ 😊 你希望测试什么功能呢?" - }, - { - "role": "assistant", - "content": "哎呀,听起来你有点小困扰呢!可能是因为网络波动、系统更新或者我偶尔“打瞌睡”了(开玩笑啦~)。不过别担心,我会努力保持最佳状态陪伴你!如果有什么具体问题,随时告诉我,我一定帮你解决得妥妥的!😊\n\n(或者你想测试什么功能?我随时待命哦!)" - }, - { - "role": "assistant", - "content": "嘿嘿,我是你的贴心语音助理小语呀!随时为你提供帮助、陪你聊天、解答问题,还能给你加油打气哦~有什么需要尽管告诉我吧! 😊" - }, - { - "role": "assistant", - "content": "我是小语呀,那个随时准备和你畅聊、给你排忧解难、陪你度过各种时光的手机全能语音助理😜 怎么啦,是不是刚才没记住呀🧐" - }, - { - "role": "assistant", - "content": "" - }, - { - "role": "user", - "content": "你好呀,你是谁呀?" - } - ], - "temperature": 0.7, - "max_tokens": 2000, - "stream": true, - "tools": [ - { - "type": "function", - "function": { - "name": "exit_interaction", - "description": "退出当前语音交互", - "parameters": { - "type": "object", - "properties": {}, - "required": [] - } - } - }, - { - "type": "function", - "function": { - "name": "enter_translation_mode", - "description": "用户请求进入实时翻译模式时,启动实时翻译功能", - "parameters": { - "type": "object", - "properties": {}, - "required": [] - } - } - }, - { - "type": "function", - "function": { - "name": "send_text_message", - "description": "发送短信", - "parameters": { - "type": "object", - "properties": { - "contact": { - "type": "string", - "description": "联系人姓名或电话号码" - }, - "message": { - "type": "string", - "description": "短信内容" - } - }, - "required": [ - "contact", - "message" - ] - } - } - }, - { - "type": "function", - "function": { - "name": "make_phone_call", - "description": "拨打电话", - "parameters": { - "type": "object", - "properties": { - "contact": { - "type": "string", - "description": "联系人姓名或电话号码" - } - }, - "required": [ - "contact" - ] - } - } - }, - { - "type": "function", - "function": { - "name": "open_app", - "description": "当用户请求打开特定应用时,启动相应应用", - "parameters": { - "type": "object", - "properties": { - "app_name": { - "type": "string", - "description": "应用名称" - } - }, - "required": [ - "app_name" - ] - } - } - } - ] -}' + adb shell dumpsys package com.yunqiinnovation.deepsound | grep BleWakeReceiver - - - -curl -X POST "https://www.deapsound.com/messages?sessionId=f156e609-6875-420b-b08d-577b7db695f1" \ --H "Accept-Charset: UTF-8" \ --H "Accept: */*" \ --H "User-Agent: ktor-client" \ --H "Content-Type: application/json" \ --d '{ - "id": 6, - "method": "tools/call", - "params": { - "name": "tool_weather", - "arguments": { - "city": "上海" - }, - "_meta": {}, - "method": "tools/call" - }, - "jsonrpc": "2.0" -}' - - - - -curl -X POST "https://www.deapsound.com/messages?sessionId=91c6e740-9a24-48f1-8c7f-62a2f99d1006" \ - -H "Accept-Charset: UTF-8" \ - -H "Accept: */*" \ - -H "User-Agent: ktor-client" \ - -H "Content-Type: application/json" \ - -d '{ - "method": "notifications/initialized", - "params": { - "method": "notifications/initialized" - }, - "jsonrpc": "2.0" -}' - -curl -X GET "https://www.deapsound.com/sse" \ - -H "Accept: text/event-stream,text/event-stream" \ - -H "Cache-Control: no-cache,no-store" \ - -H "Accept-Charset: UTF-8" \ - -H "User-Agent: ktor-client"{ - "app_alive": 270180271, - "build_connection_time": 919, - "disconnect_times": 1, - "is_background": false, - "last_connect_time": 919, - "last_duration": 438, - "payload_sign_time": 0, - "report_time": 1745779540.932181, + adb shell dumpsys package com.yunqiinnovation.deepsound | grep receiver \ No newline at end of file