Browse Source

上传音乐服务

weicu
liwei1dao 1 year ago
parent
commit
c3ae2d6e52
  1. 4
      lib/modules/agent/views/message_bubble.dart
  2. 31
      local_plugins/music_service/android/src/main/AndroidManifest.xml
  3. 363
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt

4
lib/modules/agent/views/message_bubble.dart

@ -715,7 +715,7 @@ class MessageBubble extends GetView<AgentController> {
children: [
Text(
"${city} ${now["date"]}",
style: TextStyle(
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
@ -734,7 +734,7 @@ class MessageBubble extends GetView<AgentController> {
children: [
Text(
"${now["temperature"]}°",
style: TextStyle(
style: const TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
color: Colors.white,

31
local_plugins/music_service/android/src/main/AndroidManifest.xml

@ -1,24 +1,29 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yunqiinnovation.music_service">
<!-- 基础前台服务权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- Android 14 及以上需要声明具体的前台服务类型权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<application>
<service
android:name=".MusicService"
android:exported="false"
android:enabled="true"
android:foregroundServiceType="mediaPlayback" />
<!-- 用于接收媒体按钮(耳机按钮、蓝牙播放控制) -->
<receiver
android:name="androidx.media.session.MediaButtonReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
android:exported="true">
<!-- 让系统知道这是媒体浏览服务 -->
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
<!-- 让系统知道这里能处理媒体按键 -->
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</service>
<receiver
android:name="androidx.media.session.MediaButtonReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
</application>
</manifest>

363
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt

@ -1,20 +1,25 @@
package com.yunqiinnovation.music_service
import android.app.*
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.media.AudioManager
import android.media.MediaPlayer
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import androidx.core.app.NotificationCompat
import androidx.media.MediaBrowserServiceCompat
import androidx.media.session.MediaButtonReceiver
import com.yunqiinnovation.deepvoice_utils.FileLogger
import java.util.concurrent.atomic.AtomicBoolean
class MusicService : Service() {
class MusicService : MediaBrowserServiceCompat() {
companion object {
private const val TAG = "MusicService"
@ -22,242 +27,240 @@ class MusicService : Service() {
private const val NOTIFICATION_ID = 1002
private val isRunning = AtomicBoolean(false)
private var currentStatus: Boolean = false
private var currentUrlInternal: String? = null
private var currentStatus = false
private var playlist = mutableListOf<String>()
private var currentIndex = 0
fun isServiceRunning(): Boolean = isRunning.get()
fun isPlaying(): Boolean = currentStatus
fun isServiceRunning() = isRunning.get()
fun isPlaying() = currentStatus
fun getCurrentUrl(): String? = currentUrlInternal
}
private lateinit var mediaSession: MediaSessionCompat
private lateinit var audioManager: AudioManager
private var mediaPlayer: MediaPlayer? = null
private val playlist = mutableListOf<String>() // 可选:初始化为预置歌曲
private var currentIndex = 0
override fun onCreate() {
super.onCreate()
FileLogger.init(this)
FileLogger.i(TAG, "MusicService 正在创建...")
isRunning.set(true)
mediaSession = MediaSessionCompat(this, TAG).apply {
setCallback(object : MediaSessionCompat.Callback() {
override fun onPlay() {
resume()
}
override fun onPause() {
pause()
}
override fun onSkipToNext() {
if (currentIndex + 1 < playlist.size) {
currentIndex++
play(playlist[currentIndex])
}
}
override fun onSkipToPrevious() {
if (currentIndex - 1 >= 0) {
currentIndex--
play(playlist[currentIndex])
}
}
override fun onStop() {
stopSelf()
}
})
isActive = true
}
startForeground(NOTIFICATION_ID, createNotification("等待播放..."))
Log.i(TAG, "MusicService Created")
setupMediaSession()
startForeground(NOTIFICATION_ID, createNotification("等待播放…"))
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
FileLogger.i(TAG, "MediaService 启动完成")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val command = intent?.getStringExtra("command") ?: return START_NOT_STICKY
Log.i(TAG, "onStartCommand: $command" )
intent?.action?.takeIf { it == Intent.ACTION_MEDIA_BUTTON }?.let {
// 分发给 MediaSession 处理媒体按钮
MediaButtonReceiver.handleIntent(mediaSession, intent)
return START_STICKY
}
val command = intent?.getStringExtra("command") ?: return START_NOT_STICKY
FileLogger.i(TAG, "收到命令: $command")
when (command) {
"play" -> {
val url = intent.getStringExtra("url") ?: return START_NOT_STICKY
"play" -> intent.getStringExtra("url")?.let { url ->
if (!playlist.contains(url)) playlist.add(url)
currentIndex = playlist.indexOf(url)
play(url)
}
"playlist" -> {
val urls = intent.getStringArrayListExtra("urls")?.filter { it.isNotEmpty() } ?: return START_NOT_STICKY
Log.i(TAG, "后台服务播放 playlist: $urls" )
if (urls.isNotEmpty()) {
playlist.clear()
playlist.addAll(urls)
currentIndex = 0
play(playlist[currentIndex])
} else {
Log.w(TAG, "playlist 命令收到空列表")
}
}
"pause" -> pause()
"resume" -> resume()
"stop" -> stopSelf()
"next" -> {
if (currentIndex + 1 < playlist.size) {
currentIndex++
play(playlist[currentIndex])
}
}
"previous" -> {
if (currentIndex - 1 >= 0) {
currentIndex--
play(playlist[currentIndex])
}
}
"setVolume" -> {
val volume = intent.getFloatExtra("volume", 1.0f).coerceIn(0f, 1f)
mediaPlayer?.setVolume(volume, volume)
}
"next" -> if (currentIndex + 1 < playlist.size) play(playlist[++currentIndex])
"previous" -> if (currentIndex - 1 >= 0) play(playlist[--currentIndex])
}
return START_STICKY
}
private fun setupMediaSession() {
mediaSession = MediaSessionCompat(this, "MusicMediaSession").apply {
setFlags(
MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or
MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
)
// 回调中处理具体操作
setCallback(object : MediaSessionCompat.Callback() {
override fun onPlay() { FileLogger.i(TAG, "onPlay 通知!"); resume()}
override fun onPause() { FileLogger.i(TAG, "onPause 通知!"); pause()}
override fun onSkipToNext(){ FileLogger.i(TAG, "onSkipToNext 通知!"); playNext()}
override fun onSkipToPrevious() { FileLogger.i(TAG, "onSkipToNext 通知!"); playPrevious()}
})
// 初始播放状态
setPlaybackState(
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)
.build()
)
isActive = true
}
// 将 token 暴露给 MediaBrowserServiceCompat
setSessionToken(mediaSession.sessionToken)
}
private fun createNotification(contentText: String): Notification {
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
manager.createNotificationChannel(
NotificationChannel(CHANNEL_ID, "播放控制", NotificationManager.IMPORTANCE_LOW)
)
}
val prevIntent = MediaButtonReceiver.buildMediaButtonPendingIntent(
this, PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
)
val playPauseIntent = MediaButtonReceiver.buildMediaButtonPendingIntent(
this, if (currentStatus) PlaybackStateCompat.ACTION_PAUSE else PlaybackStateCompat.ACTION_PLAY
)
val nextIntent = MediaButtonReceiver.buildMediaButtonPendingIntent(
this, PlaybackStateCompat.ACTION_SKIP_TO_NEXT
)
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("DeepSound")
.setContentText(contentText)
.setSmallIcon(android.R.drawable.ic_media_play)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setStyle(
androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(mediaSession.sessionToken)
.setShowActionsInCompactView(0, 1, 2)
)
.addAction(android.R.drawable.ic_media_previous, "上一首", prevIntent)
.addAction(
if (currentStatus) android.R.drawable.ic_media_pause else android.R.drawable.ic_media_play,
if (currentStatus) "暂停" else "播放",
playPauseIntent
)
.addAction(android.R.drawable.ic_media_next, "下一首", nextIntent)
.setOngoing(currentStatus)
.build()
}
/**
* 设置播放状态
*/
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
}
}
private fun play(url: String) {
Log.i(TAG, "播放音乐: $url")
releaseMediaPlayer()
currentStatus = true;
currentUrlInternal = url;
mediaPlayer?.release()
currentStatus = true
currentUrlInternal = url
mediaPlayer = MediaPlayer().apply {
setDataSource(url)
setAudioStreamType(AudioManager.STREAM_MUSIC)
setOnPreparedListener {
start()
currentStatus = true
updateNotification("正在播放")
Log.i(TAG, "开始播放")
}
setOnCompletionListener {
Log.i(TAG, "播放完成")
// 播放下一首
if (currentIndex + 1 < playlist.size) {
currentIndex++
play(playlist[currentIndex]) // 播放下一首
} else {
currentStatus = false
stopSelf() // 如果没有更多歌曲了,可以停止服务
}
FileLogger.i(TAG, "开始播放: $url")
}
setOnCompletionListener { playNextOrStop() }
setOnErrorListener { _, what, extra ->
Log.e(TAG, "播放错误: what=$what extra=$extra")
currentStatus = false
stopSelf()
FileLogger.e(TAG, "播放错误 what=$what extra=$extra")
true
}
prepareAsync()
}
setPlaybackState(PlaybackStateCompat.STATE_PAUSED);
}
private fun pause() {
mediaPlayer?.let {
if (it.isPlaying) {
it.pause()
currentStatus= false
updateNotification("已暂停")
Log.i(TAG, "暂停播放")
}
mediaPlayer?.takeIf { it.isPlaying }?.apply {
pause()
currentStatus = false
mediaSession.setPlaybackState(
PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_PAUSED, currentPosition.toLong(), 1.0f)
.build()
)
updateNotification("已暂停")
setPlaybackState(PlaybackStateCompat.STATE_PLAYING);
FileLogger.i(TAG, "已暂停")
}
}
private fun resume() {
mediaPlayer?.let {
if (!it.isPlaying) {
it.start()
currentStatus = true
updateNotification("继续播放")
Log.i(TAG, "继续播放")
}
mediaPlayer?.takeIf { !it.isPlaying }?.apply {
start()
currentStatus = true
mediaSession.setPlaybackState(
PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_PLAYING, currentPosition.toLong(), 1.0f)
.build()
)
updateNotification("继续播放")
setPlaybackState(PlaybackStateCompat.STATE_PAUSED);
FileLogger.i(TAG, "继续播放")
}
}
private fun releaseMediaPlayer() {
mediaPlayer?.apply {
stop()
release()
}
mediaPlayer = null
private fun playNext() {
if (playlist.isEmpty()) return
currentIndex = (currentIndex + 1) % playlist.size
play(playlist[currentIndex])
}
private fun updateNotification(contentText: String) {
val notification = createNotification(contentText)
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.notify(NOTIFICATION_ID, notification)
private fun playPrevious() {
if (playlist.isEmpty()) return
currentIndex = if (currentIndex - 1 < 0) playlist.size - 1 else currentIndex - 1
play(playlist[currentIndex])
}
private fun playNextOrStop() {
if (currentIndex + 1 < playlist.size) play(playlist[++currentIndex])
else stopSelf()
}
private fun createNotification(contentText: String): Notification {
private fun updateNotification(contentText: String) {
val notification = createNotification(contentText)
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"音乐播放服务",
NotificationManager.IMPORTANCE_LOW
)
manager.createNotificationChannel(channel)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
manager.notify(NOTIFICATION_ID, notification)
} else {
@Suppress("DEPRECATION")
startForeground(NOTIFICATION_ID, notification)
}
}
// 修复 pendingIntent:打开 App 主 Activity
val activityIntent = packageManager.getLaunchIntentForPackage(packageName)
val pendingIntent = PendingIntent.getActivity(
this,
0,
activityIntent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
override fun onGetRoot(
clientPackageName: String, clientUid: Int, rootHints: Bundle?
) = BrowserRoot("root_id", null)
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("DeepSound")
.setContentText(contentText)
.setSmallIcon(android.R.drawable.ic_media_play)
.setContentIntent(pendingIntent)
.setOngoing(true)
.setStyle(
androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(mediaSession.sessionToken)
.setShowActionsInCompactView(0, 1, 2)
)
.addAction(
NotificationCompat.Action(
android.R.drawable.ic_media_previous, "上一曲",
androidx.media.session.MediaButtonReceiver.buildMediaButtonPendingIntent(
this,
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
)
)
)
.addAction(
NotificationCompat.Action(
if (currentStatus) android.R.drawable.ic_media_pause else android.R.drawable.ic_media_play,
if (currentStatus) "暂停" else "播放",
androidx.media.session.MediaButtonReceiver.buildMediaButtonPendingIntent(
this,
PlaybackStateCompat.ACTION_PLAY_PAUSE
)
)
)
.addAction(
NotificationCompat.Action(
android.R.drawable.ic_media_next, "下一曲",
androidx.media.session.MediaButtonReceiver.buildMediaButtonPendingIntent(
this,
PlaybackStateCompat.ACTION_SKIP_TO_NEXT
)
)
)
.build()
}
override fun onLoadChildren(
parentId: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>>
) = result.sendResult(mutableListOf())
override fun onDestroy() {
Log.i(TAG, "服务销毁,释放播放器")
releaseMediaPlayer()
mediaPlayer?.release()
stopForeground(true)
isRunning.set(false)
super.onDestroy()

Loading…
Cancel
Save