完整教程:Android监听第三方播放获取音乐信息及包名

news/2025/11/9 17:09:29/文章来源:https://www.cnblogs.com/yangykaifa/p/19204501

文章目录

  • NotificationListenerService
  • 代码获取播放音乐
    • 步骤 1:声明权限和服务
    • 步骤 2:实现NotificationListenerService
    • 步骤 3:请求用户启用通知监听权限
    • 启动服务
    • 注意事项
    • 获取播放信息如下图:

NotificationListenerService

NotificationListenerService 是 Android 系统提供的一个特殊服务,允许应用监听和获取其他应用发出的通知。通过这个服务,开发者可以读取通知的内容、发送时间、来源应用等信息,还可以对通知进行操作(如删除通知)。
核心功能。

  • 监听通知:获取系统中所有应用发出的通知。
  • 读取通知内容:包括标题、文本、图标等。
  • 通知操作:删除通知或执行通知中的操作。

在 Android 中使用 NotificationListenerService 监听媒体播放器状态是可行的,因为大多数媒体应用(如音乐播放器、视频播放器)会在发出的通知中包含当前播放状态(播放 / 暂停)、曲目信息(标题、艺术家、专辑)等内容。下面介绍如何实现这一功能。

注意:有一些应用NotificationListenerService是监听不到播放状态的,比如其不发通知,或者是在后台播放情况下没有通知,这种可以考虑不使用NotificationListenerService实现,而是用一个定时器去轮询MediaSessionManager的getActiveSessions()函数获取音乐信息,这里不贴代码了。

代码获取播放音乐

步骤 1:声明权限和服务

在AndroidManifest.xml中添加以下权限和服务:

<uses-permission android:name="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE" />
<serviceandroid:name=".MediaSessionListenerService"android:label="Notification Listener"android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter><action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>

步骤 2:实现NotificationListenerService

创建MediaSessionListenerService类继承NotificationListenerService:

import android.app.Notification;
import android.media.session.MediaController;
import android.media.MediaMetadata;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.os.Bundle;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.util.Log;
public class MediaSessionListenerService extends NotificationListenerService {
private static final String TAG = "MusicProgressListener";
private MediaController mMediaController;
private String currentPlayingPackage;
public void onMusicStateChanged(PlaybackState state) {
if (state != null) {
long currentPosition = state.getPosition();
Log.d(TAG, "[" + currentPlayingPackage + "] 当前进度: " + currentPosition + " ms");
}
}
public void onMusicMetadataChanged(MediaMetadata metadata) {
if (metadata != null) {
long duration = metadata.getLong(MediaMetadata.METADATA_KEY_DURATION);
String title = metadata.getString(MediaMetadata.METADATA_KEY_TITLE);
String artist = metadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
Log.d(TAG, "[" + currentPlayingPackage + "] 歌曲: " + title + " - " + artist);
Log.d(TAG, "[" + currentPlayingPackage + "] 总时长: " + duration + " ms");
}
}
private final MediaController.Callback mControllerCallback = new MediaController.Callback() {
@Override
public void onPlaybackStateChanged(PlaybackState state) {
onMusicStateChanged(state);
}
@Override
public void onMetadataChanged(MediaMetadata metadata) {
onMusicMetadataChanged(metadata);
}
};
@Override
public void onListenerConnected() {
super.onListenerConnected();
Log.d(TAG, "通知监听服务已连接");
// 服务连接后,立即检查当前是否有活跃的媒体会话
checkActiveMediaSessions();
}
@Override
public void onNotificationPosted(StatusBarNotification sbn) {
super.onNotificationPosted(sbn);
// 当有新通知发出时,检查是否是媒体通知
handleMediaNotification(sbn);
}
@Override
public void onNotificationRemoved(StatusBarNotification sbn) {
super.onNotificationRemoved(sbn);
// 当媒体通知被移除时(如音乐停止),清理资源
if (isMediaNotification(sbn)) {
// 确保只清理当前正在播放的会话
if (mMediaController != null && sbn.getPackageName().equals(currentPlayingPackage)) {
mMediaController.unregisterCallback(mControllerCallback);
mMediaController = null;
currentPlayingPackage = null; // 重置包名
Log.d(TAG, "媒体会话已断开: " + sbn.getPackageName());
}
}
}
private void checkActiveMediaSessions() {
StatusBarNotification[] activeMediaSessions = getActiveNotifications();
if (activeMediaSessions != null && activeMediaSessions.length > 0) {
// 通常我们只关心第一个活跃的媒体会话
handleMediaNotification(activeMediaSessions[0]);
}
}
private void handleMediaNotification(StatusBarNotification sbn) {
if (!isMediaNotification(sbn)) {
return;
}
// 如果已经是当前正在处理的包,无需重复操作
if (sbn.getPackageName().equals(currentPlayingPackage)) {
return;
}
try {
Bundle extras = sbn.getNotification().extras;
MediaSession.Token token = extras.getParcelable(Notification.EXTRA_MEDIA_SESSION);
if (token != null) {
// 如果已经有一个控制器,先注销它
if (mMediaController != null) {
mMediaController.unregisterCallback(mControllerCallback);
}
// 创建新的 MediaController
mMediaController = new MediaController(this, token);
mMediaController.registerCallback(mControllerCallback);
// 从 StatusBarNotification 获取包名并存储
currentPlayingPackage = sbn.getPackageName();
Log.d(TAG, "已连接到媒体会话: " + currentPlayingPackage);
// 立即获取一次当前状态,防止错过回调
PlaybackState state = mMediaController.getPlaybackState();
MediaMetadata metadata = mMediaController.getMetadata();
if (state != null) {
onMusicStateChanged(state);
}
if (metadata != null) {
onMusicMetadataChanged(metadata);
}
}
} catch (Exception e) {
Log.e(TAG, "处理媒体通知时出错", e);
}
}
private boolean isMediaNotification(StatusBarNotification sbn) {
// 判断一个通知是否为媒体通知
Notification notification = sbn.getNotification();
return notification != null && notification.extras != null
&& notification.extras.containsKey(Notification.EXTRA_MEDIA_SESSION);
}
}

