Android实现控制第三方音乐播放器暂停/播放(附带源码)

一、项目介绍

1. 背景与意义

在许多智能家居、车载系统、穿戴设备或自定义快捷工具中,我们往往需要跨应用地控制当前系统正在播放的音乐,而不是仅限于自己应用内的播放器。比如:

  • 车载中控:接收方向盘“播放/暂停”按键指令,控制 Spotify、网易云音乐等第三方播放器

  • 自定义桌面 Widget:在桌面小部件上放置播放/暂停按钮,操作系统正在播放的音乐

  • 语音助手:“暂停音乐”指令,让系统当前活跃的音乐应用暂停播放

  • 快捷手势:通过悬浮窗或手势识别触发播放/暂停,而无需打开音乐 App

要实现跨应用媒体控制,需要借助 Android 的**媒体会话(MediaSession)框架或直接发送媒体按键(Media Key)**事件。本项目将深度剖析主流方案,并提供完整可复用的 ThirdPartyMusicController 组件。

2. 功能目标

  • 检测并连接到当前系统中所有活跃的媒体会话(第三方播放器)

  • 提供统一接口 play()pause()toggle()next()previous()

  • 支持 Android 5.0+(Lollipop)及以上系统

  • 自动处理会话权限(Notification Listener 权限或 MediaSessionManager 权限)

  • 兼容通过 MediaSessionManager + NotificationListenerServiceAudioManager.sendMediaKeyEvent 两种方案

  • 易于集成:应用只需初始化一次,即可在任意地方调用控制方法


二、相关知识

1. 媒体会话(MediaSession)架构

Android 5.0 引入了 MediaSession API,用于统一管理媒体播放的状态和控制接口。核心组件:

  • MediaSession:媒体应用内部用于发布播放状态与接收控制命令

  • MediaController:外部应用通过它来连接并控制某个 MediaSession

  • MediaSessionManager:系统级管理器,列出当前所有激活的 MediaSession(需要 Notification Listener 权限或绑定)

媒体会话流程:

  1. 播放器(Spotify)创建并激活一个 MediaSession,并通过 MediaSession.setCallback() 接收控制命令

  2. 客户端(我们的应用)通过 MediaSessionManager.getActiveSessions() 获取所有活跃会话

  3. MediaController 连接到特定 MediaSession,调用 controller.getTransportControls().pause() 等方法发送命令

2. 媒体按键事件(Media Key Button)

Android 提供了对物理或虚拟媒体按键的统一分发。可以通过两种方式模拟媒体按键:

  • AudioManager.sendMediaKeyEvent()(API ≤ 27):直接让系统广播媒体键 Intent

  • Instrumentation 或 InputManager:程序内部生成 KeyEvent 并注入系统

优点:无需额外权限即可模拟“播放/暂停”按键,系统会把事件分发给当前活跃的媒体会话。
缺点:在某些 Android 10+ 设备上可能被限制(需获得 MEDIA_CONTENT_CONTROL 权限或使用系统签名)。

3. NotificationListenerService 权限

为了获取 MediaSessionManager.getActiveSessions() 列表,应用必须注册 NotificationListenerService 并在系统设置中授予“通知读取”权限。这样才能看到其他应用发布的媒体通知并获得对应的会话令牌。

4. 兼容性与权限

  • API21+:MediaSessionManager 可用,但需先绑定 NotificationListener

  • API19-20:MediaSessionManager 部分功能受限,只能通过发送媒体键模拟

  • Android 10+:部分厂商加固,需要兼顾 sendMediaKeyEvent 兼容性或采用官方推荐 MediaSession 接口


三、实现思路

方案一:MediaSessionManager + MediaController

  1. 申请并绑定 NotificationListenerService

  2. 在 Service 中监听 onNotificationPosted,并在 onListenerConnected() 获取初次会话列表:

