响应媒体按钮

媒体按钮是 Android 设备及其他外围设备上的硬件按钮,例如蓝牙耳机上的暂停/播放按钮。当用户按下媒体按钮时,Android 会生成一个 KeyEvent,其中包含一个用于标识该按钮的键码。媒体按钮 KeyEvent 的键码是常量,以 KEYCODE_MEDIA 开头(例如,KEYCODE_MEDIA_PLAY)。

应用应能够按以下优先顺序处理三种情况下的媒体按钮事件:

  • 当应用的 UI Activity 可见时
  • 当 UI Activity 隐藏且应用的媒体会话处于活动状态时
  • 当 UI Activity 隐藏且应用的媒体会话不活动需要重启时

在前台 Activity 中处理媒体按钮

前台 activity 在其 onKeyDown() 方法中接收媒体按钮按键事件。根据运行的 Android 版本,系统有两种方式将事件路由到媒体控制器:

  • 如果您运行的是 Android 5.0 (API 级别 21) 或更高版本,请调用 FLAG_HANDLES_MEDIA_BUTTONS MediaBrowserCompat.ConnectionCallback.onConnected。这将自动调用媒体控制器的 dispatchMediaButtonEvent(),后者会将键码转换为媒体会话回调。
  • 在 Android 5.0 (API 级别 21) 之前,您需要修改 onKeyDown() 以自行处理事件。(有关详细信息,请参阅在活动的媒体会话中处理媒体按钮。)以下代码段展示了如何拦截键码并调用 dispatchMediaButtonEvent()。请务必返回 true 以指示事件已处理:

    Kotlin

        fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                return super.onKeyDown(keyCode, event)
            }
            when (keyCode) {
                KeyEvent.KEYCODE_MEDIA_PLAY -> {
                    yourMediaController.dispatchMediaButtonEvent(event)
                    return true
                }
            }
            return super.onKeyDown(keyCode, event)
        }
        

    Java

        @Override
        boolean onKeyDown(int keyCode, KeyEvent event) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                  return super.onKeyDown(keyCode, event);
                }
                switch (keyCode) {
                  case KeyEvent.KEYCODE_MEDIA_PLAY:
                          yourMediaController.dispatchMediaButtonEvent(event);
                          return true;
                }
                return super.onKeyDown(keyCode, event);
        }
        

查找媒体会话

如果前台 activity 未处理该事件,Android 将尝试查找可以处理该事件的媒体会话。同样,根据运行的 Android 版本,有两种方式来搜索媒体会话:

  • 如果您运行的是 Android 8.0 (API 级别 26) 或更高版本,系统会尝试查找最后一个播放本地音频的 MediaSession 应用。如果会话仍处于活动状态,Android 会直接将事件发送到该会话。否则,如果会话不活动且具有媒体按钮接收器,Android 会将事件发送到接收器,接收器将重新启动会话,从而使其能够接收事件。(有关详细信息,请参阅使用媒体按钮重新启动不活动的媒体会话。)如果会话没有媒体按钮接收器,系统会丢弃媒体按钮事件,什么也不会发生。逻辑如下图所示:

  • 在 Android 8.0 (API 级别 26) 之前,系统会尝试将事件发送到活动的媒体会话。如果存在多个活动的媒体会话,Android 会尝试选择一个正在准备播放(缓冲/连接)、正在播放或已暂停的媒体会话,而不是已停止的媒体会话。(有关详细信息,请参阅在活动的媒体会话中处理媒体按钮。)如果没有活动的会话,Android 会尝试将事件发送到最近活动的会话。(有关详细信息,请参阅使用媒体按钮重新启动不活动的媒体会话。)逻辑如下图所示:

在活动的媒体会话中处理媒体按钮

在 Android 5.0 (API 级别 21) 及更高版本上,Android 会通过调用 onMediaButtonEvent() 自动将媒体按钮事件分派到您的活动媒体会话。默认情况下,此回调会将 KeyEvent 转换为与键码匹配的相应媒体会话回调方法。

