title: How to get notification callbacks using Android MediaSessionCompat
body: <p>I'm having some difficulty working with Android's MediaSession.</p>
  
  <p>I've been working on a prototype radio application that should stream from a url. </p>
  
  <p>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.</p>
  
  <p>My issue is that this button does not work.</p>
  
  <p>I've detected that <code>onStartCommand</code> is getting called with the media button intent however calling <code>MediaButtonReceiver.handleIntent(mediaSession, intent)</code> results in nothing happening. My registered <code>MediaCallback</code> is never called.</p>
  
  <p>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.</p>
  
  <p>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.</p>
  
  <p>This what I have for the service: </p>
  
  <pre><code>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 &gt;= 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 -&gt; buffering()
                  State.PLAYING -&gt; playing()
                  State.STOPPED -&gt; 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))
      }
  }
  </code></pre>
  
  <p>And this is the section of the manifest for it:</p>
  
  <pre><code>&lt;service android:name="project.story.playback.PlaybackService"&gt;
              &lt;intent-filter&gt;
                  &lt;action android:name="android.intent.action.MEDIA_BUTTON" /&gt;
              &lt;/intent-filter&gt;
          &lt;/service&gt;
  
          &lt;receiver android:name="android.support.v4.media.session.MediaButtonReceiver"&gt;
              &lt;intent-filter&gt;
                  &lt;action android:name="android.intent.action.MEDIA_BUTTON"/&gt;
              &lt;/intent-filter&gt;
          &lt;/receiver&gt;
  </code></pre>
  
  <p>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.</p>
  
  <p>The <code>MediaCallback</code> is designed to be reusable, it's source it:</p>
  
  <pre><code>import android.support.v4.media.session.MediaSessionCompat
  
  class MediaCallback(
          private val onPlay: () -&gt; Unit,
          private val onPause: () -&gt; Unit,
          private val onStop: () -&gt; Unit)
      : MediaSessionCompat.Callback() {
  
      override fun onPlay() {
          super.onPlay()
  
          onPlay.invoke()
      }
  
      override fun onPause() {
          super.onPause()
  
          onPause.invoke()
      }
  
      override fun onStop() {
          super.onStop()
  
          onStop.invoke()
      }
  }
  </code></pre>
  
  <p>The source for the NotificationFactory is as follows:</p>
  
  <pre><code>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)
  }
  </code></pre>
  
  <p>The <code>PlaybackInteractor</code> and <code>ListenPresenter</code> 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.</p>
  
  <p><code>ListenPresenter</code> 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.</p>
  
  <p><code>PlaybackInteractor</code> 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 <code>dismiss</code> on the service when a termination is required.</p>
  
  <p>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.</p>
  
  <p>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.</p>