From 74ec1d4c6ae337cbbfdf35dbf87fab97ecb8856d Mon Sep 17 00:00:00 2001 From: wolfplus Date: Thu, 5 Jun 2025 17:05:50 +0100 Subject: [PATCH] service adjust --- lib/data/services/ble_manager.dart | 10 + .../controllers/settings_controller.dart | 39 ++ lib/modules/settings/views/settings_view.dart | 24 ++ .../android/src/main/AndroidManifest.xml | 2 +- .../agent_service/BleMediaService.kt | 337 ++++-------------- 5 files changed, 138 insertions(+), 274 deletions(-) diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index 1b472e8ab..43179eb5c 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -732,6 +732,16 @@ class BleManager extends GetxService { } } + /// 清除BLE配对设备 + Future clearPairedDevices() async { + try { + return await _bleService.clearAssociations(); + } catch (e) { + Logger.error('清除BLE配对设备失败: ${e.toString()}'); + return false; + } + } + /// 注册服务 static Future init() async { if (!Get.isRegistered()) { diff --git a/lib/modules/settings/controllers/settings_controller.dart b/lib/modules/settings/controllers/settings_controller.dart index 984376632..aa8ac4d54 100644 --- a/lib/modules/settings/controllers/settings_controller.dart +++ b/lib/modules/settings/controllers/settings_controller.dart @@ -8,6 +8,7 @@ import '../../../data/services/speech_factory.dart'; import '../models/language_model.dart'; import 'package:logger/logger.dart'; import '../../../data/models/user_Info.dart'; +import '../../../data/services/ble_manager.dart'; import 'package:path_provider/path_provider.dart'; import 'dart:io'; @@ -402,4 +403,42 @@ class SettingsController extends GetxController { sourceLanguage.value = targetLanguage.value; targetLanguage.value = temp; } + + /// 清除BLE配对设备 + Future clearBleDevices() async { + // 显示确认对话框 + final confirmed = await Get.dialog( + AlertDialog( + title: const Text('清除配对设备'), + content: const Text('确定要清除所有BLE配对设备吗?此操作不可撤销。'), + actions: [ + TextButton( + onPressed: () => Get.back(result: false), + child: Text('cancel'.tr), + ), + TextButton( + onPressed: () => Get.back(result: true), + style: TextButton.styleFrom( + foregroundColor: Colors.red, + ), + child: const Text('确定'), + ), + ], + ), + ); + + if (confirmed == true) { + try { + final bleManager = Get.find(); + final result = await bleManager.clearPairedDevices(); + if (result) { + Get.snackbar('成功', 'BLE配对设备已清除'); + } else { + Get.snackbar('失败', '清除BLE配对设备失败'); + } + } catch (e) { + Get.snackbar('错误', '清除BLE配对设备时发生错误'); + } + } + } } diff --git a/lib/modules/settings/views/settings_view.dart b/lib/modules/settings/views/settings_view.dart index ce889eb10..9fe16c6dc 100644 --- a/lib/modules/settings/views/settings_view.dart +++ b/lib/modules/settings/views/settings_view.dart @@ -481,6 +481,30 @@ class SettingsView extends GetView { color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[200]), + + // 清除配对设备 + _buildSimpleNavigationSetting( + title: '清除配对设备', + subtitle: '清除所有BLE配对设备记录', + icon: Icons.bluetooth_disabled, + iconBgColor: isDarkMode + ? Colors.red[900]!.withOpacity(0.3) + : Colors.red[100]!, + iconColor: + isDarkMode ? Colors.red[300]! : Colors.red[600]!, + titleColor: isDarkMode ? Colors.white : null, + subtitleColor: isDarkMode ? Colors.white70 : null, + onTap: () { + controller.clearBleDevices(); + }, + isDarkMode: isDarkMode, + ), + Divider( + height: 1, + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[200]), + // 杰理测试 _buildSimpleNavigationSetting( title: '杰理蓝牙测试', diff --git a/local_plugins/agent_service/android/src/main/AndroidManifest.xml b/local_plugins/agent_service/android/src/main/AndroidManifest.xml index ac04b7dca..b9e7e226b 100644 --- a/local_plugins/agent_service/android/src/main/AndroidManifest.xml +++ b/local_plugins/agent_service/android/src/main/AndroidManifest.xml @@ -13,7 +13,7 @@ android:name=".BleMediaService" android:exported="true" android:enabled="true" - android:foregroundServiceType="mediaPlayback"> + android:foregroundServiceType="connectedDevice"> diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt index 0a811f33a..e7452eb45 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt @@ -5,46 +5,35 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.app.Service -import android.bluetooth.BluetoothManager -import android.bluetooth.le.ScanFilter -import android.bluetooth.le.ScanSettings -import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.SharedPreferences -import android.media.AudioManager -import android.media.MediaPlayer import android.os.Build import android.os.IBinder -import android.support.v4.media.session.MediaSessionCompat -import android.support.v4.media.session.PlaybackStateCompat import androidx.core.app.NotificationCompat -import androidx.media.session.MediaButtonReceiver -import com.yunqiinnovation.agent_service.R import android.util.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import java.util.concurrent.atomic.AtomicBoolean -import android.view.KeyEvent import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import com.yunqiinnovation.ble_service.BleConst import com.yunqiinnovation.ble_service.BleService + /** - * 语音助手媒体服务 + * BLE代理服务 * 职责: - * 1. 接收并处理媒体按钮事件(播放/暂停等) - * 2. 维持媒体会话,使系统可以通过媒体按钮唤醒应用 - * 3. 最小化实现,不直接对接Flutter层 + * 1. 语音代理初始化和管理 + * 2. BLE设备连接管理 + * 3. 后台扫描结果处理 */ class BleMediaService : Service(), CoroutineScope { companion object { - private const val TAG = "BleMediaService" - private const val CHANNEL_ID = "ble_media_service_channel" + private const val TAG = "BleAgentService" + private const val CHANNEL_ID = "ble_agent_service_channel" private const val NOTIFICATION_ID = 1002 private const val PREF_NAME = "deep_voice_config" @@ -70,18 +59,10 @@ class BleMediaService : Service(), CoroutineScope { private val job = SupervisorJob() override val coroutineContext: CoroutineContext get() = Dispatchers.IO + job - - private lateinit var mediaSession: MediaSessionCompat - private lateinit var audioManager: AudioManager - - // 媒体操作防抖 - private var lastActionTime = 0L + // BLE扫描防抖 private var lastScanTime = 0L - private val ACTION_DEBOUNCE_MS = 800L // 媒体操作防抖时间(毫秒) - - // 添加音频播放器 - private var audioPlayer: MiniMediaPlayer? = null + private val SCAN_DEBOUNCE_MS = 800L // BLE扫描防抖时间(毫秒) // 语音代理是否已初始化 private var isChatAgentInitialized = false @@ -91,106 +72,85 @@ class BleMediaService : Service(), CoroutineScope { override fun onCreate() { super.onCreate() - Log.i(TAG, "BleMediaService正在创建...") + Log.i(TAG, "BLE代理服务正在创建...") // 设置服务状态 isRunning.set(true) startForeground(NOTIFICATION_ID, createNotification()) - // 初始化音频管理器 - audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager - - // 创建并启动媒体会话 - setupMediaSession() - // 注册媒体按钮接收器 - // registerMediaButtonReceiver() - // // 注册后台扫描 - // registerBackgroundScan() - - // 创建音频播放器,并播放静音文件 - audioPlayer = MiniMediaPlayer(this) - audioPlayer?.play(R.raw.silence, 0f) - setPlaybackState(PlaybackStateCompat.STATE_PAUSED) - - Log.i(TAG, "媒体服务已启动") + Log.i(TAG, "BLE代理服务已启动") } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - Log.i(TAG, "BleMediaService.onStartCommand: ${intent?.action}") + Log.i(TAG, "BleAgentService.onStartCommand: ${intent?.action}") // 保存当前Intent,用于后续初始化 currentIntent = intent - // 初始化语音代理 + + // 初始化语音代理 if (!isChatAgentInitialized) { initializeChatAgent() } - // 如果是媒体按钮事件,交给MediaButtonReceiver处理 - if (Intent.ACTION_MEDIA_BUTTON == intent?.action) { - MediaButtonReceiver.handleIntent(mediaSession, intent) - return START_STICKY - } // 处理蓝牙扫描结果事件 if (intent?.action == BleConst.ACTION_SCAN_RESULT) { - val currentTime = System.currentTimeMillis() - if (currentTime - lastScanTime >= ACTION_DEBOUNCE_MS) { - lastScanTime = currentTime - checkBleConnection() - } - + val currentTime = System.currentTimeMillis() + if (currentTime - lastScanTime >= SCAN_DEBOUNCE_MS) { + lastScanTime = currentTime + checkBleConnection() + } return START_STICKY } - - return START_STICKY } - /** * 初始化语音交互代理 */ private fun initializeChatAgent() { - try { - // 获取SharedPreferences - val prefs = applicationContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) - - // 尝试从Intent中获取参数 - val configFromIntent = extractConfigFromIntent(currentIntent) - - // 如果Intent中有参数,则保存到SharedPreferences - if (configFromIntent.isNotEmpty()) { - Log.d(TAG, "从Intent中获取到配置参数,保存到SharedPreferences") - saveConfigToPreferences(configFromIntent, prefs) - } - - // 合并Intent和SharedPreferences中的参数 - val config = getConfigFromPreferences(prefs) - Log.d(TAG, "当前配置参数: $config") - // 检查必要的配置是否存在 - if (config[KEY_AZURE_SPEECH_KEY].isNullOrEmpty() || config[KEY_OPENAI_API_KEY].isNullOrEmpty()) { - Log.w(TAG, "语音代理初始化失败: API密钥未配置") - return - } - - if (isChatAgentInitialized) { - Log.d(TAG, "ChatAgent已经初始化,跳过") - return - } - if(config.isNotEmpty()) { - // 初始化ChatAgent - Log.i(TAG, "开始初始化ChatAgent") - val success = BleAgent.initialize(applicationContext, config) - if (success) { - isChatAgentInitialized = true - Log.i(TAG, "语音交互代理初始化成功") - } else { - Log.e(TAG, "语音交互代理初始化失败") - } + try { + // 获取SharedPreferences + val prefs = applicationContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + + // 尝试从Intent中获取参数 + val configFromIntent = extractConfigFromIntent(currentIntent) + + // 如果Intent中有参数,则保存到SharedPreferences + if (configFromIntent.isNotEmpty()) { + Log.d(TAG, "从Intent中获取到配置参数,保存到SharedPreferences") + saveConfigToPreferences(configFromIntent, prefs) + } + + // 合并Intent和SharedPreferences中的参数 + val config = getConfigFromPreferences(prefs) + Log.d(TAG, "当前配置参数: $config") + + // 检查必要的配置是否存在 + if (config[KEY_AZURE_SPEECH_KEY].isNullOrEmpty() || config[KEY_OPENAI_API_KEY].isNullOrEmpty()) { + Log.w(TAG, "语音代理初始化失败: API密钥未配置") + return + } + + if (isChatAgentInitialized) { + Log.d(TAG, "ChatAgent已经初始化,跳过") + return + } + + if(config.isNotEmpty()) { + // 初始化ChatAgent + Log.i(TAG, "开始初始化ChatAgent") + val success = BleAgent.initialize(applicationContext, config) + if (success) { + isChatAgentInitialized = true + Log.i(TAG, "语音交互代理初始化成功") + } else { + Log.e(TAG, "语音交互代理初始化失败") } - } catch (e: Exception) { - Log.e(TAG, "初始化语音交互代理时发生异常", e) } + } catch (e: Exception) { + Log.e(TAG, "初始化语音交互代理时发生异常", e) + } } /** @@ -255,23 +215,7 @@ class BleMediaService : Service(), CoroutineScope { override fun onBind(intent: Intent?): IBinder? = null override fun onDestroy() { - Log.i(TAG, "BleMediaService正在销毁...") - - // 取消注册媒体按钮接收器 - try { - val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name) - audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver) - Log.i(TAG, "媒体按钮接收器已取消注册") - } catch (e: Exception) { - Log.e(TAG, "取消注册媒体按钮接收器失败: ${e.message}", e) - } - - // 释放媒体会话 - mediaSession.release() - - // 停止并释放音频播放器 - audioPlayer?.release() - audioPlayer = null + Log.i(TAG, "BLE代理服务正在销毁...") // 取消所有协程 cancel() @@ -281,62 +225,12 @@ class BleMediaService : Service(), CoroutineScope { super.onDestroy() } - - /** - * 设置媒体会话,处理媒体按钮事件 - */ - private fun setupMediaSession() { - mediaSession = MediaSessionCompat(this, "BleMediaSession").apply { - setFlags( - MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or - MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS - ) - // 设置回调,处理媒体按钮事件 - setCallback(object : MediaSessionCompat.Callback() { - - override fun onMediaButtonEvent(mediaButtonIntent: Intent): Boolean { - - // 使用系统默认的媒体按钮处理 - return super.onMediaButtonEvent(mediaButtonIntent) - } - }) - - // 设置初始播放状态 - val stateBuilder = PlaybackStateCompat.Builder() - .setActions( - PlaybackStateCompat.ACTION_PLAY or - PlaybackStateCompat.ACTION_PAUSE or - PlaybackStateCompat.ACTION_SKIP_TO_NEXT or - PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS - ) - .setState(PlaybackStateCompat.STATE_STOPPED, 0, 1.0f) - setPlaybackState(stateBuilder.build()) - - // 激活会话,使其可以接收媒体按钮事件 - isActive = true - } - } - - /** - * 注册媒体按钮接收器 - */ - private fun registerMediaButtonReceiver() { - try { - val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name) - audioManager.registerMediaButtonEventReceiver(mediaButtonReceiver) - Log.i(TAG, "媒体按钮接收器注册成功: $mediaButtonReceiver") - } catch (e: Exception) { - Log.e(TAG, "媒体按钮接收器注册失败: ${e.message}", e) - } - } - private fun checkBleConnection() { - Log.i(TAG, "checkBleConnection收到连接请求") try { - // 示例:使用协程异步处理连接 + // 使用协程异步处理连接 launch { if(!BleService.isConnected()) { BleService.connect() @@ -347,44 +241,6 @@ class BleMediaService : Service(), CoroutineScope { } } - /** - * 更新通知内容 - */ - private fun updateNotification() { - try { - val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.notify(NOTIFICATION_ID, createNotification()) - } catch (e: Exception) { - Log.e(TAG, "更新通知异常: ${e.message}") - } - } - - /** - * 设置播放状态 - */ - private fun setPlaybackState(state: Int) { - val playbackStateBuilder = PlaybackStateCompat.Builder() - .setActions( - PlaybackStateCompat.ACTION_PLAY or - PlaybackStateCompat.ACTION_PAUSE or - PlaybackStateCompat.ACTION_PLAY_PAUSE or - PlaybackStateCompat.ACTION_SKIP_TO_NEXT or - PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or - PlaybackStateCompat.ACTION_STOP or - PlaybackStateCompat.ACTION_SEEK_TO or - PlaybackStateCompat.ACTION_FAST_FORWARD or - PlaybackStateCompat.ACTION_REWIND - ) - // 使用更高的播放速度,让系统认为是真正的播放 - .setState(state, 0, 1.0f, System.currentTimeMillis()) - mediaSession.setPlaybackState(playbackStateBuilder.build()) - - // 确保MediaSession激活 - if (!mediaSession.isActive) { - mediaSession.isActive = true - } - } - /** * 创建前台服务通知 */ @@ -395,7 +251,7 @@ class BleMediaService : Service(), CoroutineScope { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( CHANNEL_ID, - "语音助手媒体服务", + "语音助手代理服务", NotificationManager.IMPORTANCE_LOW ) manager.createNotificationChannel(channel) @@ -419,77 +275,12 @@ class BleMediaService : Service(), CoroutineScope { // 构建通知 val builder = NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle("DeepSound") - // .setContentText(if (isPlaying) "正在播放" else "已暂停") - .setSmallIcon(android.R.drawable.ic_media_play) + .setContentText("语音助手代理服务运行中") + .setSmallIcon(android.R.drawable.ic_dialog_info) // 设置点击意图 pendingIntent?.let { builder.setContentIntent(it) } return builder.build() } - - /** - * 简易媒体播放器,用于播放静音资源 - */ - inner class MiniMediaPlayer(private val context: Context) { - private var mediaPlayer: MediaPlayer? = null - - /** - * 播放资源文件 - * @param resId 资源ID - * @param volume 音量 (0.0-1.0) - */ - fun play(resId: Int, volume: Float = 0f) { - try { - // 释放之前的资源 - release() - - // 创建播放器并设置资源 - mediaPlayer = MediaPlayer().apply { - // 设置资源 - context.resources.openRawResourceFd(resId)?.use { fd -> - setDataSource(fd.fileDescriptor, fd.startOffset, fd.length) - } - - // 设置音量 - setVolume(volume, volume) - - // // 播放完成后自动循环播放,保持媒体会话活跃 - // setOnCompletionListener { - // Log.i(TAG, "静音资源播放完成,再次播放保持会话活跃") - // play(resId, volume) - // } - - // 准备并播放 - prepare() - start() - } - - Log.i(TAG, "开始播放静音资源") - } catch (e: Exception) { - Log.e(TAG, "播放静音资源异常: ${e.message}", e) - release() - } - } - - /** - * 释放资源 - */ - fun release() { - try { - mediaPlayer?.apply { - if (isPlaying) stop() - release() - } - mediaPlayer = null - } catch (e: Exception) { - // 忽略异常 - } - } - - /** - * 是否正在播放 - */ - fun isPlaying() = mediaPlayer?.isPlaying == true - } } \ No newline at end of file