如何使用Android MediaSessionCompat获取通知回调 [英] How to get notification callbacks using Android MediaSessionCompat

查看:575
本文介绍了如何使用Android MediaSessionCompat获取通知回调的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I'm having some difficulty working with Android's MediaSession.

我一直在研究应该从URL流式传输的原型无线电应用程序.

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.

我检测到onStartCommand正以媒体按钮意图被调用,但是调用MediaButtonReceiver.handleIntent(mediaSession, intent)却没有任何反应.我注册的MediaCallback不会被调用.

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.

我已经查看过有关此文档的文档,观看了Google的youtube系列,将其与一些演示应用程序进行了比较,并通过StackOverflow进行了检索,到目前为止,我一直找不到适合我的应用程序的任何解决方案.

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.

我可以将媒体回调按钮替换为通知上的自定义按钮,但我不想这样做,我更希望将其与MediaSession配合使用,这样我就可以进行观看,自动和锁定屏幕集成.

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.

这是我所需要的服务:

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,因此我实际上不需要包含其中的某些代码,但是我已经测试了没有,并且似乎没有什么区别.

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.

MediaCallback被设计为可重用,其来源:

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

NotificationFactory的来源如下:

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

PlaybackInteractorListenPresenter是体系结构的表示层,因此它们通过事件总线与更广泛的系统进行通信.我将给出它们的摘要,但是除非有必要,否则我将避免发布源代码,因为该帖子已经足够大了.

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在播放,停止或终止被点击/要求时会被告知,并在事件总线上发布这些指令,它还会从总线上读取当前的播放状态并通知视图进行更新(在这种情况下,服务会更新通知).该演示者的另一个实例连接到主屏幕上的按钮.

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侦听开始,停止和终止事件,并调用Player对象的包装类上的要求.当播放器以状态更改进行回调时,它将更新事件总线上的播放状态.当需要终止时,它还会在服务上调用dismiss.

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.

此应用程序中没有MediaBrowser服务,因为我只有一个流,因此没有要浏览的内容,并且据我了解,BrowserService是可选的.

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.

在此问题上您能提供的任何帮助将不胜感激,我一直在尝试自己解决这个问题,但除了死胡同之外,我什么都没有碰到,所以我希望有人能对Media Framework有更多的经验可以阐明这件事.

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.

推荐答案

我无法调用MediaCallbacks,但确实找到了另一个解决方案.

I was unable to get the MediaCallbacks to get called but I did find another solution.

这不是理想的选择,而是让服务拦截这些意图并自行解决,而不是依靠Media Framework来通知状态更改的回调.

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.

这篇关于如何使用Android MediaSessionCompat获取通知回调的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