val mgr = getSystemService(Context.MEDIA_SESSION_SERVICE) as MediaSessionManager
activeSessions = mgr.getActiveSessions(ComponentName(this, MyNotificationListener::class.java))
  1. 过滤出支持播放控制的会话(PlaybackState.STATE_PLAYINGTransportControls 支持 play/pause)

  2. 初始化 MediaController 列表,保存到单例 ThirdPartyMusicController

  3. 对外暴露方法 play(), pause(), toggle(), next(), previous(),内部依次调用所有 controllers

方案二:AudioManager.sendMediaKeyEvent

  1. 获取 AudioManager

val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager

模拟 KEYCODE_MEDIA_PLAY_PAUSE 按键:

val down = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)
val up   = KeyEvent(KeyEvent.ACTION_UP,   KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)
audioManager.dispatchMediaKeyEvent(down)
audioManager.dispatchMediaKeyEvent(up)
  1. 对外暴露同样的接口,并在内部执行上述模拟按键

综合方案

  • 优先使用方案一(MediaSessionManager),权限允许时获取最精准的控制;

  • 否则回退到方案二(MediaKey模拟)保证在无通知权限的场景下也可控制;

  • 使用策略模式封装两个实现,调用者无需关心底层细节。


四、环境与依赖

// app/build.gradle
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'

android {
  compileSdkVersion 34
  defaultConfig {
    applicationId "com.example.thirdpartycontroller"
    minSdkVersion 21
    targetSdkVersion 34
  }
  buildFeatures { viewBinding true }
  kotlinOptions { jvmTarget="1.8" }
}

dependencies {
  implementation 'androidx.appcompat:appcompat:1.6.1'
  implementation 'androidx.core:core-ktx:1.10.1'
}

五、整合代码

// =======================================================
// 文件: AndroidManifest.xml
// 描述: 注册 Activity、NotificationListenerService
// =======================================================
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.thirdpartycontroller">

  <!-- 需要通知监听权限 -->
  <uses-permission android:name="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"/>

  <application
      android:label="ThirdPartyController"
      android:theme="@style/Theme.AppCompat.Light.NoActionBar">

    <activity android:name=".MainActivity"
        android:exported="true">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>

    <service android:name=".MyNotificationListener"
        android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
        android:exported="true">
      <intent-filter>
        <action android:name="android.service.notification.NotificationListenerService"/>
      </intent-filter>
    </service>

  </application>
</manifest>


// =======================================================
// 文件: MainActivity.kt
// 描述: 演示界面:Play/Pause/Next/Prev 四个按钮
// =======================================================
package com.example.thirdpartycontroller

import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.example.thirdpartycontroller.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
  private lateinit var binding: ActivityMainBinding
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)

    // 检查是否已授权通知监听
    if (!ThirdPartyMusicController.hasNotificationListenerPermission(this)) {
      startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS))
      Toast.makeText(this, "请授权“通知读取权限”以便控制播放器", Toast.LENGTH_LONG).show()
    }

    // 按钮事件
    binding.btnPlay.setOnClickListener    { ThirdPartyMusicController.play(this)    }
    binding.btnPause.setOnClickListener   { ThirdPartyMusicController.pause(this)   }
    binding.btnToggle.setOnClickListener  { ThirdPartyMusicController.toggle(this)  }
    binding.btnNext.setOnClickListener    { ThirdPartyMusicController.next(this)    }
    binding.btnPrevious.setOnClickListener{ ThirdPartyMusicController.previous(this)}
  }
}


// =======================================================
// 文件: ActivityMainBinding (res/layout/activity_main.xml)
// 描述: 包含五个按钮
// =======================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:gravity="center"
    android:padding="24dp"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

  <Button android:id="@+id/btnPlay"     android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Play"/>
  <Button android:id="@+id/btnPause"    android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Pause"/>
  <Button android:id="@+id/btnToggle"   android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Toggle"/>
  <Button android:id="@+id/btnNext"     android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Next"/>
  <Button android:id="@+id/btnPrevious" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Previous"/>
