Allow media session resuming from media buttons when service is killed.

This commit is contained in:
Antoine POPINEAU 2020-07-10 17:18:29 +02:00
parent b14b703f05
commit b34810d631
No known key found for this signature in database
GPG Key ID: A78AC64694F84063
5 changed files with 140 additions and 82 deletions

View File

@ -34,15 +34,22 @@
android:name="com.github.apognu.otter.activities.LoginActivity"
android:configChanges="screenSize|orientation"
android:launchMode="singleInstance" />
<activity android:name="com.github.apognu.otter.activities.MainActivity" />
<activity
android:name="com.github.apognu.otter.activities.SearchActivity"
android:launchMode="singleTop" />
<activity android:name="com.github.apognu.otter.activities.DownloadsActivity" />
<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" android:foregroundServiceType="mediaPlayback" />
<service
android:name="com.github.apognu.otter.playback.PlayerService"
android:foregroundServiceType="mediaPlayback" />
<service
android:name=".playback.PinService"

View File

@ -1,9 +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.MediaSession
import com.github.apognu.otter.playback.QueueManager
import com.github.apognu.otter.utils.*
import com.google.android.exoplayer2.database.ExoDatabaseProvider
@ -62,25 +61,7 @@ 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
}
}
val mediaSession = MediaSession(this)
override fun onCreate() {
super.onCreate()

View File

@ -6,6 +6,7 @@ import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Bitmap
import android.os.Build
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.*
@ -43,7 +44,6 @@ import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -291,9 +291,18 @@ class MainActivity : AppCompatActivity() {
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())
})
Build.VERSION_CODES.O.onApi(
{
startForegroundService(Intent(this@MainActivity, PlayerService::class.java).apply {
putExtra(PlayerService.INITIAL_COMMAND_KEY, command.command.toString())
})
},
{
startService(Intent(this@MainActivity, PlayerService::class.java).apply {
putExtra(PlayerService.INITIAL_COMMAND_KEY, command.command.toString())
})
}
)
}
is Command.RefreshTrack -> refreshCurrentTrack(command.track)

View File

@ -0,0 +1,103 @@
package com.github.apognu.otter.playback
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.ResultReceiver
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import android.view.KeyEvent
import com.github.apognu.otter.Otter
import com.github.apognu.otter.utils.Command
import com.github.apognu.otter.utils.CommandBus
import com.google.android.exoplayer2.ControlDispatcher
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
class MediaSession(private val context: Context) {
var active: Boolean = false
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 session: MediaSessionCompat by lazy {
active = true
MediaSessionCompat(context, context.packageName).apply {
setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS)
setPlaybackState(playbackStateBuilder.build())
isActive = true
}
}
val connector: MediaSessionConnector by lazy {
MediaSessionConnector(Otter.get().mediaSession.session).also {
it.setQueueNavigator(OtterQueueNavigator())
it.setMediaButtonEventHandler { _, _, event ->
if (!active) {
event.extras?.getParcelable<KeyEvent>(Intent.EXTRA_KEY_EVENT)?.let { key ->
if (key.action == KeyEvent.ACTION_UP) {
val command = when (key.keyCode) {
KeyEvent.KEYCODE_MEDIA_PLAY -> Command.ToggleState
KeyEvent.KEYCODE_MEDIA_PAUSE -> Command.ToggleState
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> Command.ToggleState
KeyEvent.KEYCODE_MEDIA_NEXT -> Command.NextTrack
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> Command.PreviousTrack
else -> null
}
command?.let {
CommandBus.send(command)
CommandBus.send(Command.StartService(command))
return@setMediaButtonEventHandler true
}
}
}
}
false
}
}
}
}
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) {
CommandBus.send(Command.NextTrack)
}
override fun getActiveQueueItemId(player: Player?) = 0L
override fun onSkipToPrevious(player: Player, controlDispatcher: ControlDispatcher) {
CommandBus.send(Command.PreviousTrack)
}
override fun onTimelineChanged(player: Player) {}
}

View File

@ -10,17 +10,15 @@ 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.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.C
import com.google.android.exoplayer2.ExoPlaybackException
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.source.TrackGroupArray
import com.google.android.exoplayer2.trackselection.TrackSelectionArray
import com.squareup.picasso.Picasso
@ -61,6 +59,8 @@ class PlayerService : Service() {
intent?.extras?.getString(INITIAL_COMMAND_KEY)?.let {
when (it) {
Command.SetState(true).toString() -> setPlaybackState(true)
Command.SetState(false).toString() -> setPlaybackState(false)
Command.ToggleState.toString() -> togglePlayback()
Command.NextTrack.toString() -> skipToNextTrack()
Command.PreviousTrack.toString() -> skipToPreviousTrack()
@ -97,7 +97,7 @@ class PlayerService : Service() {
}
}
mediaControlsManager = MediaControlsManager(this, scope, Otter.get().mediaSession)
mediaControlsManager = MediaControlsManager(this, scope, Otter.get().mediaSession.session)
player = SimpleExoPlayer.Builder(this).build().apply {
playWhenReady = false
@ -105,28 +105,13 @@ class PlayerService : Service() {
playerEventListener = PlayerEventListener().also {
addListener(it)
}
}
MediaSessionConnector(Otter.get().mediaSession).also {
it.setPlayer(this)
it.setQueueNavigator(OtterQueueNavigator())
it.setMediaMetadataProvider {
buildTrackMetadata(queue.current())
}
Otter.get().mediaSession.connector.apply {
setPlayer(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 -> setPlaybackState(true)
KeyEvent.KEYCODE_MEDIA_PAUSE -> setPlaybackState(false)
KeyEvent.KEYCODE_MEDIA_NEXT -> player.next()
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> skipToPreviousTrack()
}
}
}
true
}
setMediaMetadataProvider {
buildTrackMetadata(queue.current())
}
}
@ -250,6 +235,8 @@ class PlayerService : Service() {
audioManager.abandonAudioFocus(audioFocusChangeListener)
})
Otter.get().mediaSession.active = false
player.removeListener(playerEventListener)
setPlaybackState(false)
player.release()
@ -470,33 +457,4 @@ 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) {}
}
}