news 2026/9/9 14:41:26

targetSdk 33升级后蓝牙耳机媒体按键失灵?MediaSession排查与适配指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
targetSdk 33升级后蓝牙耳机媒体按键失灵?MediaSession排查与适配指南

前几天被组里测试拉到会议室,说升级到 targetSdk 33 之后,蓝牙耳机的播放/暂停键全部失灵,App 里的 MediaSession 收不到媒体按键。当时我第一反应是代码改动出了问题,结果回查 commit,MediaSession 相关的代码一行没动。真正开始排查才发现,API 33 之后的媒体按键分发机制比之前想象中复杂很多:通知权限、前台服务类型、MediaButtonReceiver 的导出声明、音频焦点、PlaybackState 的 actions,任何一环没对上,系统都会默默把按键交给别的应用或者直接吞掉。

这篇文章不是讲基础 API 怎么调,而是分享我从"按键完全没反应"到"稳定复现并修复"的完整排查过程,以及最终沉淀下来的一套可复用实现。如果你正在做音乐播放器、播客、有声书这类媒体类 App,升级 targetSdk 33+ 后也遇到媒体按键失灵,这篇内容应该能帮你省下至少两天排查时间。

1. 问题现象与触发链路:API 33 之后媒体按键为什么忽然没人接了

1.1 现象回顾:不同设备表现差别很大

先说现象。测试反馈里,同样是升级后的包,表现却不完全一致:

  • 某台 Android 13 真机上,蓝牙耳机按键完全没反应,App 内按钮正常。
  • 另一台 Android 12 的真机上,一切正常。
  • 还有一台 Android 13 设备,锁屏页能看到媒体卡片,但点卡片上的暂停键没响应。

这个"不同设备表现不同"很关键。如果只有一台设备出问题,可能是设备蓝牙协议问题;但多台 Android 13/14 设备同时异常,几乎可以确定是系统版本变化导致的路由策略变了。

我当时的排查方向一度走偏,以为是耳机兼容性问题,甚至换了三副耳机测试。直到把系统日志打开,发现媒体按键事件根本没有进到我们 Service 的onStartCommand,才意识到问题出在"系统要不要把按键发给你"这一层,而不是"你收到之后怎么处理"这一层。

1.2 系统媒体按键路由的三条判定规则

API 33 之后,Android 系统判断"媒体按键应该交给哪个 App"时,核心看三件事:

  1. 是否存在一个处于活跃状态的 MediaSession。mediaSession.isActive = true是最基本的前提。
  2. 该 MediaSession 是否绑定了一个"能被系统识别为媒体通知"的通知。也就是说,通知必须使用NotificationCompat.MediaStyle,并且调用了setMediaSession(session.sessionToken)
  3. 当前 App 是否有"媒体控制权"。这个控制权由音频焦点、最近活跃状态、系统媒体控制中心里的会话优先级共同决定。

以前很多开发者的做法是:在onCreatesetActive(true),然后就不管了。API 33 之前这套确实能跑,因为系统对媒体按键的分发相对宽松,哪怕没有通知、没有焦点,只要 session 存在并且 active,按键事件大概率能到MediaSession.Callback

API 33 之后,系统媒体控制中心(Media Controls)成为了媒体会话的"总调度台"。它不只负责 UI 显示,还负责把MEDIA_BUTTON事件路由给"它认为当前应该响应的媒体会话"。如果你的 App 没有满足上面三条判定规则,你在系统眼里就不是"活跃媒体应用",音量键旁边的媒体卡片不会出现,耳机上的播放/暂停键自然也不会发给你。

1.3 大多数"升级后失灵"的触发点