</LinearLayout>


// =======================================================
// 文件: MyNotificationListener.kt
// 描述: 通知监听服务,用于获取活跃 MediaSession
// =======================================================
package com.example.thirdpartycontroller

import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Context
import android.service.notification.NotificationListenerService
import android.service.notification.StatusBarNotification
import android.media.session.MediaController
import android.media.session.MediaSessionManager

@SuppressLint("OverrideAbstract")
class MyNotificationListener : NotificationListenerService() {
  override fun onListenerConnected() {
    super.onListenerConnected()
    ThirdPartyMusicController.initController(applicationContext, this)
  }
  override fun onNotificationPosted(sbn: StatusBarNotification) {
    // 新的媒体通知到来时,重新 init
    if (sbn.isOngoing) {
      ThirdPartyMusicController.initController(applicationContext, this)
    }
  }
}


// =======================================================
// 文件: ThirdPartyMusicController.kt
// 描述: 核心控制类,封装两种方案
// =======================================================
package com.example.thirdpartycontroller

import android.content.Context
import android.media.AudioManager
import android.media.session.MediaController
import android.media.session.MediaSessionManager
import android.os.Build
import android.view.KeyEvent

object ThirdPartyMusicController {
  private var controllers: List<MediaController> = emptyList()
  private var audioManager: AudioManager? = null

  /** 初始化:绑定 NotificationListenerService 后调用 */
  fun initController(ctx: Context, listener: NotificationListenerService) {
    audioManager = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
    // 尝试用 MediaSessionManager
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
      val mgr = ctx.getSystemService(Context.MEDIA_SESSION_SERVICE) as MediaSessionManager
      val comp = ComponentName(ctx, MyNotificationListener::class.java)
      controllers = mgr.getActiveSessions(comp)
    }
  }

  /** 检查通知监听权限 */
  fun hasNotificationListenerPermission(ctx: Context): Boolean {
    val enabled = android.provider.Settings.Secure.getString(
      ctx.contentResolver, "enabled_notification_listeners"
    )
    val me = ctx.packageName + "/" + MyNotificationListener::class.java.canonicalName
    return enabled != null && enabled.contains(me)
  }

  /** 播放 */
  fun play(ctx: Context) = sendCommand(MediaCommand.PLAY, ctx)
  /** 暂停 */
  fun pause(ctx: Context) = sendCommand(MediaCommand.PAUSE, ctx)
  /** 切换(播放/暂停) */
  fun toggle(ctx: Context) = sendCommand(MediaCommand.TOGGLE, ctx)
  /** 下一首 */
  fun next(ctx: Context) = sendCommand(MediaCommand.NEXT, ctx)
  /** 上一首 */
  fun previous(ctx: Context) = sendCommand(MediaCommand.PREVIOUS, ctx)

  private fun sendCommand(cmd: MediaCommand, ctx: Context) {
    // 优先 MediaSession
    if (controllers.isNotEmpty()) {
      controllers.forEach { ctrl ->
        when (cmd) {
          MediaCommand.PLAY     -> ctrl.transportControls.play()
          MediaCommand.PAUSE    -> ctrl.transportControls.pause()
          MediaCommand.TOGGLE   -> ctrl.transportControls.playPause()
          MediaCommand.NEXT     -> ctrl.transportControls.skipToNext()
          MediaCommand.PREVIOUS -> ctrl.transportControls.skipToPrevious()
        }
      }
    } else {
      // 回退到媒体按键模拟
      sendMediaKeyEvent(cmd, ctx)
    }
  }

  private fun sendMediaKeyEvent(cmd: MediaCommand, ctx: Context) {
    val am = audioManager ?: ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
    val key = when (cmd) {
      MediaCommand.PLAY, MediaCommand.TOGGLE -> KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
      MediaCommand.PAUSE                     -> KeyEvent.KEYCODE_MEDIA_PAUSE
      MediaCommand.NEXT                      -> KeyEvent.KEYCODE_MEDIA_NEXT
      MediaCommand.PREVIOUS                  -> KeyEvent.KEYCODE_MEDIA_PREVIOUS
    }
    val down = KeyEvent(KeyEvent.ACTION_DOWN, key)
    val up   = KeyEvent(KeyEvent.ACTION_UP, key)
    am.dispatchMediaKeyEvent(down)
    am.dispatchMediaKeyEvent(up)
  }

  private enum class MediaCommand { PLAY, PAUSE, TOGGLE, NEXT, PREVIOUS }
}

