Minor playback improvements

Improve the code design and fix some bugs with PlaybackService and friends.
This commit is contained in:
OxygenCobalt 2020-10-30 11:39:55 -06:00
parent 2ded706445
commit 6fc034e376
6 changed files with 109 additions and 62 deletions

View file

@ -105,9 +105,13 @@ class MainFragment : Fragment() {
} }
} }
// Start the playback service. // Start the playback service [If not already]
Intent(requireContext(), PlaybackService::class.java).also { if (!playbackModel.serviceStarted) {
requireContext().startService(it) Intent(requireContext(), PlaybackService::class.java).also {
requireContext().startService(it)
}
playbackModel.setServiceStatus(true)
} }
Log.d(this::class.simpleName, "Fragment Created.") Log.d(this::class.simpleName, "Fragment Created.")

View file

@ -72,10 +72,16 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
// --- VIEWMODEL SETUP -- // --- VIEWMODEL SETUP --
playbackModel.song.observe(viewLifecycleOwner) { playbackModel.song.observe(viewLifecycleOwner) {
Log.d(this::class.simpleName, "Updating song display to ${it.name}.") if (it != null) {
Log.d(this::class.simpleName, "Updating song display to ${it.name}.")
binding.song = it binding.song = it
binding.playbackSeekBar.max = it.seconds.toInt() binding.playbackSeekBar.max = it.seconds.toInt()
} else {
Log.d(this::class.simpleName, "No song played anymore, leaving.")
findNavController().navigateUp()
}
} }
playbackModel.index.observe(viewLifecycleOwner) { playbackModel.index.observe(viewLifecycleOwner) {

View file

@ -39,6 +39,7 @@ private const val NOTIF_ID = 0xA0A0
// A Service that manages the single ExoPlayer instance and [attempts] to keep // A Service that manages the single ExoPlayer instance and [attempts] to keep
// persistence if the app closes. // persistence if the app closes.
class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback { class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
// TODO: Use the ExoPlayer queue functionality [To an extent]? Could make things faster.
private val player: SimpleExoPlayer by lazy { private val player: SimpleExoPlayer by lazy {
val p = SimpleExoPlayer.Builder(applicationContext).build() val p = SimpleExoPlayer.Builder(applicationContext).build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
@ -50,13 +51,6 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
private val playbackManager = PlaybackStateManager.getInstance() private val playbackManager = PlaybackStateManager.getInstance()
private lateinit var mediaSession: MediaSessionCompat private lateinit var mediaSession: MediaSessionCompat
private val buttonMediaCallback = object : MediaSessionCompat.Callback() {
override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {
Log.d(this::class.simpleName, "Hello?")
return true
}
}
private val serviceJob = Job() private val serviceJob = Job()
private val serviceScope = CoroutineScope( private val serviceScope = CoroutineScope(
@ -67,19 +61,20 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
private lateinit var notification: Notification private lateinit var notification: Notification
// --- SERVICE OVERRIDES ---
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(this::class.simpleName, "Service is active.") Log.d(this::class.simpleName, "Service is active.")
return START_STICKY return START_STICKY
} }
override fun onBind(intent: Intent): IBinder? { override fun onBind(intent: Intent): IBinder? = null
return null
}
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
// Set up the media button callbacks
mediaSession = MediaSessionCompat(this, packageName).apply { mediaSession = MediaSessionCompat(this, packageName).apply {
isActive = true isActive = true
} }
@ -87,30 +82,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
val connector = MediaSessionConnector(mediaSession) val connector = MediaSessionConnector(mediaSession)
connector.setPlayer(player) connector.setPlayer(player)
connector.setMediaButtonEventHandler { _, _, mediaButtonEvent -> connector.setMediaButtonEventHandler { _, _, mediaButtonEvent ->
val item = mediaButtonEvent handleMediaButtonEvent(mediaButtonEvent)
.getParcelableExtra<Parcelable>(Intent.EXTRA_KEY_EVENT) as KeyEvent
if (item.action == KeyEvent.ACTION_DOWN) {
when (item.keyCode) {
KeyEvent.KEYCODE_MEDIA_PAUSE, KeyEvent.KEYCODE_MEDIA_PLAY,
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_HEADSETHOOK -> {
playbackManager.setPlayingStatus(!playbackManager.isPlaying)
}
KeyEvent.KEYCODE_MEDIA_NEXT -> {
playbackManager.next()
}
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
playbackManager.prev()
}
// TODO: Implement the other callbacks for
// CLOSE/STOP & REWIND
}
}
true
} }
notification = createNotification() notification = createNotification()
@ -121,14 +93,19 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
stopForeground(true)
// Release everything that could cause a memory leak if left around
player.release() player.release()
mediaSession.release() mediaSession.release()
serviceJob.cancel() serviceJob.cancel()
playbackManager.removeCallback(this) playbackManager.removeCallback(this)
stopForeground(true) Log.d(this::class.simpleName, "Service destroyed.")
} }
// --- PLAYER EVENT LISTENER OVERRIDES ---
override fun onPlaybackStateChanged(state: Int) { override fun onPlaybackStateChanged(state: Int) {
if (state == Player.STATE_ENDED) { if (state == Player.STATE_ENDED) {
playbackManager.next() playbackManager.next()
@ -137,28 +114,31 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
} }
} }
// --- PLAYBACK STATE CALLBACK OVERRIDES ---
override fun onSongUpdate(song: Song?) { override fun onSongUpdate(song: Song?) {
song?.let { song?.let {
if (!isForeground) {
startForeground(NOTIF_ID, notification)
isForeground = true
}
val item = MediaItem.fromUri(it.id.toURI()) val item = MediaItem.fromUri(it.id.toURI())
player.setMediaItem(item) player.setMediaItem(item)
player.prepare() player.prepare()
player.play() player.play()
return
} }
player.stop()
} }
override fun onPlayingUpdate(isPlaying: Boolean) { override fun onPlayingUpdate(isPlaying: Boolean) {
if (isPlaying) { if (isPlaying && !player.isPlaying) {
player.play() player.play()
startForeground(NOTIF_ID, notification)
startPollingPosition() startPollingPosition()
} else { } else {
player.pause() player.pause()
stopForeground(false)
} }
} }
@ -166,25 +146,63 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
player.seekTo(position * 1000) player.seekTo(position * 1000)
} }
// --- OTHER FUNCTIONS ---
// Awful Hack to get position polling to work, as exoplayer does not provide any // Awful Hack to get position polling to work, as exoplayer does not provide any
// onPositionChanged callback for some inane reason. // onPositionChanged callback for some inane reason.
// FIXME: Don't be surprised if this causes problems. // FIXME: There has to be a better way of polling positions.
private fun pollCurrentPosition() = flow { private fun pollCurrentPosition() = flow {
while (player.currentPosition <= player.duration) { while (player.isPlaying) {
emit(player.currentPosition) emit(player.currentPosition)
delay(500) delay(250)
} }
}.conflate() }.conflate()
private fun startPollingPosition() { private fun startPollingPosition() {
serviceScope.launch { serviceScope.launch {
pollCurrentPosition().takeWhile { true }.collect { pollCurrentPosition().takeWhile { player.isPlaying }.collect {
playbackManager.setPosition(it / 1000) playbackManager.setPosition(it / 1000)
} }
} }
} }
// Handle a media button event.
private fun handleMediaButtonEvent(event: Intent): Boolean {
val item = event
.getParcelableExtra<Parcelable>(Intent.EXTRA_KEY_EVENT) as KeyEvent
if (item.action == KeyEvent.ACTION_DOWN) {
return when (item.keyCode) {
KeyEvent.KEYCODE_MEDIA_PAUSE, KeyEvent.KEYCODE_MEDIA_PLAY,
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_HEADSETHOOK -> {
playbackManager.setPlayingStatus(!playbackManager.isPlaying)
true
}
KeyEvent.KEYCODE_MEDIA_NEXT -> {
playbackManager.next()
true
}
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
playbackManager.prev()
true
}
// TODO: Implement the other callbacks for
// CLOSE/STOP & REWIND
else -> false
}
}
return false
}
// Create a notification
// TODO: Spin this off into its own object!
private fun createNotification(): Notification { private fun createNotification(): Notification {
val notificationManager = val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@ -197,7 +215,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
) )
notificationManager.createNotificationChannel(channel) notificationManager.createNotificationChannel(channel)
} }
// TODO: Placeholder, implement proper media controls :) // TODO: Placeholder, implement proper media controls.
val notif = NotificationCompat.Builder( val notif = NotificationCompat.Builder(
applicationContext, applicationContext,
CHANNEL_ID CHANNEL_ID

View file

@ -18,10 +18,10 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager
// TODO: Implement Looping Modes // TODO: Implement Looping Modes
// TODO: Implement User Queue // TODO: Implement User Queue
// TODO: Implement Persistence through Bundles/Databases/Idk // TODO: Implement Persistence through Bundles/Databases/Idk
class PlaybackViewModel() : ViewModel(), PlaybackStateCallback { class PlaybackViewModel : ViewModel(), PlaybackStateCallback {
// Playback // Playback
private val mSong = MutableLiveData<Song>() private val mSong = MutableLiveData<Song?>()
val song: LiveData<Song> get() = mSong val song: LiveData<Song?> get() = mSong
private val mPosition = MutableLiveData(0L) private val mPosition = MutableLiveData(0L)
val position: LiveData<Long> get() = mPosition val position: LiveData<Long> get() = mPosition
@ -44,6 +44,9 @@ class PlaybackViewModel() : ViewModel(), PlaybackStateCallback {
private val mIsSeeking = MutableLiveData(false) private val mIsSeeking = MutableLiveData(false)
val isSeeking: LiveData<Boolean> get() = mIsSeeking val isSeeking: LiveData<Boolean> get() = mIsSeeking
private var mServiceStarted = false
val serviceStarted: Boolean get() = mServiceStarted
val formattedPosition = Transformations.map(mPosition) { val formattedPosition = Transformations.map(mPosition) {
it.toDuration() it.toDuration()
} }
@ -179,6 +182,10 @@ class PlaybackViewModel() : ViewModel(), PlaybackStateCallback {
mIsSeeking.value = value mIsSeeking.value = value
} }
fun setServiceStatus(value: Boolean) {
mServiceStarted = value
}
// --- OVERRIDES --- // --- OVERRIDES ---
override fun onCleared() { override fun onCleared() {
@ -186,9 +193,7 @@ class PlaybackViewModel() : ViewModel(), PlaybackStateCallback {
} }
override fun onSongUpdate(song: Song?) { override fun onSongUpdate(song: Song?) {
song?.let { mSong.value = song
mSong.value = it
}
} }
override fun onPositionUpdate(position: Long) { override fun onPositionUpdate(position: Long) {

View file

@ -9,5 +9,7 @@ interface PlaybackStateCallback {
fun onPlayingUpdate(isPlaying: Boolean) {} fun onPlayingUpdate(isPlaying: Boolean) {}
fun onShuffleUpdate(isShuffling: Boolean) {} fun onShuffleUpdate(isShuffling: Boolean) {}
fun onIndexUpdate(index: Int) {} fun onIndexUpdate(index: Int) {}
// Service callbacks
fun onSeekConfirm(position: Long) {} fun onSeekConfirm(position: Long) {}
} }

View file

@ -165,7 +165,13 @@ class PlaybackStateManager {
} }
fun setPosition(position: Long) { fun setPosition(position: Long) {
mPosition = position // Due to the hacky way I poll ExoPlayer positions, don't accept any bugged positions
// that are over the duration of the song.
mSong?.let {
if (position <= it.seconds) {
mPosition = position
}
}
} }
fun seekTo(position: Long) { fun seekTo(position: Long) {
@ -177,8 +183,14 @@ class PlaybackStateManager {
// --- QUEUE FUNCTIONS --- // --- QUEUE FUNCTIONS ---
fun next() { fun next() {
if (mIndex < mQueue.size) { if (mIndex < mQueue.lastIndex) {
mIndex = mIndex.inc() mIndex = mIndex.inc()
} else {
// TODO: Implement option so that the playlist loops instead of stops
mQueue = mutableListOf()
mSong = null
return
} }
updatePlayback(mQueue[mIndex]) updatePlayback(mQueue[mIndex])