我后来把这个问题的触发点总结成了一张图,排查时对着看非常快:

  • 通知权限被拒绝。Android 13 开始,POST_NOTIFICATIONS变成了运行时权限。如果用户没有授权,App 无法发布任何通知,媒体通知自然发不出来。没有媒体通知的 session,在部分机型上会被系统直接忽略。
  • 媒体通知没用 MediaStyle。有些项目直接用普通NotificationCompat.Builder构建通知,然后塞一个contentIntent就算了。这种通知在系统媒体控制中心里不会被识别为媒体会话,锁屏媒体控件不显示,按键也不分发。
  • 前台服务类型没声明。targetSdk 34 之后,启动媒体播放前台服务必须声明foregroundServiceType="mediaPlayback"并携带FOREGROUND_SERVICE_MEDIA_PLAYBACK权限,否则 Service 直接抛异常,session 根本没机会 active。
  • MediaButtonReceiverexported设置不对。API 31 之后强制要求显式声明 exported,很多项目从老版本升上来,随手写了android:exported="false",系统广播进不来,按键事件直接丢失。

你可能已经发现了,这些触发点都不是什么"高深"的东西,但因为分散在 Manifest、通知权限、前台服务、音频焦点好几个模块里,单独看哪一块都觉得自己没问题,合在一起才导致按键完全失联。

2. 从通知权限到前台服务:一份按优先级排序的排查清单

2.1 第一站:通知权限(POST_NOTIFICATIONS)

很多项目升级 targetSdk 33 之后,只在 Manifest 里加了:

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

然后就没有然后了。这是不够的。POST_NOTIFICATIONS是运行时权限,targetSdk 33+ 应用在 Android 13 及以上设备上运行时,必须像请求存储权限一样,在代码里动态申请。

如果你不申请,或者用户拒绝,最直接的结果是:NotificationManager.notify()虽然不会抛异常,但通知不会显示。媒体通知不显示,系统媒体控制中心里就没有你的会话,耳机按键也就不给你。

我用了一个很直接的验证方式:把 App 的通知权限从系统设置里手动打开,再去按耳机键,按键立刻恢复。这样就能确认问题是不是出在通知权限上。

处理方案是在启动阶段就主动申请:

if (Build.VERSION.SDK_INT >= 33 && ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED ) { ActivityCompat.requestPermissions( activity, arrayOf(Manifest.permission.POST_NOTIFICATIONS), REQUEST_CODE_POST_NOTIFICATIONS ) }

建议不要用"用户拒绝一次就永久不问"的策略。媒体类 App 的通知权限直接影响核心功能,最好在权限弹窗之前先给用户解释一下:这个权限用于显示正在播放的媒体通知,让你能用耳机和锁屏控制播放。用户的接受率会高很多。

2.2 第二站:前台服务声明与 Android 14 的类型要求

媒体播放类 App 几乎一定会用到前台服务。一方面是为了在后台持续播放,另一方面,系统对"媒体应用"的判定也和前台服务强相关。

Android 14(API 34)开始,前台服务必须指定类型。如果你的 App targetSdk 升到了 34,Manifest 里的 service 声明必须长这样:

<service android:name=".MediaPlaybackService" android:foregroundServiceType="mediaPlayback" android:exported="false" />

并声明权限:

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />

代码里启动前台服务时,Android 14 上也要传类型:

if (Build.VERSION.SDK_INT >= 34) { startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK) } else { startForeground(NOTIFICATION_ID, notification) }

这里有个很容易忽略的点:如果你只在 Manifest 里写了foregroundServiceType,但代码里调用startForeground()时没有传第三参,或者第三参传的 mask 和 Manifest 不一致,API 34 上同样会出问题。我遇到过的情况是:Manifest 已经写了mediaPlayback,但startForeground()漏传了类型,结果真机直接MissingForegroundServiceTypeException

顺带一提,如果你的 App 还有一个"下载"或"播放缓存"的前台服务,不要混用类型。后台下载用dataSync,媒体播放用mediaPlayback,分开声明,否则在系统层面可能出现类型冲突,导致某些机型上前台服务启动失败。

2.3 第三站:MediaButtonReceiver 的 exported 与广播分发

