Browse Source

上传音乐服务

newdev_shunjiawei
liwei1dao 1 year ago
parent
commit
e363fab5b2
  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: [ children: [
Text( Text(
"${city} ${now["date"]}", "${city} ${now["date"]}",
style: TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Colors.white, color: Colors.white,
@ -734,7 +734,7 @@ class MessageBubble extends GetView<AgentController> {
children: [ children: [
Text( Text(
"${now["temperature"]}°", "${now["temperature"]}°",
style: TextStyle( style: const TextStyle(
fontSize: 36, fontSize: 36,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white, 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" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yunqiinnovation.music_service"> package="com.yunqiinnovation.music_service">
<!-- 基础前台服务权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- Android 14 及以上需要声明具体的前台服务类型权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<application> <application>
<service <service
android:name=".MusicService" android:name=".MusicService"
android:exported="false" android:exported="true">
android:enabled="true" <!-- 让系统知道这是媒体浏览服务 -->
android:foregroundServiceType="mediaPlayback" /> <intent-filter>
<!-- 用于接收媒体按钮(耳机按钮、蓝牙播放控制) --> <action android:name="android.media.browse.MediaBrowserService" />
<receiver </intent-filter>
android:name="androidx.media.session.MediaButtonReceiver" <!-- 让系统知道这里能处理媒体按键 -->
android:exported="false"> <intent-filter>
<intent-filter> <action android:name="android.intent.action.MEDIA_BUTTON" />
<action android:name="android.intent.action.MEDIA_BUTTON" /> </intent-filter>
</intent-filter> </service>
</receiver>
<receiver
android:name="androidx.media.session.MediaButtonReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
</application> </application>
</manifest> </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 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.Context
import android.content.Intent import android.content.Intent
import android.media.AudioManager import android.media.AudioManager
import android.media.MediaPlayer import android.media.MediaPlayer
import android.os.Build import android.os.Build
import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import android.util.Log import android.support.v4.media.MediaBrowserCompat
import androidx.core.app.NotificationCompat
import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat 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 com.yunqiinnovation.deepvoice_utils.FileLogger
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
class MusicService : Service() { class MusicService : MediaBrowserServiceCompat() {
companion object { companion object {
private const val TAG = "MusicService" private const val TAG = "MusicService"
@ -22,242 +27,240 @@ class MusicService : Service() {
private const val NOTIFICATION_ID = 1002 private const val NOTIFICATION_ID = 1002
private val isRunning = AtomicBoolean(false) private val isRunning = AtomicBoolean(false)
private var currentStatus: Boolean = false
private var currentUrlInternal: String? = null private var currentUrlInternal: String? = null
private var currentStatus = false
private var playlist = mutableListOf<String>()
private var currentIndex = 0
fun isServiceRunning() = isRunning.get()
fun isServiceRunning(): Boolean = isRunning.get() fun isPlaying() = currentStatus
fun isPlaying(): Boolean = currentStatus
fun getCurrentUrl(): String? = currentUrlInternal fun getCurrentUrl(): String? = currentUrlInternal
} }
private lateinit var mediaSession: MediaSessionCompat private lateinit var mediaSession: MediaSessionCompat
private lateinit var audioManager: AudioManager
private var mediaPlayer: MediaPlayer? = null private var mediaPlayer: MediaPlayer? = null
private val playlist = mutableListOf<String>() // 可选:初始化为预置歌曲
private var currentIndex = 0
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
FileLogger.init(this) FileLogger.init(this)
FileLogger.i(TAG, "MusicService 正在创建...")
isRunning.set(true) isRunning.set(true)
mediaSession = MediaSessionCompat(this, TAG).apply { setupMediaSession()
setCallback(object : MediaSessionCompat.Callback() { startForeground(NOTIFICATION_ID, createNotification("等待播放…"))
override fun onPlay() { audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
resume() FileLogger.i(TAG, "MediaService 启动完成")
}
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")
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val command = intent?.getStringExtra("command") ?: return START_NOT_STICKY intent?.action?.takeIf { it == Intent.ACTION_MEDIA_BUTTON }?.let {
Log.i(TAG, "onStartCommand: $command" ) // 分发给 MediaSession 处理媒体按钮
MediaButtonReceiver.handleIntent(mediaSession, intent)
return START_STICKY
}
val command = intent?.getStringExtra("command") ?: return START_NOT_STICKY
FileLogger.i(TAG, "收到命令: $command")
when (command) { when (command) {
"play" -> { "play" -> intent.getStringExtra("url")?.let { url ->
val url = intent.getStringExtra("url") ?: return START_NOT_STICKY
if (!playlist.contains(url)) playlist.add(url) if (!playlist.contains(url)) playlist.add(url)
currentIndex = playlist.indexOf(url) currentIndex = playlist.indexOf(url)
play(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() "pause" -> pause()
"resume" -> resume() "resume" -> resume()
"stop" -> stopSelf() "stop" -> stopSelf()
"next" -> { "next" -> if (currentIndex + 1 < playlist.size) play(playlist[++currentIndex])
if (currentIndex + 1 < playlist.size) { "previous" -> if (currentIndex - 1 >= 0) play(playlist[--currentIndex])
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)
}
} }
return START_STICKY 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) { private fun play(url: String) {
Log.i(TAG, "播放音乐: $url") mediaPlayer?.release()
releaseMediaPlayer() currentStatus = true
currentStatus = true; currentUrlInternal = url
currentUrlInternal = url;
mediaPlayer = MediaPlayer().apply { mediaPlayer = MediaPlayer().apply {
setDataSource(url) setDataSource(url)
setAudioStreamType(AudioManager.STREAM_MUSIC) setAudioStreamType(AudioManager.STREAM_MUSIC)
setOnPreparedListener { setOnPreparedListener {
start() start()
currentStatus = true
updateNotification("正在播放") updateNotification("正在播放")
Log.i(TAG, "开始播放") FileLogger.i(TAG, "开始播放: $url")
}
setOnCompletionListener {
Log.i(TAG, "播放完成")
// 播放下一首
if (currentIndex + 1 < playlist.size) {
currentIndex++
play(playlist[currentIndex]) // 播放下一首
} else {
currentStatus = false
stopSelf() // 如果没有更多歌曲了,可以停止服务
}
} }
setOnCompletionListener { playNextOrStop() }
setOnErrorListener { _, what, extra -> setOnErrorListener { _, what, extra ->
Log.e(TAG, "播放错误: what=$what extra=$extra") FileLogger.e(TAG, "播放错误 what=$what extra=$extra")
currentStatus = false
stopSelf()
true true
} }
prepareAsync() prepareAsync()
} }
setPlaybackState(PlaybackStateCompat.STATE_PAUSED);
} }
private fun pause() { private fun pause() {
mediaPlayer?.let { mediaPlayer?.takeIf { it.isPlaying }?.apply {
if (it.isPlaying) { pause()
it.pause() currentStatus = false
currentStatus= false mediaSession.setPlaybackState(
updateNotification("已暂停") PlaybackStateCompat.Builder()
Log.i(TAG, "暂停播放") .setState(PlaybackStateCompat.STATE_PAUSED, currentPosition.toLong(), 1.0f)
} .build()
)
updateNotification("已暂停")
setPlaybackState(PlaybackStateCompat.STATE_PLAYING);
FileLogger.i(TAG, "已暂停")
} }
} }
private fun resume() { private fun resume() {
mediaPlayer?.let { mediaPlayer?.takeIf { !it.isPlaying }?.apply {
if (!it.isPlaying) { start()
it.start() currentStatus = true
currentStatus = true mediaSession.setPlaybackState(
updateNotification("继续播放") PlaybackStateCompat.Builder()
Log.i(TAG, "继续播放") .setState(PlaybackStateCompat.STATE_PLAYING, currentPosition.toLong(), 1.0f)
} .build()
)
updateNotification("继续播放")
setPlaybackState(PlaybackStateCompat.STATE_PAUSED);
FileLogger.i(TAG, "继续播放")
} }
} }
private fun releaseMediaPlayer() { private fun playNext() {
mediaPlayer?.apply { if (playlist.isEmpty()) return
stop() currentIndex = (currentIndex + 1) % playlist.size
release() play(playlist[currentIndex])
}
mediaPlayer = null
} }
private fun updateNotification(contentText: String) { private fun playPrevious() {
val notification = createNotification(contentText) if (playlist.isEmpty()) return
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager currentIndex = if (currentIndex - 1 < 0) playlist.size - 1 else currentIndex - 1
manager.notify(NOTIFICATION_ID, notification) 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 val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { manager.notify(NOTIFICATION_ID, notification)
val channel = NotificationChannel( } else {
CHANNEL_ID, @Suppress("DEPRECATION")
"音乐播放服务", startForeground(NOTIFICATION_ID, notification)
NotificationManager.IMPORTANCE_LOW
)
manager.createNotificationChannel(channel)
} }
}
// 修复 pendingIntent:打开 App 主 Activity override fun onGetRoot(
val activityIntent = packageManager.getLaunchIntentForPackage(packageName) clientPackageName: String, clientUid: Int, rootHints: Bundle?
val pendingIntent = PendingIntent.getActivity( ) = BrowserRoot("root_id", null)
this,
0,
activityIntent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
return NotificationCompat.Builder(this, CHANNEL_ID) override fun onLoadChildren(
.setContentTitle("DeepSound") parentId: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>>
.setContentText(contentText) ) = result.sendResult(mutableListOf())
.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 onDestroy() { override fun onDestroy() {
Log.i(TAG, "服务销毁,释放播放器") mediaPlayer?.release()
releaseMediaPlayer()
stopForeground(true) stopForeground(true)
isRunning.set(false) isRunning.set(false)
super.onDestroy() super.onDestroy()

Loading…
Cancel
Save