在 Android 5.0 (API 级别 21) 之前,Android 通过广播带有 ACTION_MEDIA_BUTTON 操作的 intent 来处理媒体按钮事件。您的应用必须注册 BroadcastReceiver 来拦截这些 intent。MediaButtonReceiver 类正是为此目的而设计的。它是 Android media-compat 库中的一个便捷类,它处理 ACTION_MEDIA_BUTTON 并将传入的 Intent 转换为适当的 MediaSessionCompat.Callback 方法调用。

MediaButtonReceiver 是一个短暂的 BroadcastReceiver。它将传入的 intent 转发给管理您的媒体会话的服务。如果您想在 Android 5.0 之前的系统中使用媒体按钮,则必须在清单中包含 MediaButtonReceiver,并带有 MEDIA_BUTTON Intent 过滤器。

<receiver android:name="android.support.v4.media.session.MediaButtonReceiver" >
   <intent-filter>
     <action android:name="android.intent.action.MEDIA_BUTTON" />
   </intent-filter>
 </receiver>

BroadcastReceiver 将 intent 转发到您的服务。要解析 intent 并向您的媒体会话生成回调,请将 MediaButtonReceiver.handleIntent() 方法包含在您的服务的 onStartCommand() 中。这会将键码转换为相应的会话回调方法。

Kotlin

private val mediaSessionCompat: MediaSessionCompat = ...

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    MediaButtonReceiver.handleIntent(mediaSessionCompat, intent)
    return super.onStartCommand(intent, flags, startId)
}

Java

private MediaSessionCompat mediaSessionCompat = ...;

 public int onStartCommand(Intent intent, int flags, int startId) {
   MediaButtonReceiver.handleIntent(mediaSessionCompat, intent);
   return super.onStartCommand(intent, flags, startId);
 }

使用媒体按钮重新启动不活动的媒体会话

如果 Android 能够识别上次活动的媒体会话,它会尝试通过向清单中注册的组件(例如服务或 BroadcastReceiver)发送 ACTION_MEDIA_BUTTON Intent 来重新启动会话。

这允许您的应用在其 UI 不可见时重新开始播放,这对于大多数音频应用来说都是如此。

当您使用 MediaSessionCompat 时,此行为会自动启用。如果您使用 Android 框架的 MediaSession 或 Support Library 24.0.0 到 25.1.1,您必须调用 setMediaButtonReceiver 才能让媒体按钮重新启动不活动的媒体会话。

您可以通过设置 null 媒体按钮接收器,在 Android 5.0 (API 级别 21) 及更高版本中禁用此行为:

Kotlin

// Create a MediaSessionCompat
mediaSession = MediaSessionCompat(context, LOG_TAG)
mediaSession.setMediaButtonReceiver(null)

Java

// Create a MediaSessionCompat
mediaSession = new MediaSessionCompat(context, LOG_TAG);
mediaSession.setMediaButtonReceiver(null);

自定义媒体按钮处理程序

onMediaButtonEvent() 的默认行为会提取键码,并使用媒体会话的当前状态和支持的操作列表来确定要调用哪个方法。例如,KEYCODE_MEDIA_PLAY 会调用 onPlay()

为了在所有应用中提供一致的媒体按钮体验,您应该使用默认行为,并且只针对特定目的进行偏差。如果媒体按钮需要自定义处理,请重写回调的 onMediaButtonEvent() 方法,使用 intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT) 提取 KeyEvent,自行处理事件,然后返回 true

摘要

要在所有 Android 版本中正确处理媒体按钮事件,您必须在创建媒体会话时指定 FLAG_HANDLES_MEDIA_BUTTONS

此外,根据您计划支持的 Android 版本,您还必须满足以下要求:

在 Android 5.0 或更高版本中运行时

  • 从媒体控制器 onConnected() 回调中调用 MediaControllerCompat.setMediaController()
  • 要允许媒体按钮重新启动不活动的会话,请通过调用 setMediaButtonReceiver() 并向其传递 PendingIntent 来动态创建 MediaButtonReceiver

在 Android 5.0 之前的系统中运行时

  • 重写 activity 的 onKeyDown() 以处理媒体按钮
  • 通过将其添加到应用的清单中来静态创建 MediaButtonReceiver