|
|
|
@ -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() |
|
|
|
|