Мне трудно работать с MediaSession для Android.
Я работал над прототипом радио-приложения, которое должно транслироваться с URL.
Пока у меня есть работа с сервисом переднего плана, который управляется с помощью кнопки на домашнем экране. Звук продолжается по ширине приложения, как и ожидалось, однако у меня есть уведомление, в котором отображается кнопка воспроизведения или остановки в зависимости от состояния воспроизведения.
Моя проблема в том, что эта кнопка не работает.
Я обнаружил, что onStartCommand
вызывается с намерением нажатия кнопки мультимедиа, однако при вызове MediaButtonReceiver.handleIntent(mediaSession, intent)
ничего не происходит. Мой зарегистрированный MediaCallback
никогда не вызывается.
Я изучил эту документацию, посмотрел серию статей о YouTube на Google, сравнил ее с некоторыми демонстрационными приложениями и просмотрел их через StackOverflow, и до сих пор мне не удалось найти решение, подходящее для моего приложения.
Я мог бы поменять кнопки обратного вызова мультимедиа для пользовательских кнопок в уведомлении, но я бы предпочел не делать этого, я бы предпочел, чтобы оно работало с MediaSession, поэтому я получаю интеграцию просмотра, авто и блокировки экрана.
Вот что я имею за услугу:
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))
}
}
И это раздел манифеста для него:
<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>
Моя минимальная версия - 23, так что на самом деле мне не нужно включать часть кода, но я тестировал без него, и, похоже, это не имеет значения.
* * * * * * * * * * MediaCallback
предназначен для многократного использования, его источник:
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()
}
}
Источник для NotificationFactory выглядит следующим образом:
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)
}
PlaybackInteractor
и ListenPresenter
являются уровнем представления архитектуры, поэтому они взаимодействуют с более широкой системой через шину событий. Я приведу их краткое изложение, но постараюсь не публиковать источник, если в этом нет необходимости, поскольку этот пост уже достаточно велик.
ListenPresenter
сообщается, когда воспроизведение, остановка или завершение касаются / требуются, и он отправляет на шину событий эти инструкции, он также считывает текущее состояние воспроизведения с шины и уведомляет представление об обновлении (в этом случае служба обновить уведомление). Другой экземпляр этого докладчика подключен к кнопке на главном экране.
PlaybackInteractor
прослушивает события запуска, остановки и завершения и вызывает требование к классу-оболочке для объекта Player. Он обновляет состояние воспроизведения на шине событий, когда проигрыватель перезванивает с изменениями состояния. Он также вызывает dismiss
на услуге, когда требуется прекращение.
У меня нет службы MediaBrowser в этом приложении, поскольку у меня есть только один поток, поэтому просматривать нечего, и, насколько я понимаю, BrowserService не является обязательным.
Любая помощь, которую вы можете оказать в этом вопросе, была бы очень признательна, я пытался решить эту проблему самостоятельно, но ничего не нашел, кроме тупиков, так что я надеюсь, что кто-то с большим опытом работы с Media Framework может пролить свет на этот вопрос.