这是老项目升级时最容易踩的坑。

Android 12(API 31)开始,所有带intent-filter的四大组件都必须显式声明android:exported,否则安装或更新时会直接失败。很多项目为了通过编译和安装,随手统一加了android:exported="false",正好把MediaButtonReceiver也给设成了 false。

问题在于,MediaButtonReceiver接收的是系统发出的android.intent.action.MEDIA_BUTTON广播,系统属于"外部调用方"。如果 exported 是 false,这个广播根本无法到达你的 receiver。

正确的声明方式:

<receiver android:name=".MediaButtonReceiver" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MEDIA_BUTTON" /> </intent-filter> </receiver>

注意,不要因为担心安全风险就把它设成 false。这里的 exported true 是系统分发媒体按钮事件所必需的,跟普通的外部应用自定义广播不是一回事。当然,你可以在 Receiver 内部校验 intent 的 action 和 package 来源,做一层保护,但 exported 属性不能改。

2.4 用 dumpsys media_session 验证路由

排查到这一步,如果还不行,就该用系统工具看看 session 的真实状态了。

adb shell dumpsys media_session会输出当前所有 MediaSession 的信息,包括包名、是否 active、PlaybackState 的 state、actions,以及系统当前认定的 "button receiver"。

我自己排查时重点关注这几块:

  • 输出里有没有我们 App 的包名。如果没有,说明 service 没启动,或者 session 没创建,或者 session 被释放了。
  • 有没有active=true。如果 session 存在但没 active,系统不会把它纳入媒体按键候选。
  • state=是什么。如果是STATE_NONE,系统会认为你当前没有播放内容,很可能不显示媒体卡片,也不分发按键。
  • actions=里有没有PLAY_PAUSE之类。actions 为空的话,系统可能认为你这个 session 不支持媒体控制。

通过 dumpsys 基本可以把问题缩小到具体某一层,比来回看日志高效太多。

下面是一个检查清单,直接照着过:

检查项期望值常见错误
POST_NOTIFICATIONS 权限已动态申请且授权只在 Manifest 声明,未 request
前台服务类型mediaPlayback未声明类型或漏传 startForeground 第三参
FOREGROUND_SERVICE_MEDIA_PLAYBACK声明targetSdk 34 未声明
MediaButtonReceiver exportedtrue误写 false
MediaStyle 通知setMediaSession 绑定用普通通知样式
MediaSession isActivetrue只在 onCreate 设置,未保持
PlaybackState statePLAYING / PAUSED一直 STATE_NONE
PlaybackState actions包含 PLAY/PAUSE 等未设置

3. 一个能直接跑通的最小实现:Service、MediaSession 与媒体通知配合

3.1 工程依赖与清单配置

排查完之后,我重新整理了一个最小可运行实现,关键是把 Service、MediaSession、媒体通知、MediaButtonReceiver 四个角色的关系理顺。

首先在依赖里加上 AndroidX Media:

implementation "androidx.media:media:1.7.0"

Manifest 里的完整配置:

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.WAKE_LOCK" /> <application ...> <service android:name=".MediaPlaybackService" android:foregroundServiceType="mediaPlayback" android:exported="false" /> <receiver android:name=".MediaButtonReceiver" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MEDIA_BUTTON" /> </intent-filter> </receiver> </application>

WAKE_LOCK不是必须的,但媒体播放场景通常需要持有唤醒锁防止播放时休眠,所以我一般一起加上。

3.2 Service 中 MediaSession 的初始化

Service 的核心任务是:创建 MediaSession,保持 active,设置正确的 PlaybackState,并且把返回的 session token 交给媒体通知。

