Browse Source

service adjust

newdev_shunjiawei
wolfplus 1 year ago
parent
commit
326f3ba7d2
  1. 10
      lib/data/services/ble_manager.dart
  2. 39
      lib/modules/settings/controllers/settings_controller.dart
  3. 24
      lib/modules/settings/views/settings_view.dart
  4. 2
      local_plugins/agent_service/android/src/main/AndroidManifest.xml
  5. 337
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt

10
lib/data/services/ble_manager.dart

@ -732,6 +732,16 @@ class BleManager extends GetxService {
}
}
/// 清除BLE配对设备
Future<bool> clearPairedDevices() async {
try {
return await _bleService.clearAssociations();
} catch (e) {
Logger.error('清除BLE配对设备失败: ${e.toString()}');
return false;
}
}
/// 注册服务
static Future<BleManager> init() async {
if (!Get.isRegistered<BleManager>()) {

39
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<void> clearBleDevices() async {
// 显示确认对话框
final confirmed = await Get.dialog<bool>(
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<BleManager>();
final result = await bleManager.clearPairedDevices();
if (result) {
Get.snackbar('成功', 'BLE配对设备已清除');
} else {
Get.snackbar('失败', '清除BLE配对设备失败');
}
} catch (e) {
Get.snackbar('错误', '清除BLE配对设备时发生错误');
}
}
}
}

24
lib/modules/settings/views/settings_view.dart

@ -481,6 +481,30 @@ class SettingsView extends GetView<SettingsController> {
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: '杰理蓝牙测试',

2
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">
<!-- <intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter> -->

337
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
}
}
Loading…
Cancel
Save