mirror of
https://github.com/apognu/otter
synced 2025-02-13 17:40:34 +01:00
Allow media session resuming from media buttons when service is killed.
This commit is contained in:
parent
b14b703f05
commit
b34810d631
@ -34,15 +34,22 @@
|
|||||||
android:name="com.github.apognu.otter.activities.LoginActivity"
|
android:name="com.github.apognu.otter.activities.LoginActivity"
|
||||||
android:configChanges="screenSize|orientation"
|
android:configChanges="screenSize|orientation"
|
||||||
android:launchMode="singleInstance" />
|
android:launchMode="singleInstance" />
|
||||||
|
|
||||||
<activity android:name="com.github.apognu.otter.activities.MainActivity" />
|
<activity android:name="com.github.apognu.otter.activities.MainActivity" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.github.apognu.otter.activities.SearchActivity"
|
android:name="com.github.apognu.otter.activities.SearchActivity"
|
||||||
android:launchMode="singleTop" />
|
android:launchMode="singleTop" />
|
||||||
|
|
||||||
<activity android:name="com.github.apognu.otter.activities.DownloadsActivity" />
|
<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.SettingsActivity" />
|
||||||
|
|
||||||
<activity android:name="com.github.apognu.otter.activities.LicencesActivity" />
|
<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
|
<service
|
||||||
android:name=".playback.PinService"
|
android:name=".playback.PinService"
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
package com.github.apognu.otter
|
package com.github.apognu.otter
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
|
||||||
import android.support.v4.media.session.PlaybackStateCompat
|
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import com.github.apognu.otter.playback.MediaSession
|
||||||
import com.github.apognu.otter.playback.QueueManager
|
import com.github.apognu.otter.playback.QueueManager
|
||||||
import com.github.apognu.otter.utils.*
|
import com.github.apognu.otter.utils.*
|
||||||
import com.google.android.exoplayer2.database.ExoDatabaseProvider
|
import com.google.android.exoplayer2.database.ExoDatabaseProvider
|
||||||
@ -62,25 +61,7 @@ class Otter : Application() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val playbackStateBuilder = PlaybackStateCompat.Builder().apply {
|
val mediaSession = MediaSession(this)
|
||||||
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() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
@ -6,6 +6,7 @@ import android.animation.ObjectAnimator
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.DisplayMetrics
|
import android.util.DisplayMetrics
|
||||||
import android.view.*
|
import android.view.*
|
||||||
@ -43,7 +44,6 @@ import kotlinx.coroutines.Dispatchers.IO
|
|||||||
import kotlinx.coroutines.Dispatchers.Main
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.toList
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@ -291,9 +291,18 @@ class MainActivity : AppCompatActivity() {
|
|||||||
CommandBus.get().collect { command ->
|
CommandBus.get().collect { command ->
|
||||||
when (command) {
|
when (command) {
|
||||||
is Command.StartService -> {
|
is Command.StartService -> {
|
||||||
startService(Intent(this@MainActivity, PlayerService::class.java).apply {
|
Build.VERSION_CODES.O.onApi(
|
||||||
putExtra(PlayerService.INITIAL_COMMAND_KEY, command.command.toString())
|
{
|
||||||
})
|
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)
|
is Command.RefreshTrack -> refreshCurrentTrack(command.track)
|
||||||
|
@ -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) {}
|
||||||
|
}
|
@ -10,17 +10,15 @@ import android.media.AudioFocusRequest
|
|||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.media.MediaMetadata
|
import android.media.MediaMetadata
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.ResultReceiver
|
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
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.Otter
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
import com.github.apognu.otter.utils.*
|
import com.github.apognu.otter.utils.*
|
||||||
import com.google.android.exoplayer2.*
|
import com.google.android.exoplayer2.C
|
||||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
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.source.TrackGroupArray
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray
|
import com.google.android.exoplayer2.trackselection.TrackSelectionArray
|
||||||
import com.squareup.picasso.Picasso
|
import com.squareup.picasso.Picasso
|
||||||
@ -61,6 +59,8 @@ class PlayerService : Service() {
|
|||||||
|
|
||||||
intent?.extras?.getString(INITIAL_COMMAND_KEY)?.let {
|
intent?.extras?.getString(INITIAL_COMMAND_KEY)?.let {
|
||||||
when (it) {
|
when (it) {
|
||||||
|
Command.SetState(true).toString() -> setPlaybackState(true)
|
||||||
|
Command.SetState(false).toString() -> setPlaybackState(false)
|
||||||
Command.ToggleState.toString() -> togglePlayback()
|
Command.ToggleState.toString() -> togglePlayback()
|
||||||
Command.NextTrack.toString() -> skipToNextTrack()
|
Command.NextTrack.toString() -> skipToNextTrack()
|
||||||
Command.PreviousTrack.toString() -> skipToPreviousTrack()
|
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 {
|
player = SimpleExoPlayer.Builder(this).build().apply {
|
||||||
playWhenReady = false
|
playWhenReady = false
|
||||||
@ -105,28 +105,13 @@ class PlayerService : Service() {
|
|||||||
playerEventListener = PlayerEventListener().also {
|
playerEventListener = PlayerEventListener().also {
|
||||||
addListener(it)
|
addListener(it)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MediaSessionConnector(Otter.get().mediaSession).also {
|
Otter.get().mediaSession.connector.apply {
|
||||||
it.setPlayer(this)
|
setPlayer(player)
|
||||||
it.setQueueNavigator(OtterQueueNavigator())
|
|
||||||
it.setMediaMetadataProvider {
|
|
||||||
buildTrackMetadata(queue.current())
|
|
||||||
}
|
|
||||||
|
|
||||||
it.setMediaButtonEventHandler { player, _, mediaButtonEvent ->
|
setMediaMetadataProvider {
|
||||||
mediaButtonEvent.extras?.getParcelable<KeyEvent>(Intent.EXTRA_KEY_EVENT)?.let { key ->
|
buildTrackMetadata(queue.current())
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,6 +235,8 @@ class PlayerService : Service() {
|
|||||||
audioManager.abandonAudioFocus(audioFocusChangeListener)
|
audioManager.abandonAudioFocus(audioFocusChangeListener)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Otter.get().mediaSession.active = false
|
||||||
|
|
||||||
player.removeListener(playerEventListener)
|
player.removeListener(playerEventListener)
|
||||||
setPlaybackState(false)
|
setPlaybackState(false)
|
||||||
player.release()
|
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) {}
|
|
||||||
}
|
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user