Improve PlayerService lifecycle to accommodate a proper queue persistent storing

This commit is contained in:
vfsfitvnm 2022-07-04 22:03:11 +02:00
parent 0655e3efd5
commit 36bf5b17a6
3 changed files with 112 additions and 57 deletions

View file

@ -109,7 +109,7 @@ interface Database {
fun insert(song: Song): Long
@Insert(onConflict = OnConflictStrategy.ABORT)
fun insert(queuedMediaItem: QueuedMediaItem)
fun insert(queuedMediaItems: List<QueuedMediaItem>)
@Transaction
fun insert(mediaItem: MediaItem, block: (Song) -> Song = { it }) {

View file

@ -7,6 +7,7 @@ import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Configuration
import android.graphics.Color
import android.net.Uri
@ -61,7 +62,8 @@ import kotlin.math.roundToInt
import kotlin.system.exitProcess
class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback {
class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback,
SharedPreferences.OnSharedPreferenceChangeListener {
private lateinit var mediaSession: MediaSessionCompat
private lateinit var cache: SimpleCache
private lateinit var player: ExoPlayer
@ -93,20 +95,34 @@ class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback
private var hack: Hack? = null
private var isTaskRemoved = false
private var isVolumeNormalizationEnabled = false
private var isPersistentQueueEnabled = false
private val handler = Handler(Looper.getMainLooper())
private val mediaControllerCallback = object : MediaControllerCompat.Callback() {
override fun onPlaybackStateChanged(state: PlaybackStateCompat?) {
when (state?.state) {
STATE_PLAYING -> {
ContextCompat.startForegroundService(this@PlayerService, intent<PlayerService>())
ContextCompat.startForegroundService(
this@PlayerService,
intent<PlayerService>()
)
startForeground(NotificationId, notification())
hack?.stop()
}
STATE_PAUSED -> {
if (player.playbackState == Player.STATE_ENDED || !player.playWhenReady) {
stopForeground(false)
if (isPersistentQueueEnabled) {
if (isTaskRemoved) {
stopForeground(false)
}
} else {
stopForeground(false)
}
notificationManager.notify(NotificationId, notification())
@ -133,9 +149,16 @@ class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback
return binder
}
override fun onRebind(intent: Intent?) {
isTaskRemoved = false
hack?.stop()
hack = null
super.onRebind(intent)
}
override fun onUnbind(intent: Intent?): Boolean {
hack = Hack()
return super.onUnbind(intent)
return true
}
override fun onCreate() {
@ -150,7 +173,14 @@ class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback
createNotificationChannel()
getSharedPreferences(
Preferences.fileName,
Context.MODE_PRIVATE
).registerOnSharedPreferenceChangeListener(this)
val preferences = Preferences()
isPersistentQueueEnabled = preferences.persistentQueue
isVolumeNormalizationEnabled = preferences.volumeNormalization
val cacheEvictor = LeastRecentlyUsedCacheEvictor(preferences.exoPlayerDiskCacheMaxSizeBytes)
cache = SimpleCache(cacheDir, cacheEvictor, StandaloneDatabaseProvider(this))
@ -172,37 +202,10 @@ class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback
player.repeatMode = preferences.repeatMode
player.skipSilenceEnabled = preferences.skipSilence
player.playWhenReady = true
player.addListener(this)
player.addAnalyticsListener(PlaybackStatsListener(false, this))
if (preferences.persistentQueue) {
coroutineScope.launch(Dispatchers.IO) {
val queuedSong = Database.queue()
Database.clearQueue()
if (queuedSong.isEmpty()) return@launch
val index = queuedSong.indexOfFirst { it.position != null }.coerceAtLeast(0)
withContext(Dispatchers.Main) {
player.setMediaItems(
queuedSong
.map(QueuedMediaItem::mediaItem)
.map { mediaItem ->
mediaItem.buildUpon()
.setUri(mediaItem.mediaId)
.setCustomCacheKey(mediaItem.mediaId)
.build()
},
true
)
player.seekTo(index, queuedSong[index].position ?: 0)
player.playWhenReady = false
player.prepare()
}
}
}
maybeRestorePlayerQueue()
mediaSession = MediaSessionCompat(baseContext, "PlayerService")
mediaSession.setCallback(SessionCallback(player))
@ -213,36 +216,24 @@ class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
MediaButtonReceiver.handleIntent(mediaSession, intent)
return START_NOT_STICKY
return START_STICKY
}
override fun onTaskRemoved(rootIntent: Intent?) {
isTaskRemoved = true
if (!player.playWhenReady) {
notificationManager.cancel(NotificationId)
stopSelf()
}
super.onTaskRemoved(rootIntent)
}
override fun onDestroy() {
if (Preferences().persistentQueue) {
val mediaItems = player.currentTimeline.mediaItems
val mediaItemIndex = player.currentMediaItemIndex
val mediaItemPosition = player.currentPosition
maybeSavePlayerQueue()
query {
Database.clearQueue()
mediaItems.forEachIndexed { index, mediaItem ->
Database.insert(
QueuedMediaItem(
mediaItem = mediaItem,
position = if (index == mediaItemIndex) mediaItemPosition else null
)
)
}
}
}
getSharedPreferences(
Preferences.fileName,
Context.MODE_PRIVATE
).unregisterOnSharedPreferenceChangeListener(this)
hack?.stop()
hack = null
@ -290,8 +281,59 @@ class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback
}
}
private fun maybeSavePlayerQueue() {
if (!isPersistentQueueEnabled) return
val mediaItems = player.currentTimeline.mediaItems
val mediaItemIndex = player.currentMediaItemIndex
val mediaItemPosition = player.currentPosition
mediaItems.mapIndexed { index, mediaItem ->
QueuedMediaItem(
mediaItem = mediaItem,
position = if (index == mediaItemIndex) mediaItemPosition else null
)
}.let { queuedMediaItems ->
query {
Database.clearQueue()
Database.insert(queuedMediaItems)
}
}
}
private fun maybeRestorePlayerQueue() {
if (!isPersistentQueueEnabled) return
coroutineScope.launch(Dispatchers.IO) {
val queuedSong = Database.queue()
Database.clearQueue()
if (queuedSong.isEmpty()) return@launch
val index = queuedSong.indexOfFirst { it.position != null }.coerceAtLeast(0)
withContext(Dispatchers.Main) {
player.setMediaItems(
queuedSong
.map { mediaItem ->
mediaItem.mediaItem.buildUpon()
.setUri(mediaItem.mediaItem.mediaId)
.setCustomCacheKey(mediaItem.mediaItem.mediaId)
.build()
},
true
)
player.seekTo(index, queuedSong[index].position ?: 0)
player.prepare()
ContextCompat.startForegroundService(this@PlayerService, intent<PlayerService>())
startForeground(NotificationId, notification())
}
}
}
private fun normalizeVolume() {
if (Preferences().volumeNormalization) {
if (isVolumeNormalizationEnabled) {
player.volume = player.currentMediaItem?.let { mediaItem ->
songPendingLoudnessDb.getOrElse(mediaItem.mediaId) {
mediaItem.mediaMetadata.extras?.getFloatOrNull("loudnessDb")
@ -349,6 +391,15 @@ class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback
mediaSession.setPlaybackState(stateBuilder.build())
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
when (key) {
Preferences.Keys.persistentQueue -> isPersistentQueueEnabled =
sharedPreferences.getBoolean(key, isPersistentQueueEnabled)
Preferences.Keys.volumeNormalization -> isVolumeNormalizationEnabled =
sharedPreferences.getBoolean(key, isVolumeNormalizationEnabled)
}
}
private fun notification(): Notification {
fun NotificationCompat.Builder.addMediaAction(
@DrawableRes resId: Int,
@ -510,7 +561,11 @@ class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback
ringBuffer.append(videoId to url.toUri())
dataSpec.withUri(url.toUri())
.subrange(dataSpec.uriPositionOffset, chunkLength)
} ?: throw PlaybackException(null, null, PlaybackException.ERROR_CODE_REMOTE_ERROR)
} ?: throw PlaybackException(
null,
null,
PlaybackException.ERROR_CODE_REMOTE_ERROR
)
}
}
}
@ -622,7 +677,7 @@ class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback
}
}
private inner class SessionCallback(private val player: Player) : MediaSessionCompat.Callback() {
private class SessionCallback(private val player: Player) : MediaSessionCompat.Callback() {
override fun onPlay() = player.play()
override fun onPause() = player.pause()
override fun onSkipToPrevious() = player.seekToPrevious()
@ -637,7 +692,7 @@ class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback
}
// https://stackoverflow.com/q/53502244/16885569
private inner class Hack: Runnable {
private inner class Hack : Runnable {
private var isStarted = false
private val intervalMs = 30_000L

View file

@ -72,7 +72,7 @@ class Preferences(
var persistentQueue = initialPersistentQueue
set(value) = edit { putBoolean(Keys.persistentQueue, value) }
private object Keys {
object Keys {
const val colorPaletteMode = "colorPaletteMode"
const val searchFilter = "searchFilter"
const val repeatMode = "repeatMode"