Streamline the way the media session is controled across devices.

This commit is contained in:
Antoine POPINEAU 2020-07-12 18:28:50 +02:00
parent e7cb5e4c6e
commit b0640cf1b2
No known key found for this signature in database
GPG Key ID: A78AC64694F84063
4 changed files with 61 additions and 79 deletions

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.github.apognu.otter">
<uses-permission android:name="android.permission.INTERNET" />
@ -19,7 +20,7 @@
android:usesCleartextTraffic="true">
<activity
android:name="com.github.apognu.otter.activities.SplashActivity"
android:name=".activities.SplashActivity"
android:launchMode="singleInstance"
android:noHistory="true">
@ -33,36 +34,48 @@
</activity>
<activity
android:name="com.github.apognu.otter.activities.LoginActivity"
android:name=".activities.LoginActivity"
android:configChanges="screenSize|orientation"
android:launchMode="singleInstance" />
<activity android:name="com.github.apognu.otter.activities.MainActivity" />
<activity android:name=".activities.MainActivity" />
<activity
android:name="com.github.apognu.otter.activities.SearchActivity"
android:name=".activities.SearchActivity"
android:launchMode="singleTop" />
<activity android:name="com.github.apognu.otter.activities.DownloadsActivity" />
<activity android:name=".activities.DownloadsActivity" />
<activity android:name="com.github.apognu.otter.activities.SettingsActivity" />
<activity android:name=".activities.SettingsActivity" />
<activity android:name="com.github.apognu.otter.activities.LicencesActivity" />
<activity android:name=".activities.LicencesActivity" />
<service
android:name="com.github.apognu.otter.playback.PlayerService"
android:foregroundServiceType="mediaPlayback" />
android:name=".playback.PlayerService"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</service>
<service
android:name=".playback.PinService"
android:exported="false">
<intent-filter>
<action android:name="com.google.android.exoplayer.downloadService.action.RESTART" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>
<receiver android:name="com.github.apognu.otter.playback.MediaControlActionReceiver" />
<receiver android:name="androidx.media.session.MediaButtonReceiver">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
</application>

View File

@ -3,16 +3,19 @@ package com.github.apognu.otter.playback
import android.app.Notification
import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.media.app.NotificationCompat.MediaStyle
import androidx.media.session.MediaButtonReceiver
import com.github.apognu.otter.R
import com.github.apognu.otter.activities.MainActivity
import com.github.apognu.otter.utils.*
import com.github.apognu.otter.utils.AppContext
import com.github.apognu.otter.utils.Track
import com.github.apognu.otter.utils.log
import com.github.apognu.otter.utils.maybeNormalizeUrl
import com.squareup.picasso.Picasso
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.Default
@ -21,14 +24,13 @@ import kotlinx.coroutines.launch
class MediaControlsManager(val context: Service, private val scope: CoroutineScope, private val mediaSession: MediaSessionCompat) {
companion object {
const val NOTIFICATION_ACTION_OPEN_QUEUE = 0
const val NOTIFICATION_ACTION_PREVIOUS = 1
const val NOTIFICATION_ACTION_TOGGLE = 2
const val NOTIFICATION_ACTION_NEXT = 3
}
private var notification: Notification? = null
fun updateNotification(track: Track?, playing: Boolean) {
"updateNotification".log()
if (notification == null && !playing) return
track?.let {
@ -74,19 +76,19 @@ class MediaControlsManager(val context: Service, private val scope: CoroutineSco
.addAction(
action(
R.drawable.previous, context.getString(R.string.control_previous),
NOTIFICATION_ACTION_PREVIOUS
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
)
)
.addAction(
action(
stateIcon, context.getString(R.string.control_toggle),
NOTIFICATION_ACTION_TOGGLE
PlaybackStateCompat.ACTION_PLAY_PAUSE
)
)
.addAction(
action(
R.drawable.next, context.getString(R.string.control_next),
NOTIFICATION_ACTION_NEXT
PlaybackStateCompat.ACTION_SKIP_TO_NEXT
)
)
.build()
@ -102,26 +104,9 @@ class MediaControlsManager(val context: Service, private val scope: CoroutineSco
}
}
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)
return NotificationCompat.Action.Builder(icon, title, pendingIntent).build()
}
}
class MediaControlActionReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
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))
private fun action(icon: Int, title: String, id: Long): NotificationCompat.Action {
return MediaButtonReceiver.buildMediaButtonPendingIntent(context, id).run {
NotificationCompat.Action.Builder(icon, title, this).build()
}
}
}

