feat: rewrite RadioPlaybackService as MediaLibraryService with browse tree

Made-with: Cursor
This commit is contained in:
cottongin
2026-03-18 06:10:17 -04:00
parent 04225de888
commit 00f4da20b6
3 changed files with 286 additions and 211 deletions

View File

@@ -1,65 +0,0 @@
package xyz.cottongin.radio247.service
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import androidx.core.app.NotificationCompat
import android.support.v4.media.session.MediaSessionCompat
import xyz.cottongin.radio247.R
import xyz.cottongin.radio247.audio.IcyMetadata
import xyz.cottongin.radio247.audio.MetadataFormatter
import xyz.cottongin.radio247.data.model.Station
class NotificationHelper(private val context: Context) {
companion object {
const val CHANNEL_ID = "radio_playback"
const val NOTIFICATION_ID = 1
}
fun createChannel() {
val channel = NotificationChannel(
CHANNEL_ID,
"Radio Playback",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "24/7 Radio playback"
}
context.getSystemService(NotificationManager::class.java)
.createNotificationChannel(channel)
}
fun buildNotification(
station: Station,
metadata: IcyMetadata?,
isReconnecting: Boolean,
mediaSession: MediaSessionCompat,
stopPendingIntent: PendingIntent
): Notification {
return NotificationCompat.Builder(context, CHANNEL_ID)
.setContentTitle(station.name)
.setContentText(
when {
isReconnecting -> "Reconnecting..."
metadata != null -> MetadataFormatter.formatTrackInfo(metadata, fallback = "Playing")
else -> "Playing"
}
)
.setSmallIcon(R.drawable.ic_notification)
.setOngoing(true)
.setStyle(
androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(mediaSession.sessionToken)
.setShowActionsInCompactView(0)
)
.addAction(
NotificationCompat.Action.Builder(
android.R.drawable.ic_media_pause,
"Stop",
stopPendingIntent
).build()
)
.build()
}
}

View File