六、代码解读

  1. MyNotificationListener

    • 继承 NotificationListenerService,在 onListenerConnected 与新媒体通知到来时调用 ThirdPartyMusicController.initController(),获取并缓存所有活跃的 MediaController

  2. ThirdPartyMusicController

    • 单例封装两种控制方案:

      • 方案一:MediaSessionManager

        • getActiveSessions() 返回当前系统所有发布媒体会话的 MediaController 列表;

        • 调用 controller.transportControls 发送 play()pause() 等指令。

      • 方案二:AudioManager 媒体键发送

        • 调用 AudioManager.dispatchMediaKeyEvent() 模拟媒体按键事件,保证在未授权通知监听时仍可控制。

    • 对外统一方法:play(), pause(), toggle(), next(), previous()

  3. MainActivity

    • 按钮点击时直接调用 ThirdPartyMusicController,无需关注底层实现;

    • 首次启动时检查并引导用户开启“通知读取”权限以支持方案一。

  4. 权限与兼容

    • BIND_NOTIFICATION_LISTENER_SERVICE 在 Manifest 中注册;

    • 在 Android 设置中手动开启应用的“通知访问”权限;

    • API21+ 优先使用 MediaSession,API19-20 则退回媒体键。


七、性能与优化

  1. 会话刷新

    • 当媒体播放器切换或应用被重启时,需在 onNotificationPosted 或定时任务中重新调用 initController()

  2. 权限回退

    • 若用户撤销通知监听权限,应实时检测 hasNotificationListenerPermission(),并清空 controllers 列表,回退到媒体键方案。

  3. 多会话冲突

    • 系统中可能同时存在多个活跃会话(QQ 音乐、网易云、Spotify),可根据包名或 playbackState 过滤优先级最高的那一项。


八、项目总结与拓展

  • 本文实现了跨应用的第三方音乐控制,兼顾精确控制(MediaSession)和极简兼容(MediaKey)。

  • 通过统一的组件接口,业务层无需关心底层技术差异,极大提高了可维护性。

拓展方向

  1. UI 反馈:检测当前是否正在播放(controller.playbackState.state),并更新按钮状态;

  2. 其他命令:支持快进、快退、设置播放位置;

  3. 会话优先级:按最近使用、按包名白名单等方式选取要控制的主体应用;

  4. 可视化 Widget:把控制按钮做成桌面 Widget,实现桌面直接操作。


九、FAQ

Q1:为何需要 Notification Listener 权限?
A1:系统出于隐私安全考虑,将第三方应用的 MediaSession 与通知绑定。只有授权后,才能获得活跃会话列表。

Q2:AudioManager.sendMediaKeyEvent 是否可靠?
A2:在大多数 Android 设备上正常工作,但某些厂商在 Android10+ 对此做了限制。建议优先使用 MediaSession。

Q3:如何过滤只控制某个特定播放器?
A3:在 controllers 列表中,可按 controller.packageName 过滤,仅调用指定包名的会话。

Q4:MediaSessionManager 需要什么权限?
A4:需要“通知监听”权限,且仅在 API21+ 可用。API19-20 上 MediaSessionManager 功能受限。

Q5:如何处理音频焦点?
A5:在控制播放/暂停时,可结合 AudioManager.requestAudioFocus()AudioFocusRequest,确保焦点合理切换。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值