Let the media session live when playback is paused.
As per Android policy and internal logic, we stopped the playback foreground service when playback was paused. This made our PlayService elligible for garbage collection by the OS. This had the consequences of not allowing someone to pause playback and resume it after some time. Android would always kill the service after around one minute. This commit, on supported Android version (7.0+) detaches the notification when stopping the foreground service, leaving the notification in place even when the service is killed, allowing the user to resume playback whenever they please. We also had to move the MediaSession out of the service, for it to remain alive between service killing and resurrection.
This commit is contained in:
parent
a3f84cc56c
commit
4ecb607f45
|
@ -42,7 +42,7 @@
|
|||
<activity android:name="com.github.apognu.otter.activities.SettingsActivity" />
|
||||
<activity android:name="com.github.apognu.otter.activities.LicencesActivity" />
|
||||
|
||||
<service android:name="com.github.apognu.otter.playback.PlayerService" />
|
||||
<service android:name="com.github.apognu.otter.playback.PlayerService" android:foregroundServiceType="mediaPlayback" />
|
||||
|
||||
<service
|
||||
android:name=".playback.PinService"
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package com.github.apognu.otter
|
||||
|
||||
import android.app.Application
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import android.support.v4.media.session.PlaybackStateCompat
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import com.github.apognu.otter.playback.QueueManager
|
||||
import com.github.apognu.otter.utils.*
|
||||
|
@ -60,6 +62,26 @@ class Otter : Application() {
|
|||
}
|
||||
}
|
||||
|
||||
private val playbackStateBuilder = PlaybackStateCompat.Builder().apply {
|
||||
setActions(
|
||||
PlaybackStateCompat.ACTION_PLAY_PAUSE or
|
||||
PlaybackStateCompat.ACTION_PLAY or
|
||||
PlaybackStateCompat.ACTION_PAUSE or
|
||||
PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
|
||||
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or
|
||||
PlaybackStateCompat.ACTION_SEEK_TO or
|
||||
PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM
|
||||
)
|
||||
}
|
||||
|
||||
val mediaSession: MediaSessionCompat by lazy {
|
||||
MediaSessionCompat(this, applicationContext.packageName).apply {
|
||||
setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS)
|
||||
setPlaybackState(playbackStateBuilder.build())
|
||||
isActive = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
|
|
|
@ -286,6 +286,12 @@ class MainActivity : AppCompatActivity() {
|
|||
lifecycleScope.launch(Main) {
|
||||
CommandBus.get().collect { command ->
|
||||
when (command) {
|
||||
is Command.StartService -> {
|
||||
startService(Intent(this@MainActivity, PlayerService::class.java).apply {
|
||||
putExtra(PlayerService.INITIAL_COMMAND_KEY, command.command.toString())
|
||||
})
|
||||
}
|
||||
|
||||
is Command.RefreshTrack -> refreshCurrentTrack(command.track)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,6 @@ import android.app.Service
|
|||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.media.MediaMetadata
|
||||
import android.support.v4.media.MediaMetadataCompat
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
|
@ -18,9 +16,7 @@ import com.github.apognu.otter.utils.*
|
|||
import com.squareup.picasso.Picasso
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers.Default
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class MediaControlsManager(val context: Service, private val scope: CoroutineScope, private val mediaSession: MediaSessionCompat) {
|
||||
companion object {
|
||||
|
@ -32,27 +28,6 @@ class MediaControlsManager(val context: Service, private val scope: CoroutineSco
|
|||
|
||||
private var notification: Notification? = null
|
||||
|
||||
fun buildTrackMetadata(track: Track?): MediaMetadataCompat {
|
||||
track?.let {
|
||||
val coverUrl = maybeNormalizeUrl(track.album.cover.original)
|
||||
|
||||
return MediaMetadataCompat.Builder().apply {
|
||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, track.title)
|
||||
putString(MediaMetadataCompat.METADATA_KEY_ARTIST, track.artist.name)
|
||||
putLong(MediaMetadata.METADATA_KEY_DURATION, (track.bestUpload()?.duration?.toLong() ?: 0L) * 1000)
|
||||
|
||||
try {
|
||||
runBlocking(IO) {
|
||||
this@apply.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, Picasso.get().load(coverUrl).get())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
return MediaMetadataCompat.Builder().build()
|
||||
}
|
||||
|
||||
fun updateNotification(track: Track?, playing: Boolean) {
|
||||
if (notification == null && !playing) return
|
||||
|
||||
|
@ -82,7 +57,10 @@ class MediaControlsManager(val context: Service, private val scope: CoroutineSco
|
|||
.setSmallIcon(R.drawable.ottershape)
|
||||
.run {
|
||||
coverUrl?.let {
|
||||
try { setLargeIcon(Picasso.get().load(coverUrl).get()) } catch (_: Exception) {}
|
||||
try {
|
||||
setLargeIcon(Picasso.get().load(coverUrl).get())
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
|
||||
return@run this
|
||||
}
|
||||
|
@ -114,20 +92,16 @@ class MediaControlsManager(val context: Service, private val scope: CoroutineSco
|
|||
.build()
|
||||
|
||||
notification?.let {
|
||||
NotificationManagerCompat.from(context).notify(AppContext.NOTIFICATION_MEDIA_CONTROL, it)
|
||||
if (playing) {
|
||||
context.startForeground(AppContext.NOTIFICATION_MEDIA_CONTROL, it)
|
||||
} else {
|
||||
NotificationManagerCompat.from(context).notify(AppContext.NOTIFICATION_MEDIA_CONTROL, it)
|
||||
}
|
||||
}
|
||||
|
||||
if (playing) tick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun tick() {
|
||||
notification?.let {
|
||||
context.startForeground(AppContext.NOTIFICATION_MEDIA_CONTROL, it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun action(icon: Int, title: String, id: Int): NotificationCompat.Action {
|
||||
val intent = Intent(context, MediaControlActionReceiver::class.java).apply { action = id.toString() }
|
||||
val pendingIntent = PendingIntent.getBroadcast(context, id, intent, 0)
|
||||
|
@ -138,16 +112,16 @@ class MediaControlsManager(val context: Service, private val scope: CoroutineSco
|
|||
|
||||
class MediaControlActionReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
when (intent?.action) {
|
||||
MediaControlsManager.NOTIFICATION_ACTION_PREVIOUS.toString() -> CommandBus.send(
|
||||
Command.PreviousTrack
|
||||
)
|
||||
MediaControlsManager.NOTIFICATION_ACTION_TOGGLE.toString() -> CommandBus.send(
|
||||
Command.ToggleState
|
||||
)
|
||||
MediaControlsManager.NOTIFICATION_ACTION_NEXT.toString() -> CommandBus.send(
|
||||
Command.NextTrack
|
||||
)
|
||||
val command = when (intent?.action) {
|
||||
MediaControlsManager.NOTIFICATION_ACTION_PREVIOUS.toString() -> Command.PreviousTrack
|
||||
MediaControlsManager.NOTIFICATION_ACTION_TOGGLE.toString() -> Command.ToggleState
|
||||
MediaControlsManager.NOTIFICATION_ACTION_NEXT.toString() -> Command.NextTrack
|
||||
else -> null
|
||||
}
|
||||
|
||||
command?.let {
|
||||
CommandBus.send(command)
|
||||
CommandBus.send(Command.StartService(command))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,28 +8,32 @@ import android.content.IntentFilter
|
|||
import android.media.AudioAttributes
|
||||
import android.media.AudioFocusRequest
|
||||
import android.media.AudioManager
|
||||
import android.media.MediaMetadata
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.os.ResultReceiver
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import android.support.v4.media.MediaMetadataCompat
|
||||
import android.support.v4.media.session.PlaybackStateCompat
|
||||
import android.view.KeyEvent
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.google.android.exoplayer2.*
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import com.squareup.picasso.Picasso
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class PlayerService : Service() {
|
||||
companion object {
|
||||
const val INITIAL_COMMAND_KEY = "start_command"
|
||||
}
|
||||
|
||||
private var started = false
|
||||
private val scope: CoroutineScope = CoroutineScope(Job() + Main)
|
||||
|
||||
|
@ -40,9 +44,10 @@ class PlayerService : Service() {
|
|||
|
||||
private lateinit var queue: QueueManager
|
||||
private lateinit var mediaControlsManager: MediaControlsManager
|
||||
private lateinit var mediaSession: MediaSessionCompat
|
||||
private lateinit var player: SimpleExoPlayer
|
||||
|
||||
private val mediaMetadataBuilder = MediaMetadataCompat.Builder()
|
||||
|
||||
private lateinit var playerEventListener: PlayerEventListener
|
||||
private val headphonesUnpluggedReceiver = HeadphonesUnpluggedReceiver()
|
||||
|
||||
|
@ -51,7 +56,17 @@ class PlayerService : Service() {
|
|||
private lateinit var radioPlayer: RadioPlayer
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (!started) watchEventBus()
|
||||
if (!started) {
|
||||
watchEventBus()
|
||||
|
||||
intent?.extras?.getString(INITIAL_COMMAND_KEY)?.let {
|
||||
when (it) {
|
||||
Command.ToggleState.toString() -> togglePlayback()
|
||||
Command.NextTrack.toString() -> skipToNextTrack()
|
||||
Command.PreviousTrack.toString() -> skipToPreviousTrack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
started = true
|
||||
|
||||
|
@ -82,18 +97,7 @@ class PlayerService : Service() {
|
|||
}
|
||||
}
|
||||
|
||||
mediaSession = MediaSessionCompat(this, applicationContext.packageName).apply {
|
||||
isActive = true
|
||||
setPlaybackState(PlaybackStateCompat.Builder()
|
||||
.setActions(
|
||||
PlaybackStateCompat.ACTION_PLAY_PAUSE or
|
||||
PlaybackStateCompat.ACTION_SEEK_TO or
|
||||
PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
|
||||
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
|
||||
).build())
|
||||
}
|
||||
|
||||
mediaControlsManager = MediaControlsManager(this, scope, mediaSession)
|
||||
mediaControlsManager = MediaControlsManager(this, scope, Otter.get().mediaSession)
|
||||
|
||||
player = SimpleExoPlayer.Builder(this).build().apply {
|
||||
playWhenReady = false
|
||||
|
@ -102,40 +106,21 @@ class PlayerService : Service() {
|
|||
addListener(it)
|
||||
}
|
||||
|
||||
MediaSessionConnector(mediaSession).also {
|
||||
MediaSessionConnector(Otter.get().mediaSession).also {
|
||||
it.setPlayer(this)
|
||||
it.setQueueNavigator(OtterQueueNavigator())
|
||||
it.setMediaMetadataProvider {
|
||||
mediaControlsManager.buildTrackMetadata(queue.current())
|
||||
buildTrackMetadata(queue.current())
|
||||
}
|
||||
|
||||
it.setQueueNavigator(object : MediaSessionConnector.QueueNavigator {
|
||||
override fun onSkipToQueueItem(player: Player, controlDispatcher: ControlDispatcher, id: Long) {}
|
||||
|
||||
override fun onCurrentWindowIndexChanged(player: Player) {}
|
||||
|
||||
override fun onCommand(player: Player, controlDispatcher: ControlDispatcher, command: String, extras: Bundle?, cb: ResultReceiver?) = true
|
||||
|
||||
override fun getSupportedQueueNavigatorActions(player: Player): Long {
|
||||
return PlaybackStateCompat.ACTION_PLAY_PAUSE or PlaybackStateCompat.ACTION_SKIP_TO_NEXT or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
|
||||
}
|
||||
|
||||
override fun onSkipToNext(player: Player, controlDispatcher: ControlDispatcher) {}
|
||||
|
||||
override fun getActiveQueueItemId(player: Player?) = 0L
|
||||
|
||||
override fun onSkipToPrevious(player: Player, controlDispatcher: ControlDispatcher) {}
|
||||
|
||||
override fun onTimelineChanged(player: Player) {}
|
||||
})
|
||||
|
||||
it.setMediaButtonEventHandler { player, _, mediaButtonEvent ->
|
||||
mediaButtonEvent.extras?.getParcelable<KeyEvent>(Intent.EXTRA_KEY_EVENT)?.let { key ->
|
||||
if (key.action == KeyEvent.ACTION_UP) {
|
||||
when (key.keyCode) {
|
||||
KeyEvent.KEYCODE_MEDIA_PLAY -> state(true)
|
||||
KeyEvent.KEYCODE_MEDIA_PAUSE -> state(false)
|
||||
KeyEvent.KEYCODE_MEDIA_PLAY -> setPlaybackState(true)
|
||||
KeyEvent.KEYCODE_MEDIA_PAUSE -> setPlaybackState(false)
|
||||
KeyEvent.KEYCODE_MEDIA_NEXT -> player.next()
|
||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> previousTrack()
|
||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> skipToPreviousTrack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -146,12 +131,12 @@ class PlayerService : Service() {
|
|||
}
|
||||
|
||||
if (queue.current > -1) {
|
||||
player.prepare(queue.datasources, true, true)
|
||||
player.prepare(queue.datasources)
|
||||
|
||||
Cache.get(this, "progress")?.let { progress ->
|
||||
player.seekTo(queue.current, progress.readLine().toLong())
|
||||
|
||||
val (current, duration, percent) = progress(true)
|
||||
val (current, duration, percent) = getProgress(true)
|
||||
|
||||
ProgressBus.send(current, duration, percent)
|
||||
}
|
||||
|
@ -179,7 +164,7 @@ class PlayerService : Service() {
|
|||
queue.replace(command.queue)
|
||||
player.prepare(queue.datasources, true, true)
|
||||
|
||||
state(true)
|
||||
setPlaybackState(true)
|
||||
|
||||
CommandBus.send(Command.RefreshTrack(queue.current()))
|
||||
}
|
||||
|
@ -193,22 +178,17 @@ class PlayerService : Service() {
|
|||
queue.current = command.index
|
||||
player.seekTo(command.index, C.TIME_UNSET)
|
||||
|
||||
state(true)
|
||||
setPlaybackState(true)
|
||||
|
||||
CommandBus.send(Command.RefreshTrack(queue.current()))
|
||||
}
|
||||
|
||||
is Command.ToggleState -> toggle()
|
||||
is Command.SetState -> state(command.state)
|
||||
is Command.ToggleState -> togglePlayback()
|
||||
is Command.SetState -> setPlaybackState(command.state)
|
||||
|
||||
is Command.NextTrack -> {
|
||||
player.next()
|
||||
|
||||
Cache.set(this@PlayerService, "progress", "0".toByteArray())
|
||||
ProgressBus.send(0, 0, 0)
|
||||
}
|
||||
is Command.PreviousTrack -> previousTrack()
|
||||
is Command.Seek -> progress(command.progress)
|
||||
is Command.NextTrack -> skipToNextTrack()
|
||||
is Command.PreviousTrack -> skipToPreviousTrack()
|
||||
is Command.Seek -> seek(command.progress)
|
||||
|
||||
is Command.ClearQueue -> queue.clear()
|
||||
|
||||
|
@ -222,10 +202,6 @@ class PlayerService : Service() {
|
|||
is Command.PinTrack -> PinService.download(this@PlayerService, command.track)
|
||||
is Command.PinTracks -> command.tracks.forEach { PinService.download(this@PlayerService, it) }
|
||||
}
|
||||
|
||||
if (player.playWhenReady) {
|
||||
mediaControlsManager.tick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -243,7 +219,7 @@ class PlayerService : Service() {
|
|||
while (true) {
|
||||
delay(1000)
|
||||
|
||||
val (current, duration, percent) = progress()
|
||||
val (current, duration, percent) = getProgress()
|
||||
|
||||
if (player.playWhenReady) {
|
||||
ProgressBus.send(current, duration, percent)
|
||||
|
@ -256,6 +232,8 @@ class PlayerService : Service() {
|
|||
|
||||
@SuppressLint("NewApi")
|
||||
override fun onDestroy() {
|
||||
scope.cancel()
|
||||
|
||||
try {
|
||||
unregisterReceiver(headphonesUnpluggedReceiver)
|
||||
} catch (_: Exception) {
|
||||
|
@ -272,11 +250,8 @@ class PlayerService : Service() {
|
|||
audioManager.abandonAudioFocus(audioFocusChangeListener)
|
||||
})
|
||||
|
||||
mediaSession.isActive = false
|
||||
mediaSession.release()
|
||||
|
||||
player.removeListener(playerEventListener)
|
||||
state(false)
|
||||
setPlaybackState(false)
|
||||
player.release()
|
||||
|
||||
stopForeground(true)
|
||||
|
@ -286,9 +261,9 @@ class PlayerService : Service() {
|
|||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
private fun state(state: Boolean) {
|
||||
private fun setPlaybackState(state: Boolean) {
|
||||
if (!state) {
|
||||
val (progress, _, _) = progress()
|
||||
val (progress, _, _) = getProgress()
|
||||
|
||||
Cache.set(this@PlayerService, "progress", progress.toString().toByteArray())
|
||||
}
|
||||
|
@ -329,11 +304,11 @@ class PlayerService : Service() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun toggle() {
|
||||
state(!player.playWhenReady)
|
||||
private fun togglePlayback() {
|
||||
setPlaybackState(!player.playWhenReady)
|
||||
}
|
||||
|
||||
private fun previousTrack() {
|
||||
private fun skipToPreviousTrack() {
|
||||
if (player.currentPosition > 5000) {
|
||||
return player.seekTo(0)
|
||||
}
|
||||
|
@ -341,7 +316,14 @@ class PlayerService : Service() {
|
|||
player.previous()
|
||||
}
|
||||
|
||||
private fun progress(force: Boolean = false): Triple<Int, Int, Int> {
|
||||
private fun skipToNextTrack() {
|
||||
player.next()
|
||||
|
||||
Cache.set(this@PlayerService, "progress", "0".toByteArray())
|
||||
ProgressBus.send(0, 0, 0)
|
||||
}
|
||||
|
||||
private fun getProgress(force: Boolean = false): Triple<Int, Int, Int> {
|
||||
if (!player.playWhenReady && !force) return progressCache
|
||||
|
||||
return queue.current()?.bestUpload()?.let { upload ->
|
||||
|
@ -354,7 +336,7 @@ class PlayerService : Service() {
|
|||
} ?: Triple(0, 0, 0)
|
||||
}
|
||||
|
||||
private fun progress(value: Int) {
|
||||
private fun seek(value: Int) {
|
||||
val duration = ((queue.current()?.bestUpload()?.duration ?: 0) * (value.toFloat() / 100)) * 1000
|
||||
|
||||
progressCache = Triple(duration.toInt(), queue.current()?.bestUpload()?.duration ?: 0, value)
|
||||
|
@ -362,6 +344,28 @@ class PlayerService : Service() {
|
|||
player.seekTo(duration.toLong())
|
||||
}
|
||||
|
||||
private fun buildTrackMetadata(track: Track?): MediaMetadataCompat {
|
||||
track?.let {
|
||||
val coverUrl = maybeNormalizeUrl(track.album.cover.original)
|
||||
|
||||
return mediaMetadataBuilder.apply {
|
||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, track.title)
|
||||
putString(MediaMetadataCompat.METADATA_KEY_ARTIST, track.artist.name)
|
||||
putLong(MediaMetadata.METADATA_KEY_DURATION, (track.bestUpload()?.duration?.toLong() ?: 0L) * 1000)
|
||||
|
||||
try {
|
||||
runBlocking(IO) {
|
||||
this@apply.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, Picasso.get().load(coverUrl).get())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
return mediaMetadataBuilder.build()
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
inner class PlayerEventListener : Player.EventListener {
|
||||
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
|
||||
super.onPlayerStateChanged(playWhenReady, playbackState)
|
||||
|
@ -388,7 +392,11 @@ class PlayerService : Service() {
|
|||
|
||||
if (playbackState == Player.STATE_READY) {
|
||||
mediaControlsManager.updateNotification(queue.current(), false)
|
||||
stopForeground(false)
|
||||
|
||||
Build.VERSION_CODES.N.onApi(
|
||||
{ stopForeground(STOP_FOREGROUND_DETACH) },
|
||||
{ stopForeground(false) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -441,18 +449,18 @@ class PlayerService : Service() {
|
|||
AudioManager.AUDIOFOCUS_GAIN -> {
|
||||
player.volume = 1f
|
||||
|
||||
state(stateWhenLostFocus)
|
||||
setPlaybackState(stateWhenLostFocus)
|
||||
stateWhenLostFocus = false
|
||||
}
|
||||
|
||||
AudioManager.AUDIOFOCUS_LOSS -> {
|
||||
stateWhenLostFocus = false
|
||||
state(false)
|
||||
setPlaybackState(false)
|
||||
}
|
||||
|
||||
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
||||
stateWhenLostFocus = player.playWhenReady
|
||||
state(false)
|
||||
setPlaybackState(false)
|
||||
}
|
||||
|
||||
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
||||
|
@ -462,4 +470,33 @@ class PlayerService : Service() {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class OtterQueueNavigator : MediaSessionConnector.QueueNavigator {
|
||||
override fun onSkipToQueueItem(player: Player, controlDispatcher: ControlDispatcher, id: Long) {
|
||||
CommandBus.send(Command.PlayTrack(id.toInt()))
|
||||
}
|
||||
|
||||
override fun onCurrentWindowIndexChanged(player: Player) {}
|
||||
|
||||
override fun onCommand(player: Player, controlDispatcher: ControlDispatcher, command: String, extras: Bundle?, cb: ResultReceiver?) = true
|
||||
|
||||
override fun getSupportedQueueNavigatorActions(player: Player): Long {
|
||||
return PlaybackStateCompat.ACTION_PLAY_PAUSE or
|
||||
PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
|
||||
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or
|
||||
PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM
|
||||
}
|
||||
|
||||
override fun onSkipToNext(player: Player, controlDispatcher: ControlDispatcher) {
|
||||
skipToNextTrack()
|
||||
}
|
||||
|
||||
override fun getActiveQueueItemId(player: Player?) = queue.current.toLong()
|
||||
|
||||
override fun onSkipToPrevious(player: Player, controlDispatcher: ControlDispatcher) {
|
||||
skipToPreviousTrack()
|
||||
}
|
||||
|
||||
override fun onTimelineChanged(player: Player) {}
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.conflate
|
|||
import kotlinx.coroutines.launch
|
||||
|
||||
sealed class Command {
|
||||
class StartService(val command: Command) : Command()
|
||||
object RefreshService : Command()
|
||||
|
||||
object ToggleState : Command()
|
||||
|
|
Loading…
Reference in New Issue