@@ -1,18 +1,27 @@
package xyz.cottongin.radio247.service package xyz.cottongin.radio247.service
import android.app.PendingIntent import android.app.PendingIntent
import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.Network import android.net.Network
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.net.NetworkRequest import android.net.NetworkRequest
import android.os.IBinder import android.os.Bundle
import android.os.PowerManager import android.os.PowerManager
import android.util.Log import android.util.Log
import androidx.lifecycle.LifecycleService import com.google.common.util.concurrent.SettableFuture
import android.support.v4.media.session.MediaSessionCompat import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.session.CommandButton
import androidx.media3.session.LibraryResult
import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionResult
import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import xyz.cottongin.radio247.RadioApplication import xyz.cottongin.radio247.RadioApplication
import xyz.cottongin.radio247.audio.AudioEngine import xyz.cottongin.radio247.audio.AudioEngine
import xyz.cottongin.radio247.audio.AudioEngineEvent import xyz.cottongin.radio247.audio.AudioEngineEvent
@@ -25,6 +34,7 @@ import xyz.cottongin.radio247.data.db.StationDao
import xyz.cottongin.radio247.data.model.ConnectionSpan import xyz.cottongin.radio247.data.model.ConnectionSpan
import xyz.cottongin.radio247.data.model.ListeningSession import xyz.cottongin.radio247.data.model.ListeningSession
import xyz.cottongin.radio247.data.model.MetadataSnapshot import xyz.cottongin.radio247.data.model.MetadataSnapshot
import xyz.cottongin.radio247.data.model.Playlist
import xyz.cottongin.radio247.data.model.Station import xyz.cottongin.radio247.data.model.Station
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -42,7 +52,7 @@ import kotlinx.coroutines.Job
class ConnectionFailedException(cause: Throwable) : Exception("Connection failed", cause) class ConnectionFailedException(cause: Throwable) : Exception("Connection failed", cause)
class RadioPlaybackService : LifecycleService() { class RadioPlaybackService : MediaLibraryService() {
companion object { companion object {
private const val TAG = "RadioPlayback" private const val TAG = "RadioPlayback"
const val ACTION_PLAY = "xyz.cottongin.radio247.PLAY" const val ACTION_PLAY = "xyz.cottongin.radio247.PLAY"
@@ -64,11 +74,12 @@ class RadioPlaybackService : LifecycleService() {
private val connectionSpanDao: ConnectionSpanDao get() = database.connectionSpanDao() private val connectionSpanDao: ConnectionSpanDao get() = database.connectionSpanDao()
private val metadataSnapshotDao: MetadataSnapshotDao get() = database.metadataSnapshotDao() private val metadataSnapshotDao: MetadataSnapshotDao get() = database.metadataSnapshotDao()
private val notificationHelper = NotificationHelper(this)
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
private val reconnectionMutex = Mutex() private val reconnectionMutex = Mutex()
private var mediaSession: MediaSessionCompat? = null private var mediaSession: MediaLibraryService.MediaLibrarySession? = null
private var playerAdapter: RadioPlayerAdapter? = null
private val seekToLiveCommand = SessionCommand("SEEK_TO_LIVE", Bundle.EMPTY)
private var engine: AudioEngine? = null private var engine: AudioEngine? = null
private var wakeLock: PowerManager.WakeLock? = null private var wakeLock: PowerManager.WakeLock? = null
private var wifiLock: android.net.wifi.WifiManager.WifiLock? = null private var wifiLock: android.net.wifi.WifiManager.WifiLock? = null
@@ -101,12 +112,16 @@ class RadioPlaybackService : LifecycleService() {
controller.updateState(newState) controller.updateState(newState)
} }
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibraryService.MediaLibrarySession? {
return mediaSession
}
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
notificationHelper.createChannel()
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
when (intent?.action) { when (intent?.action) {
ACTION_PLAY -> { ACTION_PLAY -> {
val stationId = intent.getLongExtra(EXTRA_STATION_ID, -1L) val stationId = intent.getLongExtra(EXTRA_STATION_ID, -1L)
@@ -150,8 +165,6 @@ class RadioPlaybackService : LifecycleService() {
} }
} }
override fun onBind(intent: Intent): IBinder? = null
override fun onDestroy() { override fun onDestroy() {
cleanupResources() cleanupResources()
serviceScope.cancel() serviceScope.cancel()
@@ -164,6 +177,8 @@ class RadioPlaybackService : LifecycleService() {
releaseLocks() releaseLocks()
mediaSession?.release() mediaSession?.release()
mediaSession = null mediaSession = null
playerAdapter?.clearState()
playerAdapter = null
unregisterNetworkCallback() unregisterNetworkCallback()
controller.updateLatency(0) controller.updateLatency(0)
} }
@@ -181,7 +196,8 @@ class RadioPlaybackService : LifecycleService() {
acquireLocks() acquireLocks()
ensureMediaSession() ensureMediaSession()
startForegroundWithPlaceholder(station) playerAdapter?.updateStation(station)
playerAdapter?.updatePlaybackState(PlaybackState.Connecting(station))
try { try {
val urls = app.streamResolver.resolveUrls(station) val urls = app.streamResolver.resolveUrls(station)
@@ -200,15 +216,15 @@ class RadioPlaybackService : LifecycleService() {
val currentState = controller.state.value val currentState = controller.state.value
when { when {
currentState is PlaybackState.Paused -> { currentState is PlaybackState.Paused -> {
updateNotification(station, currentMetadata, isReconnecting = false, isPaused = true) playerAdapter?.updatePlaybackState(PlaybackState.Paused(station = station, metadata = currentMetadata, sessionStartedAt = sessionStartedAt))
} }
else -> { else -> {
val isActiveJob = playJob == coroutineContext[Job] val isActiveJob = playJob == coroutineContext[Job]
if (isActiveJob) { if (isActiveJob) {
transition(PlaybackState.Idle) transition(PlaybackState.Idle)
playerAdapter?.updatePlaybackState(PlaybackState.Idle)
endListeningSession() endListeningSession()
cleanupResources() cleanupResources()
stopForeground(Service.STOP_FOREGROUND_REMOVE)
stopSelf() stopSelf()
} }
} }
@@ -256,20 +272,199 @@ class RadioPlaybackService : LifecycleService() {
} }
private fun ensureMediaSession() { private fun ensureMediaSession() {
if (mediaSession == null) { if (mediaSession != null) return
mediaSession = MediaSessionCompat(this, "Radio247").apply {
setFlags( val adapter = RadioPlayerAdapter(
MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or onPlay = {
MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS val state = controller.state.value
val station = when (state) {
is PlaybackState.Playing -> state.station
is PlaybackState.Paused -> state.station
is PlaybackState.Connecting -> state.station
is PlaybackState.Reconnecting -> state.station
is PlaybackState.Idle -> null
}
station?.let { controller.play(it) }
},
onStop = { controller.stop() }
)
playerAdapter = adapter
val sessionActivityIntent = packageManager
.getLaunchIntentForPackage(packageName)
?.let { intent ->
PendingIntent.getActivity(
this, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
) )
setCallback(object : MediaSessionCompat.Callback() {
override fun onStop() {
this@RadioPlaybackService.controller.stop()
}
})
isActive = true
} }
val seekToLiveButton = CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD)
.setDisplayName("Live")
.setSessionCommand(seekToLiveCommand)
.build()
mediaSession = MediaLibrarySession.Builder(this, adapter, LibrarySessionCallback())
.also { builder ->
sessionActivityIntent?.let { builder.setSessionActivity(it) }
}
.setCustomLayout(listOf(seekToLiveButton))
.build()
}
private inner class LibrarySessionCallback : MediaLibraryService.MediaLibrarySession.Callback {
override fun onGetLibraryRoot(
session: MediaLibraryService.MediaLibrarySession,
browser: MediaSession.ControllerInfo,
params: MediaLibraryService.LibraryParams?
): ListenableFuture<LibraryResult<MediaItem>> {
val root = MediaItem.Builder()
.setMediaId("root")
.setMediaMetadata(
MediaMetadata.Builder()
.setIsBrowsable(true)
.setIsPlayable(false)
.setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_MIXED)
.build()
)
.build()
return Futures.immediateFuture(LibraryResult.ofItem(root, params))
} }
override fun onGetChildren(
session: MediaLibraryService.MediaLibrarySession,
browser: MediaSession.ControllerInfo,
parentId: String,
page: Int,
pageSize: Int,
params: MediaLibraryService.LibraryParams?
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
val future = SettableFuture.create<LibraryResult<ImmutableList<MediaItem>>>()
serviceScope.launch {
try {
val items = when {
parentId == "root" -> buildRootChildren()
parentId == "unsorted" -> buildUnsortedStations()
parentId.startsWith("playlist:") -> {
val playlistId = parentId.removePrefix("playlist:").toLong()
buildPlaylistStations(playlistId)
}
else -> emptyList()
}
future.set(LibraryResult.ofItemList(ImmutableList.copyOf(items), params))
} catch (_: Exception) {
future.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_UNKNOWN))
}
}
return future
}
override fun onCustomCommand(
session: MediaSession,
controller: MediaSession.ControllerInfo,
customCommand: SessionCommand,
args: Bundle
): ListenableFuture<SessionResult> {
if (customCommand.customAction == "SEEK_TO_LIVE") {
handleSeekLive()
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
}
return super.onCustomCommand(session, controller, customCommand, args)
}
override fun onConnect(
session: MediaSession,
controller: MediaSession.ControllerInfo
): MediaSession.ConnectionResult {
val sessionCommands = MediaSession.ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS
.buildUpon()
.add(seekToLiveCommand)
.build()
return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
.setAvailableSessionCommands(sessionCommands)
.build()
}
override fun onAddMediaItems(
mediaSession: MediaSession,
controller: MediaSession.ControllerInfo,
mediaItems: MutableList<MediaItem>
): ListenableFuture<List<MediaItem>> {
val item = mediaItems.firstOrNull()
?: return Futures.immediateFuture(emptyList())
val mediaId = item.mediaId
if (mediaId.startsWith("station:")) {
val stationId = mediaId.removePrefix("station:").toLongOrNull()
if (stationId != null) {
serviceScope.launch {
val station = stationDao.getStationById(stationId)
station?.let { this@RadioPlaybackService.controller.play(it) }
}
}
}
return Futures.immediateFuture(mediaItems)
}
}
private suspend fun buildRootChildren(): List<MediaItem> {
val items = mutableListOf<MediaItem>()
val playlists: List<Playlist> = database.playlistDao().getAllPlaylists().first()
for (playlist in playlists) {
items.add(
MediaItem.Builder()
.setMediaId("playlist:${playlist.id}")
.setMediaMetadata(
MediaMetadata.Builder()
.setTitle(playlist.name)
.setIsBrowsable(true)
.setIsPlayable(false)
.setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_PLAYLISTS)
.build()
)
.build()
)
}
val unsorted = stationDao.getUnsortedStations().first()
if (unsorted.isNotEmpty()) {
items.add(
MediaItem.Builder()
.setMediaId("unsorted")
.setMediaMetadata(
MediaMetadata.Builder()
.setTitle("Unsorted")
.setIsBrowsable(true)
.setIsPlayable(false)
.setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_PLAYLISTS)
.build()
)
.build()
)
}
return items
}
private suspend fun buildPlaylistStations(playlistId: Long): List<MediaItem> {
return stationDao.getStationsByPlaylist(playlistId).first().map { it.toMediaItem() }
}
private suspend fun buildUnsortedStations(): List<MediaItem> {
return stationDao.getUnsortedStations().first().map { it.toMediaItem() }
}
private fun Station.toMediaItem(): MediaItem {
return MediaItem.Builder()
.setMediaId("station:${id}")
.setMediaMetadata(
MediaMetadata.Builder()
.setTitle(name)
.setArtworkUri(defaultArtworkUrl?.let { android.net.Uri.parse(it) })
.setIsPlayable(true)
.setIsBrowsable(false)
.setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC)
.build()
)
.build()
} }
private suspend fun startEngine(station: Station, urls: List<String>) { private suspend fun startEngine(station: Station, urls: List<String>) {
@@ -284,7 +479,7 @@ class RadioPlaybackService : LifecycleService() {
sessionStartedAt = sessionStartedAt sessionStartedAt = sessionStartedAt
) )
) )
updateNotification(station, null, isReconnecting = false) playerAdapter?.updatePlaybackState(PlaybackState.Connecting(station = station))
val bufferMs = app.preferences.bufferMs.first() val bufferMs = app.preferences.bufferMs.first()
engine = AudioEngine(url, bufferMs) engine = AudioEngine(url, bufferMs)
@@ -328,7 +523,7 @@ class RadioPlaybackService : LifecycleService() {
connectionStartedAt = connectionStartedAt connectionStartedAt = connectionStartedAt
) )
) )
updateNotification(station, null, false) playerAdapter?.updatePlaybackState(PlaybackState.Playing(station = station))
controller.updateLatency(engine!!.estimatedLatencyMs) controller.updateLatency(engine!!.estimatedLatencyMs)
} }
is AudioEngineEvent.MetadataChanged -> { is AudioEngineEvent.MetadataChanged -> {
@@ -337,7 +532,16 @@ class RadioPlaybackService : LifecycleService() {
if (playingState is PlaybackState.Playing) { if (playingState is PlaybackState.Playing) {
transition(playingState.copy(metadata = event.metadata)) transition(playingState.copy(metadata = event.metadata))
} }
updateNotification(station, event.metadata, false) serviceScope.launch {
val artUrl = app.albumArtResolver.resolve(
artist = event.metadata.artist,
title = event.metadata.title,
icyStreamUrl = event.metadata.streamUrl,
stationArtworkUrl = station.defaultArtworkUrl
)
val artUri = artUrl?.let { android.net.Uri.parse(it) }
playerAdapter?.updateMetadata(station, event.metadata, artUri)
}
persistMetadataSnapshot(station.id, event.metadata) persistMetadataSnapshot(station.id, event.metadata)
} }
is AudioEngineEvent.StreamInfoReceived -> { is AudioEngineEvent.StreamInfoReceived -> {
@@ -395,7 +599,7 @@ class RadioPlaybackService : LifecycleService() {
attempt = attempt attempt = attempt
) )
) )
updateNotification(station, currentMetadata, isReconnecting = true) playerAdapter?.updatePlaybackState(PlaybackState.Reconnecting(station = station, metadata = currentMetadata, sessionStartedAt = sessionStartedAt, attempt = attempt))
val delayMs = minOf(1000L * (1 shl (attempt - 1)), 30_000L) val delayMs = minOf(1000L * (1 shl (attempt - 1)), 30_000L)
val chunk = 500L val chunk = 500L
var remaining = delayMs var remaining = delayMs
@@ -439,29 +643,6 @@ class RadioPlaybackService : LifecycleService() {
} }
} }
private fun startForegroundWithPlaceholder(station: Station) {
updateNotification(station, null, isReconnecting = false)
}
private fun updateNotification(station: Station, metadata: IcyMetadata?, isReconnecting: Boolean, isPaused: Boolean = false) {
val session = mediaSession ?: return
val stopIntent = Intent(this, RadioPlaybackService::class.java).apply {
action = ACTION_STOP
}
val stopPendingIntent = PendingIntent.getService(
this, 0, stopIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val notification = notificationHelper.buildNotification(
station = station,
metadata = metadata,
isReconnecting = isReconnecting,
mediaSession = session,
stopPendingIntent = stopPendingIntent
)
startForeground(NotificationHelper.NOTIFICATION_ID, notification)
}
private suspend fun persistMetadataSnapshot(stationId: Long, metadata: IcyMetadata) { private suspend fun persistMetadataSnapshot(stationId: Long, metadata: IcyMetadata) {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {

View File

@@ -1,100 +1,74 @@
package xyz.cottongin.radio247.service package xyz.cottongin.radio247.service
import android.os.Looper import android.os.Looper
import androidx.media3.common.BasePlayer
import androidx.media3.common.C
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata import androidx.media3.common.MediaMetadata
import androidx.media3.common.PlaybackParameters
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.Timeline import androidx.media3.common.SimpleBasePlayer
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import xyz.cottongin.radio247.audio.IcyMetadata import xyz.cottongin.radio247.audio.IcyMetadata
import xyz.cottongin.radio247.data.model.Station import xyz.cottongin.radio247.data.model.Station
class RadioPlayerAdapter( class RadioPlayerAdapter(
private val onPlay: () -> Unit, private val onPlay: () -> Unit,
private val onStop: () -> Unit, private val onStop: () -> Unit,
) : BasePlayer() { ) : SimpleBasePlayer(Looper.getMainLooper()) {
private val listeners = mutableListOf<Player.Listener>() @Volatile
private var _playbackState: Int = Player.STATE_IDLE private var _playbackState: Int = Player.STATE_IDLE
@Volatile
private var _playWhenReady: Boolean = false private var _playWhenReady: Boolean = false
@Volatile
private var _mediaMetadata: MediaMetadata = MediaMetadata.EMPTY private var _mediaMetadata: MediaMetadata = MediaMetadata.EMPTY
@Volatile
private var _currentMediaItem: MediaItem? = null private var _currentMediaItem: MediaItem? = null
override fun getApplicationLooper(): Looper = Looper.getMainLooper() private val commands = Player.Commands.Builder()
.add(Player.COMMAND_PLAY_PAUSE)
.add(Player.COMMAND_STOP)
.add(Player.COMMAND_GET_CURRENT_MEDIA_ITEM)
.add(Player.COMMAND_GET_MEDIA_ITEMS_METADATA)
.build()
override fun addListener(listener: Player.Listener) { override fun getState(): SimpleBasePlayer.State {
listeners.add(listener) val mediaItem = _currentMediaItem
} val playlist = if (mediaItem != null) {
listOf(getPlaceholderMediaItemData(mediaItem))
} else {
emptyList()
}
override fun removeListener(listener: Player.Listener) { return SimpleBasePlayer.State.Builder()
listeners.remove(listener) .setAvailableCommands(commands)
} .setPlaybackState(_playbackState)
.setPlayWhenReady(_playWhenReady, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST)
override fun getAvailableCommands(): Player.Commands = .setCurrentMediaItemIndex(if (mediaItem != null) 0 else 0)
Player.Commands.Builder() .setContentPositionMs(0L)
.add(Player.COMMAND_PLAY_PAUSE) .setIsLoading(false)
.add(Player.COMMAND_STOP) .setPlaylist(playlist)
.add(Player.COMMAND_GET_CURRENT_MEDIA_ITEM)
.add(Player.COMMAND_GET_MEDIA_ITEMS_METADATA)
.build() .build()
override fun getPlaybackState(): Int = _playbackState
override fun getPlayWhenReady(): Boolean = _playWhenReady
override fun isPlaying(): Boolean = _playbackState == Player.STATE_READY && _playWhenReady
override fun getMediaMetadata(): MediaMetadata = _mediaMetadata
override fun getCurrentMediaItem(): MediaItem? = _currentMediaItem
override fun play() {
_playWhenReady = true
onPlay()
} }
override fun pause() { override fun handleSetPlayWhenReady(playWhenReady: Boolean): ListenableFuture<*> {
_playWhenReady = playWhenReady
if (playWhenReady) {
onPlay()
} else {
onStop()
}
return Futures.immediateVoidFuture()
}
override fun handleStop(): ListenableFuture<*> {
_playWhenReady = false _playWhenReady = false
onStop() onStop()
return Futures.immediateVoidFuture()
} }
override fun stop() {
_playWhenReady = false
onStop()
}
override fun prepare() {}
override fun setPlayWhenReady(playWhenReady: Boolean) {
if (playWhenReady) play() else pause()
}
override fun release() {
listeners.clear()
}
override fun getPlaybackParameters(): PlaybackParameters = PlaybackParameters.DEFAULT
override fun setPlaybackParameters(playbackParameters: PlaybackParameters) {}
override fun getRepeatMode(): Int = Player.REPEAT_MODE_OFF
override fun setRepeatMode(repeatMode: Int) {}
override fun getShuffleModeEnabled(): Boolean = false
override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) {}
override fun seekToDefaultPosition() {}
override fun seekToDefaultPosition(mediaItemIndex: Int) {}
override fun seekTo(positionMs: Long) {}
override fun seekTo(mediaItemIndex: Int, positionMs: Long) {}
override fun getCurrentTimeline(): Timeline = Timeline.EMPTY
override fun getCurrentMediaItemIndex(): Int = 0
override fun getContentPosition(): Long = 0
override fun getContentDuration(): Long = C.TIME_UNSET
override fun getCurrentPosition(): Long = 0
override fun getDuration(): Long = C.TIME_UNSET
override fun getBufferedPosition(): Long = 0
override fun getTotalBufferedDuration(): Long = 0
override fun isCurrentMediaItemLive(): Boolean = true
fun updatePlaybackState(state: PlaybackState) { fun updatePlaybackState(state: PlaybackState) {
val (newPlayerState, newPlayWhenReady) = when (state) { val (newPlayerState, newPlayWhenReady) = when (state) {
is PlaybackState.Idle -> Player.STATE_IDLE to false is PlaybackState.Idle -> Player.STATE_IDLE to false
@@ -103,26 +77,9 @@ class RadioPlayerAdapter(
is PlaybackState.Paused -> Player.STATE_READY to false is PlaybackState.Paused -> Player.STATE_READY to false
is PlaybackState.Reconnecting -> Player.STATE_BUFFERING to true is PlaybackState.Reconnecting -> Player.STATE_BUFFERING to true
} }
val stateChanged = newPlayerState != _playbackState
val playWhenReadyChanged = newPlayWhenReady != _playWhenReady
_playbackState = newPlayerState _playbackState = newPlayerState
_playWhenReady = newPlayWhenReady _playWhenReady = newPlayWhenReady
invalidateState()
if (stateChanged || playWhenReadyChanged) {
listeners.forEach { listener ->
if (playWhenReadyChanged) {
listener.onPlayWhenReadyChanged(
newPlayWhenReady,
Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST
)
}
if (stateChanged) {
listener.onPlaybackStateChanged(newPlayerState)
}
listener.onIsPlayingChanged(isPlaying)
}
}
} }
fun updateStation(station: Station) { fun updateStation(station: Station) {
@@ -135,12 +92,7 @@ class RadioPlayerAdapter(
.build() .build()
) )
.build() .build()
listeners.forEach { invalidateState()
it.onMediaItemTransition(
_currentMediaItem,
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED
)
}
} }
fun updateMetadata( fun updateMetadata(
@@ -155,6 +107,13 @@ class RadioPlayerAdapter(
.setArtworkUri(artworkUri) .setArtworkUri(artworkUri)
.setIsPlayable(true) .setIsPlayable(true)
.build() .build()
listeners.forEach { it.onMediaMetadataChanged(_mediaMetadata) } invalidateState()
}
fun clearState() {
_currentMediaItem = null
_mediaMetadata = MediaMetadata.EMPTY
_playbackState = Player.STATE_IDLE
_playWhenReady = false
} }
} }