步骤 3:请求用户启用通知监听权限

在Activity中检查并请求权限,系统不会自动授予此权限。你需要引导用户去设置页面手动开启:
MainActivity.java

private void checkNotificationListenerPermission() {
if (!isNotificationServiceEnabled()) {
startActivity(new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"));
}
}
private boolean isNotificationServiceEnabled() {
String packageName = getPackageName();
String flat = Settings.Secure.getString(getContentResolver(), "enabled_notification_listeners");
if (flat != null) {
String[] names = flat.split(":");
for (String name : names) {
ComponentName cn = ComponentName.unflattenFromString(name);
if (cn != null && cn.getPackageName().equals(packageName)) {
return true;
}
}
}
return false;
}

然后MainActivity.java中调用

checkNotificationListenerPermission();

用户必须点击进入设置,并手动开启你的 App 的“通知监听”开关。
在这里插入图片描述

启动服务

通常不需要显式启动 NotificationListenerService,只要用户开启了权限,系统就会自动绑定并运行服务。
但你可以调用以下代码来“尝试”触发绑定(实际是否运行仍由系统控制),

// 可以尝试启动服务(非必需)
Intent intent = new Intent(this, MediaSessionListenerService.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent);
} else {
startService(intent);
}

运行正常的话onListenerConnected()会被自动调用

@Override
public void onListenerConnected() {
super.onListenerConnected();
Log.d(TAG, "通知监听服务已连接"); // 这行会被执行
checkActiveMediaSessions();
}

你可以在 Logcat 中搜索 “通知监听服务已连接” 来确认是否成功触发。

注意事项

  1. 用户授权:必须引导用户到设置中启用应用的通知监听权限。
  2. 媒体会话限制:并非所有应用都使用MediaSession,因此可能无法检测到所有播放情况。
  3. 多应用播放:可能存在多个应用同时播放的情况,需根据业务逻辑处理。

如果报以下错误,
java.lang.SecurityException: Missing permission to control media.
需要加上这个权限

获取播放信息如下图:

在这里插入图片描述
注意上面有总时长为-1的,这个是不对的需要特殊处理。
作者:帅得不敢出门

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/960579.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

git的各种HEAD以及使用示例

gitrevisions - git docs git-rev-parse - git docs中文 HEAD 命名工作区中的更改所基于的提交。 这个很常用了,HEAD为当前分支最新提交,经常用HEAD^、HEAD^^^、HEAD~n来定位之前的提交。 # 清除工作区和暂存区的所有…

OneDrive上传和下载速度慢?有什么解决办法吗? - 指南

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

win10安装广东省正版化检查工具

