|
|
|
@ -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" |
|
|
|
|
|
|
|
@ -71,17 +60,9 @@ class BleMediaService : Service(), CoroutineScope { |
|
|
|
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) |
|
|
|
try { |
|
|
|
// 获取SharedPreferences |
|
|
|
val prefs = applicationContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) |
|
|
|
|
|
|
|
// 尝试从Intent中获取参数 |
|
|
|
val configFromIntent = extractConfigFromIntent(currentIntent) |
|
|
|
// 尝试从Intent中获取参数 |
|
|
|
val configFromIntent = extractConfigFromIntent(currentIntent) |
|
|
|
|
|
|
|
// 如果Intent中有参数,则保存到SharedPreferences |
|
|
|
if (configFromIntent.isNotEmpty()) { |
|
|
|
Log.d(TAG, "从Intent中获取到配置参数,保存到SharedPreferences") |
|
|
|
saveConfigToPreferences(configFromIntent, prefs) |
|
|
|
} |
|
|
|
// 如果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 |
|
|
|
} |
|
|
|
// 合并Intent和SharedPreferences中的参数 |
|
|
|
val config = getConfigFromPreferences(prefs) |
|
|
|
Log.d(TAG, "当前配置参数: $config") |
|
|
|
|
|
|
|
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, "语音交互代理初始化失败") |
|
|
|
} |
|
|
|
// 检查必要的配置是否存在 |
|
|
|
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() |
|
|
|
@ -282,61 +226,11 @@ 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 |
|
|
|
} |
|
|
|
} |