问题
I'm having some difficulty working with Android's MediaSession.
I've been working on a prototype radio application that should stream from a url.
So far I've got it working with a foreground service that gets controlled from a button in the home screen. The audio continues out width the app as expected however I have a notification that either shows a play or stop button depending on the playback state.
My issue is that this button does not work.
I've detected that onStartCommand
is getting called with the media button intent however calling MediaButtonReceiver.handleIntent(mediaSession, intent)
results in nothing happening. My registered MediaCallback
is never called.
I've been over the docs for this, watched Google's youtube series, compared it to some demo apps and trawled through StackOverflow and so far I've been unable to find any solution that works for my app.
I could swap the media callback buttons for custom buttons on the notification but I'd rather not do that, I'd prefer to get it working with the MediaSession so I get watch, auto and lock screen integration.
This what I have for the service:
import android.app.*
import android.content.Context
import android.content.Intent
import android.os.IBinder
import android.support.v4.app.NotificationManagerCompat
import android.support.v4.content.ContextCompat
import project.base.App
import project.dagger.FeatureDagger
import javax.inject.Inject
import android.graphics.BitmapFactory
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat
import android.media.AudioManager
import android.os.Build
import android.support.v4.media.session.MediaButtonReceiver
import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log
import project.dagger.holder.FeatureHolder
import project.extensions.toActivityPendingIntent
import project.story.listen.*
private const val NOTIFICATION_ID = 1
class PlaybackService : Service(), PlaybackInteraction, ListenView {
@Inject lateinit var interactor: PlaybackInteractor
@Inject lateinit var presenter: ListenPresenter
@Inject lateinit var notificationFactory: NotificationFactory
private lateinit var mediaSession: MediaSessionCompat
override fun onBind(intent: Intent?): IBinder? = null
override fun onCreate() {
super.onCreate()
FeatureDagger.create(application as App).component.inject(this)
FeatureHolder.create(application as App)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) notificationFactory.createChannel()
mediaSession = MediaSessionCompat(this, "PlayerService")
mediaSession.setFlags(
MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or
MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS)
mediaSession.setCallback(MediaCallback(
presenter::playTapped,
presenter::stopTapped,
presenter::terminatePlayback))
mediaSession.setSessionActivity(launchIntent())
mediaSession.setMetadata(metadata())
val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
audioManager.requestAudioFocus({
// Ignore
}, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN)
mediaSession.isActive = true
presenter.onViewCreated(this)
presenter.onStart()
interactor.onInteractionCreated(this)
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
MediaButtonReceiver.handleIntent(mediaSession, intent)
return START_NOT_STICKY
}
override fun showState(state: State) =
when (state) {
State.BUFFERING -> buffering()
State.PLAYING -> playing()
State.STOPPED -> stopped()
}
private fun buffering() =
startForeground(NOTIFICATION_ID, notificationFactory.bufferingNotification())
private fun playing() {
mediaSession.setPlaybackState(playingState())
startForeground(NOTIFICATION_ID, notificationFactory.playingNotification(mediaSession))
}
private fun stopped() {
mediaSession.setPlaybackState(stoppedState())
stopForeground(false)
NotificationManagerCompat
.from(this)
.notify(NOTIFICATION_ID, notificationFactory.stoppedNotification(mediaSession))
}
override fun dismiss() {
mediaSession.release()
stopSelf()
}
private fun playingState() =
PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_PLAYING, 0, 0f)
.setActions(PlaybackStateCompat.ACTION_STOP)
.build()
private fun stoppedState() =
PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_STOPPED, 0, 0f)
.setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE)
.build()
private fun metadata() =
MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, "Test Artist")
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, "Test Album")
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Test Track Name")
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, 10000)
.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART,
BitmapFactory.decodeResource(resources,
R.mipmap.ic_launcher))
.build()
private fun launchIntent() =
ListenActivity.buildIntent(this)
.toActivityPendingIntent(this)
companion object {
fun launch(context: Context) =
ContextCompat.startForegroundService(context, Intent(context, PlaybackService::class.java))
}
}
And this is the section of the manifest for it:
<service android:name="project.story.playback.PlaybackService">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</service>
<receiver android:name="android.support.v4.media.session.MediaButtonReceiver">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON"/>
</intent-filter>
</receiver>
My min version is 23 so I actually shouldn't need some of the code included but I've tested without and it seems to make no difference.
The MediaCallback
is designed to be reusable, it's source it:
import android.support.v4.media.session.MediaSessionCompat
class MediaCallback(
private val onPlay: () -> Unit,
private val onPause: () -> Unit,
private val onStop: () -> Unit)
: MediaSessionCompat.Callback() {
override fun onPlay() {
super.onPlay()
onPlay.invoke()
}
override fun onPause() {
super.onPause()
onPause.invoke()
}
override fun onStop() {
super.onStop()
onStop.invoke()
}
}
The source for the NotificationFactory is as follows:
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import android.support.annotation.RequiresApi
import android.support.v4.app.NotificationCompat
import android.support.v4.content.ContextCompat
import android.support.v4.media.session.MediaButtonReceiver
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import project.extensions.toActivityPendingIntent
import project.feature.listen.R
import project.story.listen.ListenActivity
private const val CHANNEL_ID = "playback"
class NotificationFactory(private val context: Context) {
private fun baseNotification() =
NotificationCompat
.Builder(context, CHANNEL_ID)
.setContentTitle(context.getString(R.string.app_name))
.setSmallIcon(uk.co.keithkirk.cuillinfm.R.drawable.ic_notification)
.setColor(ContextCompat.getColor(context, uk.co.keithkirk.cuillinfm.R.color.accent))
.setAutoCancel(false)
.setContentIntent(launchIntent())
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
fun bufferingNotification() =
baseNotification()
.setOngoing(true)
.setContentText(context.getString(R.string.buffering))
.setProgress(0, 0, true)
.build()
fun playingNotification(session: MediaSessionCompat) =
baseNotification()
.setOngoing(true)
.setContentText(context.getString(R.string.playing))
.setStyle(android.support.v4.media.app.NotificationCompat.MediaStyle()
.setMediaSession(session.sessionToken)
.setShowCancelButton(true)
.setCancelButtonIntent(
MediaButtonReceiver.buildMediaButtonPendingIntent(
context,
PlaybackStateCompat.ACTION_STOP)))
.addAction(stopAction())
.build()
fun stoppedNotification(session: MediaSessionCompat) =
baseNotification()
.setOngoing(false)
.setContentText(context.getString(R.string.stopped))
.setDeleteIntent(terminateIntent())
.setStyle(android.support.v4.media.app.NotificationCompat.MediaStyle()
.setMediaSession(session.sessionToken)
.setShowCancelButton(false))
.addAction(playAction())
.build()
@RequiresApi(Build.VERSION_CODES.O)
fun createChannel() {
val channel = NotificationChannel(CHANNEL_ID,
context.getString(R.string.media_playback),
NotificationManager.IMPORTANCE_LOW)
channel.description = context.getString(R.string.media_playback_controls)
channel.setShowBadge(false)
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
(context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager)
.createNotificationChannel(channel)
}
private fun launchIntent() =
ListenActivity.buildIntent(context)
.toActivityPendingIntent(context)
private fun playAction() = NotificationCompat.Action(
R.drawable.ic_play_arrow_white,
context.getString(R.string.play),
playIntent())
private fun stopAction() = NotificationCompat.Action(
R.drawable.ic_stop_white,
context.getString(R.string.stop),
stopIntent())
private fun playIntent() =
MediaButtonReceiver.buildMediaButtonPendingIntent(
context,
PlaybackStateCompat.ACTION_PLAY)
private fun stopIntent() =
MediaButtonReceiver.buildMediaButtonPendingIntent(
context,
PlaybackStateCompat.ACTION_PAUSE)
private fun terminateIntent() =
MediaButtonReceiver.buildMediaButtonPendingIntent(
context,
PlaybackStateCompat.ACTION_STOP)
}
The PlaybackInteractor
and ListenPresenter
are the presentation layer of the architecture so they communicate with the wider system over an eventbus. I'll give a summary of them but I'll avoid posting source unless it's necessary since this post is big enough already.
ListenPresenter
gets told when play, stop or terminate are tapped/required and it posts on the eventbus these instructions, it also reads the current playback state from the bus and notifies the view to update (in this case the service to update the notification). Another instance of this presenter is connected to the button on the home screen.
PlaybackInteractor
listens for start, stop and terminate events and calls the requirement on a wrapper class for the Player object. It updates the playback state on the eventbus when the Player calls back with state changes. It also calls dismiss
on the service when a termination is required.
I don't have a MediaBrowser service in this app as I only have one stream so there is nothing to browse and from my understanding the BrowserService is optional.
Any assistance you can give on this matter would be much appreciated, I've been trying to get this resolved on my own but have hit nothing but dead ends so I'm hoping someone out there with more experience with the Media Framework can shed some light on the matter.
回答1:
I was unable to get the MediaCallbacks to get called but I did find another solution.
It's not ideal but instead of relying on the Media Framework to notify the callback of state changes I had the service intercept the intents and work it out itself.
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
handleIntent(intent)
MediaButtonReceiver.handleIntent(mediaSession, intent)
return START_NOT_STICKY
}
private fun handleIntent(intent: Intent) =
(intent.extras?.get(Intent.EXTRA_KEY_EVENT) as KeyEvent?)?.keyCode.also {
when (it) {
KeyEvent.KEYCODE_MEDIA_PAUSE -> presenter.stopTapped()
KeyEvent.KEYCODE_MEDIA_PLAY -> presenter.playTapped()
KeyEvent.KEYCODE_MEDIA_STOP -> presenter.terminatePlayback()
}
}
Also not the prettiest code although it functions which is enough to unblock development.
来源:https://stackoverflow.com/questions/52017261/how-to-get-notification-callbacks-using-android-mediasessioncompat