响应媒体按钮

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

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

  • 应用的界面 Activity 可见时
  • 当界面 Activity 被隐藏且应用的媒体会话处于活动状态时
  • 当界面 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 会直接向其发送事件。否则,如果会话未处于活动状态并且有一个 Mediabutton 接收器,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 并生成对媒体会话的回调,请在服务的 onStartCommand() 中添加 MediaButtonReceiver.handleIntent() 方法。这会将键码转换为相应的会话回调方法。

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 来重启会话。

这可让您的应用在其界面不可见的情况下重新开始播放,大多数音频应用都是如此。

使用 MediaSessionCompat 时会自动启用此行为。如果您使用的是 Android 框架的 MediaSession 或支持库 24.0.0 至 25.1.1,则必须调用 setMediaButtonReceiver 才能让媒体按钮重新启动非活动的媒体会话。

您可以通过设置空媒体按钮接收器来停用 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 添加到应用的清单来静态创建该接收器