View File

@ -6,8 +6,6 @@ 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
@ -15,7 +13,7 @@ import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
class MediaSession(private val context: Context) {
var active: Boolean = false
var active = false
private val playbackStateBuilder = PlaybackStateCompat.Builder().apply {
setActions(
@ -30,41 +28,28 @@ class MediaSession(private val context: Context) {
}
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
active = true
}
}
val connector: MediaSessionConnector by lazy {
MediaSessionConnector(Otter.get().mediaSession.session).also {
MediaSessionConnector(session).also {
it.setQueueNavigator(OtterQueueNavigator())
it.setMediaButtonEventHandler { _, _, event ->
it.setMediaButtonEventHandler { _, _, intent ->
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
}
context.startService(Intent(context, PlayerService::class.java).apply {
action = intent.action
command?.let {
CommandBus.send(command)
CommandBus.send(Command.StartService(command))
intent.extras?.let { extras -> putExtras(extras) }
})
return@setMediaButtonEventHandler true
}
}
}
return@setMediaButtonEventHandler true
}
false

View File

@ -13,6 +13,7 @@ import android.os.Build
import android.os.IBinder
import android.support.v4.media.MediaMetadataCompat
import androidx.core.app.NotificationManagerCompat
import androidx.media.session.MediaButtonReceiver
import com.github.apognu.otter.Otter
import com.github.apognu.otter.R
import com.github.apognu.otter.utils.*
@ -55,18 +56,14 @@ class PlayerService : Service() {
private lateinit var radioPlayer: RadioPlayer
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
intent?.action?.let {
if (it == Intent.ACTION_MEDIA_BUTTON) {
MediaButtonReceiver.handleIntent(Otter.get().mediaSession.session, intent)
}
}
if (!started) {
watchEventBus()
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()
}
}
}
started = true
@ -98,8 +95,6 @@ class PlayerService : Service() {
}
}
Otter.get().mediaSession.active = true
mediaControlsManager = MediaControlsManager(this, scope, Otter.get().mediaSession.session)
player = SimpleExoPlayer.Builder(this).build().apply {
@ -110,6 +105,8 @@ class PlayerService : Service() {
}
}
Otter.get().mediaSession.active = true
Otter.get().mediaSession.connector.apply {
setPlayer(player)
@ -247,12 +244,12 @@ class PlayerService : Service() {
audioManager.abandonAudioFocus(audioFocusChangeListener)
})
Otter.get().mediaSession.active = false
player.removeListener(playerEventListener)
setPlaybackState(false)
player.release()
Otter.get().mediaSession.active = false
super.onDestroy()
}
@ -401,8 +398,10 @@ class PlayerService : Service() {
override fun onTracksChanged(trackGroups: TrackGroupArray, trackSelections: TrackSelectionArray) {
super.onTracksChanged(trackGroups, trackSelections)
queue.current = player.currentWindowIndex
mediaControlsManager.updateNotification(queue.current(), player.playWhenReady)
if (queue.current != player.currentWindowIndex) {
queue.current = player.currentWindowIndex
mediaControlsManager.updateNotification(queue.current(), player.playWhenReady)
}
if (queue.get().isNotEmpty() && queue.current() == queue.get().last() && radioPlayer.isActive()) {
scope.launch(IO) {