class MediaPlaybackService : Service() { private lateinit var mediaSession: MediaSessionCompat private val notificationManager by lazy { getSystemService(NotificationManager::class.java) } override fun onCreate() { super.onCreate() mediaSession = MediaSessionCompat(this, "MediaPlaybackService").apply { setCallback(object : MediaSessionCompat.Callback() { override fun onPlay() { startPlayback() } override fun onPause() { pausePlayback() } override fun onSkipToNext() { skipToNext() } override fun onSkipToPrevious() { skipToPrevious() } }) isActive = true setPlaybackState( PlaybackStateCompat.Builder() .setActions( PlaybackStateCompat.ACTION_PLAY or PlaybackStateCompat.ACTION_PAUSE or PlaybackStateCompat.ACTION_PLAY_PAUSE or PlaybackStateCompat.ACTION_SKIP_TO_NEXT or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS ) .setState(PlaybackStateCompat.STATE_NONE, 0L, 1.0f) .build() ) } } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (intent?.action == Intent.ACTION_MEDIA_BUTTON) { MediaButtonReceiver.handleIntent(mediaSession, intent) } return START_STICKY } override fun onBind(intent: Intent?): IBinder? = null override fun onDestroy() { mediaSession.release() super.onDestroy() } }

注意onStartCommand里那句MediaButtonReceiver.handleIntent(mediaSession, intent),它负责把广播里的KeyEvent解析成对应的onPlayonPauseonSkipToNext等回调。这个调用不要漏,不然 MediaButtonReceiver 收到广播后,事件就断在中间了。

3.3 媒体通知的绑定细节

媒体通知不是普通通知,它必须满足两个条件:使用NotificationCompat.MediaStyle,并且通过setMediaSession()绑定 session token。

private fun buildMediaNotification(): Notification { val contentIntent = PendingIntent.getActivity( this, 0, Intent(this, MainActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) return NotificationCompat.Builder(this, CHANNEL_ID) .setSmallIcon(R.drawable.ic_notification) .setContentTitle("正在播放:测试歌曲") .setContentText("歌手信息") .setContentIntent(contentIntent) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setOnlyAlertOnce(true) .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE) .setStyle( androidx.media.app.NotificationCompat.MediaStyle() .setMediaSession(mediaSession.sessionToken) .setShowActionsInCompactView(0, 1, 2) ) .addAction(R.drawable.ic_prev, "上一首", mediaButtonPendingIntent(PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS)) .addAction(R.drawable.ic_play, "播放", mediaButtonPendingIntent(PlaybackStateCompat.ACTION_PLAY)) .addAction(R.drawable.ic_next, "下一首", mediaButtonPendingIntent(PlaybackStateCompat.ACTION_SKIP_TO_NEXT)) .build() }

setMediaSession(mediaSession.sessionToken)非常关键。少了这一行,通知就是一个"长得像媒体通知"的普通通知,系统不会把它和 MediaSession 关联起来。

3.4 处理 MediaButtonReceiver 的广播

MediaButtonReceiver自己不需要写太多逻辑,继承androidx.media.session.MediaButtonReceiver即可:

