android - 如何使用 Android MediaSessionCompat 获取通知回调

标签 android kotlin android-mediasession

我在使用 Android 的 MediaSession 时遇到了一些困难。

我一直在研究应该从 url 流式传输的原型(prototype) radio 应用程序。

到目前为止,我已经将它与前台服务一起使用,该服务可以通过主屏幕中的按钮进行控制。音频按预期继续超出应用程序的宽度,但是我有一个通知,根据播放状态显示播放或停止按钮。

我的问题是这个按钮不起作用。

我检测到 onStartCommand被媒体按钮 Intent 调用,但调用 MediaButtonReceiver.handleIntent(mediaSession, intent)结果什么也没发生。我的注册MediaCallback永远不会被调用。

我已经为此阅读了文档,观看了 Google 的 youtube 系列,将其与一些演示应用程序进行了比较,并浏览了 StackOverflow,到目前为止,我一直无法找到任何适用于我的应用程序的解决方案。

我可以将媒体回调按钮换成通知上的自定义按钮,但我宁愿不这样做,我更愿意让它与 MediaSession 一起工作,这样我就可以获得 watch 、自动和锁屏集成。

这就是我的服务:

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))
    }
}

这是它的 list 部分:
<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)
}
PlaybackInteractorListenPresenter是架构的表示层,因此它们通过事件总线与更广泛的系统进行通信。我将对其进行总结,但除非有必要,否则我会避免发布源代码,因为这篇文章已经足够大了。
ListenPresenter当播放、停止或终止被点击/需要时被告知,它在事件总线上发布这些指令,它还从总线读取当前播放状态并通知 View 更新(在这种情况下,服务更新通知)。此演示者的另一个实例连接到主屏幕上的按钮。
PlaybackInteractor监听开始、停止和终止事件,并调用 Player 对象的包装类要求。当播放器通过状态更改进行回调时,它会更新事件总线上的播放状态。它还调用 dismiss在需要终止服务时。

我在这个应用程序中没有 MediaBrowser 服务,因为我只有一个流,所以没有什么可以浏览的,据我了解 BrowserService 是可选的。

您可以在此问题上提供的任何帮助将不胜感激,我一直在尝试自己解决此问题,但除了死胡同外什么都没遇到,所以我希望在媒体框架方面有更多经验的人可以摆脱一些对此事轻描淡写。

最佳答案

我无法让 MediaCallbacks 被调用,但我确实找到了另一种解决方案。

这并不理想,但不是依靠媒体框架来通知状态更改的回调,而是让服务拦截 Intent 并自行解决。

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()
            }
        }

也不是最漂亮的代码,尽管它的功能足以解除开发障碍。

关于android - 如何使用 Android MediaSessionCompat 获取通知回调,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52017261/

相关文章:

android - 密封类在 Android 环境中的性能影响是什么?

android - 尝试构建发行版后,Android崩溃

android - 单击耳机按钮时应用程序崩溃

java - 锁屏播放器控件和元数据

android - 回收站 View 滚动不流畅

android - 如何使用 OpenGL-ES 2 在 Android 中加载和显示 .obj 文件

java - 这是 Jackson JsonParser 中的错误,还是我做错了什么?

android - MediaBrowserCompat 队列查找项目

java - 如何使用 addChildEventListener 仅获取已在 Firebase 中修改的数据?

java - 如何在 google map v2 for android 中显示标记旁边的文本