win10安装广东省正版化检查工具这个工具如果用管理员账户进行默认安装,启动后加载是不会弹出用户账户控制的,但如果切换到标准用户,启动则会弹出用户账户控制,需要管理员授权才能启动。 经过尝试,发现除了要禁用系…

详细介绍:深入浅出MATLAB数据可视化:超越plot()

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

【JEECG 组件扩展】JSwitch开关组件扩展单个多选框样式 - 详解

【JEECG 组件扩展】JSwitch开关组件扩展单个多选框样式 - 详解pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Con…

既然道可道相当道,那么传道授业解惑的根基是什么?

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

P10592 BZOJ4361 isn

遇到这种题还是太吃操作了。 首先看如果没有必须为非降序列的限制怎么办,那么就是求出一种长度种类的方案,然后删除的时候剩下的随便删即可。 然后考虑容斥,每次减去上一次操作不合法的位置即可,还是比较套路的。

阿道夫

阿道夫报告名称:[二刻BU--G0-P2工艺] 业务需求与数据目标报告 版本:V1.0 日期:[2025-11-07] 编制人:[易志伟/数字智能部] 审核人:[张迪/数字智能部]引言 1.1 背景与业务场景 • 工艺类型:AR(Active Area Revers…

软件开发公司常犯的5个设计误区,看看你中招了吗?

软件开发公司常犯的5个设计误区,看看你中招了吗?在软件开发行业,“功能实现” 往往被视为核心目标,而 “设计” 常被当作 “锦上添花” 的环节 —— 要么凭经验拍脑袋设计,要么照搬竞品框架,要么忽视用户真实需求…

使用jmeter做压力测试 - 实践

使用jmeter做压力测试 - 实践pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco"…

CSP2025游记总结

J组 好难!2.5个小时一道题都没做出来。 --前言 赛时 8:30~9:30 开 T1,原本想着还要分讨,但一看数据范围,秒了。 T2 也是分讨。应该是最没把握的,不仅很难对拍,而且大样例又水。 T3 一眼 dp,稍微优化一下,就过大…

连续出现的字符

点击查看代码 #include<iostream> #include<string> using namespace std; string s; int main() {int n;cin >> n; cin.ignore();getline(cin, s);int len = s.length();int cnt = 1;for (int i = …

详解WebSocket及其妙用 - 指南

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

2025 csp_j 游忌

1.number 赛事 AC 思路 此题比较简单, AC 思路比较多,我的思路如下 因为他让我们求字符串 \(s\) 中数字能组成的最大数字是多少,我们用一个 \(mp[x]\) 来存数字 \(x\) 在 \(s\) 中出现的次数,而能组成的最大数就是…

利用序列ID漏洞下载整个公司用户数据库的技术分析

本文详细描述了作者如何通过发现序列ID漏洞,成功获取某公司完整用户数据库的技术过程。文章涉及API安全测试、会话管理漏洞等实际渗透测试技术,展示了从基础侦察到完整数据泄露的完整攻击链。如何利用序列ID漏洞下载…

详细介绍:STM32 定时中断逻辑拆解:为什么 “每 2 次中断翻一次 LED”,却是 1 秒亮 1 秒灭?

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

11.8 NOIP模拟4 改题记录

该文被密码保护。HZOJ 写在前面 突袭放假日。为啥我们不能跟高一的一起放。为啥我们假期比高一少一坤时。进入NOIP模拟套题阶段后好像没咋写过改题记录了。一个是干其他事去了,另一个是如果每场都只改一两个写着好像也…

TCP和

计算机网络中的三种通信方式单工单工通信(Simplex Communication)是一种最简单的数据传输方式,数据只能在一个方向上传输。在这种模式下,一个设备只能作为发送方,另一个设备只能作为接收方,数据传输是单向的。在…

2025-11-08 NOIP 模拟赛4 赛后总结

Record8:06 会了 T1。特殊性质立大功。 8:22 过掉 T1 大洋里。开 T2。 8:30 没有任何思路。 9:16 思考 T2 思考了一个小时但还是没有任何头绪。放弃 T2。听说 T3 比 T2 可做。 10:23 写完 T3 了。直接过掉大洋里。 11:…

C 指针初识

这是一个变量的声明及初始化语句:int a=10;在程序员眼中,会关注:变量a的类型和变量a的值;而在系统计算机眼中,可能是这样的:在地址0x1234开始占用了四个字节的内存,这个内存上需要写入一个整型值10; 对,我们在…