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

View File

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

View File

@ -6,8 +6,6 @@ import android.os.Bundle
import android.os.ResultReceiver import android.os.ResultReceiver
import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat 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.Command
import com.github.apognu.otter.utils.CommandBus import com.github.apognu.otter.utils.CommandBus
import com.google.android.exoplayer2.ControlDispatcher import com.google.android.exoplayer2.ControlDispatcher
@ -15,7 +13,7 @@ import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
class MediaSession(private val context: Context) { class MediaSession(private val context: Context) {
var active: Boolean = false var active = false
private val playbackStateBuilder = PlaybackStateCompat.Builder().apply { private val playbackStateBuilder = PlaybackStateCompat.Builder().apply {
setActions( setActions(
@ -30,41 +28,28 @@ class MediaSession(private val context: Context) {
} }
val session: MediaSessionCompat by lazy { val session: MediaSessionCompat by lazy {
active = true
MediaSessionCompat(context, context.packageName).apply { MediaSessionCompat(context, context.packageName).apply {
setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS) setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS)
setPlaybackState(playbackStateBuilder.build()) setPlaybackState(playbackStateBuilder.build())
isActive = true isActive = true
active = true
} }
} }
val connector: MediaSessionConnector by lazy { val connector: MediaSessionConnector by lazy {
MediaSessionConnector(Otter.get().mediaSession.session).also { MediaSessionConnector(session).also {
it.setQueueNavigator(OtterQueueNavigator()) it.setQueueNavigator(OtterQueueNavigator())
it.setMediaButtonEventHandler { _, _, event -> it.setMediaButtonEventHandler { _, _, intent ->
if (!active) { if (!active) {
event.extras?.getParcelable<KeyEvent>(Intent.EXTRA_KEY_EVENT)?.let { key -> context.startService(Intent(context, PlayerService::class.java).apply {
if (key.action == KeyEvent.ACTION_UP) { action = intent.action
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 { intent.extras?.let { extras -> putExtras(extras) }
CommandBus.send(command) })
CommandBus.send(Command.StartService(command))
return@setMediaButtonEventHandler true return@setMediaButtonEventHandler true
}
}
}
} }
false false

View File

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