class MediaButtonReceiver : MediaButtonReceiver() { // 默认 onReceive 会负责把广播转为 startService 调用 // 如果 Service 需要额外处理,可以重写 onReceive,但必须调用 super }

它在收到广播后会启动我们声明的MediaPlaybackService,并把 intent 传进去。随后 Service 的onStartCommand调用MediaButtonReceiver.handleIntent(mediaSession, intent),把按键事件转成 Callback 回调。

这里有一个小细节:MediaButtonReceiver默认是通过ContextCompat.startForegroundService()启动服务的。也就是说,广播接收器里发生了前台服务启动。从 Android 14 开始,后台启动前台服务限制也更严了,但 "从媒体按钮广播启动媒体播放前台服务" 是系统明确允许的例外场景,所以这个链路上没有问题。

如果你在onCreate里没有调用setMediaButtonReceiver(),系统在某些情况下会走"直接调用 MediaSession.Callback"的路径,不经过广播。两条路径我都在代码里做了兼容,实测下来 Android 13 和 Android 14 都能稳定收到按键。

4. 实测中很难复现的四个隐蔽坑:从模拟器到多应用抢占

4.1 模拟器测试与真机差异

第一坑来自测试环境。Android 模拟器上,媒体按键的模拟方式非常有限。adb shell input keyevent KEYCODE_MEDIA_PLAY_PAUSE在部分模拟器上会被当成普通键盘事件,根本不会进入 MediaSession 的路由流程。哪怕按键事件到了系统,模拟器也没有真实的 A2DP 蓝牙协议栈,很多跟蓝牙耳机相关的按键状态变化在模拟器上根本不会发生。

所以如果你在模拟器上测试发现按键没反应,先别慌。换一台真机,连接真实的蓝牙耳机,或者用手机自带的耳机按键测试,结果往往完全不同。我在这个坑上浪费了小半天,最后发现代码逻辑没问题,纯粹是模拟器环境不支持媒体按键分发。

如果手头实在没有真机,可以用系统媒体控制中心(通知栏下拉后的媒体卡片)点击播放/暂停来验证 session 是否活跃,这比模拟器按键更接近真实场景。

4.2 通知被清理后媒体按键立刻失效

第二个坑和用户行为相关。媒体通知如果被用户滑掉,但 Service 还在运行、MediaSession 还 active,这时候按蓝牙耳机播放键,经常会出现:按键事件被系统接收到,但没有任何 App 响应。

原因在于,系统媒体控制中心主要依赖媒体通知来维持"当前活跃媒体会话"的认知。通知没了,系统可能还保留 session 在手势队列里,但不会再去主动分发媒体按键给它。

解决方案我在项目里改成了:媒体通知设置为不可滑动删除(setOngoing(true)),并且在onTaskRemoved、服务被销毁等场景下保证通知状态的一致性。如果你不希望用户无法清除通知,至少要在用户滑动通知时,通过PendingIntent把"停止播放并释放焦点"的逻辑执行完,否则就会出现"通知没了但播放还在后台"的中间状态,按键自然乱套。

顺带一提,很多用户清理最近任务列表时会把你的 App 进程一起杀掉。如果服务是START_STICKY,系统会尝试重建,但重建后的 MediaSession 是否还能拿到媒体按键的优先路由,取决于重建时机和通知是否恢复。建议在onTaskRemoved里面不要简单 stopSelf,而是根据当前是否在播放来决定是继续驻留还是优雅停止。

4.3 音频焦点:被忽略的隐形条件

第三个坑在代码之外。很多媒体 App 调用了setAudioAttributessetAudioFocusRequest,但有些项目从来不管音频焦点,觉得"只要 MediaSession active,系统就该把按键给我"。

API 33 之后不是这样。系统在决定媒体按键归属时,会把音频焦点状态作为一个重要参考。如果你的 App 在播放时没有持有音频焦点,系统会认为你并不是"真正在播放"的那一方,甚至可能把按键路由给其他正在持有焦点的媒体 App。

我的做法是:在onPlay()回调中请求音频焦点,在onPause()/onStop()中放弃焦点。Android 8.0(API 26)以上推荐用AudioFocusRequest

private val audioFocusRequest by lazy { AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) .setAudioAttributes( AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build() ) .setOnAudioFocusChangeListener { focusChange -> when (focusChange) { AudioManager.AUDIOFOCUS_LOSS -> pausePlayback() AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> pausePlayback() AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { // 降低音量而不暂停 } } } .build() }

只有拿到焦点,播放状态和系统媒体控制中心的状态才能保持一致。

4.4 多个媒体应用抢着认领按键

第四个坑和"竞争对手"有关。如果手机里装了多个媒体类 App,比如系统自带音乐、QQ 音乐、网易云,而你自己的 App 只是偶尔播放一下,那么系统更容易把耳机按键优先给"最近活跃过的媒体 App",而不是你的 App。

这种情况下,光把 MediaSession setActive 不够,还需要在每次播放前主动请求音频焦点,并确保 media notification 已经发布。因为系统媒体控制中心一般把"最近一次持有焦点且发布过媒体通知"的会话放在优先位置。

我还遇到过一种情况:用户用语音助手(长按耳机键)后,系统会把短按媒体键的处理权临时交给语音助手。这时候不是你的代码问题,而是系统层面的语音助手抢占。判断方法很简单:用dumpsys media_session看当时的 button receiver 指向谁,如果指向的是语音助手的包名,那就不是你能控制的场景。

5. 让 MediaSession 更稳的进阶配置:焦点、动作、适配 Android 14

5.1 PlaybackState 的 state 和 actions 必须跟着播放状态走

一个非常常见的问题:App 启动后立刻把 MediaSession 设为 active,但 PlaybackState 一直停在STATE_NONE,然后去按耳机键,系统不给任何响应。

原因是,系统媒体控制中心倾向于只把媒体按键分发给"当前有播放状态"的会话。STATE_NONE在系统看来等于"这个应用暂时没有在放东西"。所以正确的做法是:

  • 开始播放:setState(STATE_PLAYING, position, playbackSpeed)
  • 暂停:setState(STATE_PAUSED, position, 0f)
  • 还有STATE_STOPPEDSTATE_BUFFERING等按场景设置。

actions 也一样,不要只设置一次就忘记更新。比如播放中把ACTION_PAUSE放出来,暂停时把ACTION_PLAY放出来,这样锁屏控件和耳机按键的行为才会符合预期。

5.2 锁屏场景和耳机事件的处理策略

如果你的 App 常驻后台,并且需要支持锁屏媒体控制,可以额外做两件事:

  • onPlay()过程中尽早把前台服务拉起来,并发布媒体通知。锁屏控件依赖通知的可见性,setVisibility(NotificationCompat.VISIBILITY_PUBLIC)必须设置,否则锁屏上不显示。
  • 对于耳机线控,很多现代耳机发送的是KEYCODE_HEADSETHOOK,系统通常会把它转换成KEYCODE_MEDIA_PLAY_PAUSE。如果你的自定义耳机按键希望区分单击、双击,建议在onPlayPauseonSkipToNext上处理逻辑,而不是自己拦截 KeyEvent。因为系统在分发时可能已经做了动作合并。

Android 系统本身对媒体按键的 KeyEvent 分发偶尔会有抖动,我见过耳机按一次出现两次onPlayPause的情况。稳妥的做法是在播放状态切换前增加一个短时间去重(比如 300ms),避免明显的停顿和恢复抖动。这个在低端蓝牙耳机上尤其明显。

5.3 适配 Android 14 的强制前台服务类型

最后再说一次 Android 14 的适配,因为这是"API 33+"里最容易让应用崩溃的一环,而且和媒体按键看起来毫无关系,实际关系极大。

如果需要把 targetSdk 升到 34,请务必做这三件事:

  1. Manifest 里声明FOREGROUND_SERVICE_MEDIA_PLAYBACK权限。
  2. Service 的android:foregroundServiceType设为mediaPlayback
  3. 代码里startForeground()传入正确的类型枚举。

如果漏掉任何一项,前台服务启动会抛MissingForegroundServiceTypeExceptionSecurityException。服务起不来,MediaSession 自然不会 active,媒体按键就像石沉大海一样毫无反应。那种"崩溃日志在启动 Service 那一刻就出现,但我一开始根本没往崩溃上想"的经历,希望你不要再经历一遍。

另外,Android 14 对后台启动前台服务的限制进一步收紧。从后台点击通知启动 mediaPlayback 类型的服务是允许的,但普通startService()启动 mediaPlayback 服务则可能被拒绝。所以你的媒体按键链路里,MediaButtonReceiver转发广播这个环节不能省,它是系统认可的合法启动路径。

5.4 最后的调试姿势

整个排查过程中,对我帮助最大的两个命令,分享给你:

# 查看当前 MediaSession 状态、路由优先级、button receiver adb shell dumpsys media_session # 查看系统当前前台服务以及类型 adb shell dumpsys activity services

dumpsys media_session的输出里,重点看有没有你的包名、active=truestate=以及buttonReceiver=。当你在多个媒体应用之间来回切换时,这个输出能直观展示系统把按键优先给了谁。

我个人在实际操作中的体会是:MediaSession 在 API 33+ 上面临的问题,绝大多数都不是"MediaSession 本身"的问题,而是它和通知权限、前台服务、音频焦点、系统媒体控制中心之间的协作没有闭环。把这几个模块串起来看,问题定位会快很多。最后再说一个有点反直觉的小技巧:如果你的播放器在通知栏媒体卡片上点击正常,但耳机按键不响应,优先去看dumpsys media_session里的 button receiver 指向,很多时候是系统把按键路由给了上一次控制过媒体的其他 App,和你的代码没有半点关系。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 14:40:34

微信小程序校园自动点餐与跑腿系统开发实战:从需求到支付对接

大学食堂一到饭点就排长队&#xff0c;你想吃的档口永远挤满了人&#xff0c;外卖进不了校门&#xff0c;取个快递还得穿过整个生活区。这个需求憋到毕业设计或者接单的时候&#xff0c;就变成了我要说的这套"微信小程序校园自动点餐系统带跑腿"。它的定位很清晰&…

作者头像 李华
网站建设 2026/9/9 14:40:24

Agent智能体评估体系:从单元测试到四层Evals流水线

1. 这不是写测试用例&#xff0c;是给AI智能体装上“体检报告系统”你有没有遇到过这样的情况&#xff1a;花两周时间调通了一个购物比价Agent&#xff0c;它能自动爬商品、比价格、生成推荐理由&#xff0c;但上线第一天就因为某家电商页面结构微调而彻底卡死&#xff0c;报错…

作者头像 李华
网站建设 2026/9/9 14:39:08

WPF C#上位机Demo实战:MVVM、实时曲线与扫码枪处理

简介&#xff1a;《WPF专业编程指南》一书的配套C#演示代码集合&#xff0c;面向刚接触WPF或希望系统掌握桌面应用开发的新手开发者。资源共746个文件&#xff0c;压缩包约5.79MB&#xff0c;以228个cs源码文件和76个xaml界面文件为核心&#xff0c;同时包含38个sln解决方案、3…

作者头像 李华
网站建设 2026/9/9 14:39:06

Java毕设股票管理系统开发全攻略:选题、架构与答辩要点

每年到了毕设季&#xff0c;总有一批同学找我聊同一个题目&#xff1a;老师&#xff0c;用Java写股票管理系统行不行&#xff1f;行&#xff0c;当然行&#xff0c;但真正把它做成一个能过查重、能跑通演示、能扛住答辩老师追问的系统&#xff0c;跟拿个开源项目改个LOGO是两回…

作者头像 李华
网站建设 2026/9/9 14:38:09

Spring循环依赖深度解析:三级缓存原理与源码实战

循环依赖这个话题&#xff0c;在Spring面试里几乎是必问项&#xff0c;在真实项目里也经常踩坑。我见过不少同事&#xff0c;代码跑起来报了个BeanCurrentlyInCreationException&#xff0c;一脸懵地来问我“这啥意思”&#xff0c;然后我一看&#xff0c;好嘛&#xff0c;两个…

作者头像 李华
网站建设 2026/9/9 14:38:03

bRPC深度剖析:C++高性能RPC框架实战路径

bRPC深度剖析&#xff1a;C高性能RPC框架实战路径 【免费下载链接】brpc brpc is an Industrial-grade RPC framework using C Language, which is often used in high performance system such as Search, Storage, Machine learning, Advertisement, Recommendation etc. &qu…

作者头像 李华