EN

Android Service:开发自己的通知中心(2):辅助性服务实战

字数统计: 2.6k阅读时长: 11 min
2014/04/01
loading

2026-10-13 更新:本文记录的是 Android 4.3 之前通过无障碍事件观察通知的旧方案,这条通用通知中心实现路线已过时。Android 4.3(API 18)起提供专门的 NotificationListenerService,第 4 节补充 Android 7.0(API 24)及以上的最小监听示例。旧代码仅用于理解历史实现,不保证取得完整通知列表。

1.辅助性服务实战介绍

上一篇文章介绍了什么是 Accessibility 以及简单的使用,这一篇文章就来讲讲如何使用Accessibility服务来创建一个简单的Android通知中心。Android中通知中心是一个系统层面的服务,负责显示应用和系统发来的通知(Notification,比如USB插入、选择输入法、未接来电、截图、天气信息、新闻推送等等)。在android4.3之前,一般的第三方应用是无法获取Notification list的(在Android4.3之后,有了一个新的接口,NotificationListenerService.getActiveNotifications(),可以获取当前的Notification)。但是利用Accessibility服务可以监听到各种事件的特性,可以开发一个第三方的通知中心,实现与系统通知栏类似的功能。

下面保留原有无障碍方案的实现过程。示例中的布局资源由原工程提供;它不是完整 Android 工程。新的通知中心应使用第 4 节的专用接口。

2.开发第三方通知中心

2.1 继承 AccessibilityService

按照上一篇的做法,在应用中创建继承 AccessibilityService 的服务。下面先检查事件类型和数据类型,再发送给本应用的界面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.app.Notification;
import android.content.Intent;
import android.os.Parcelable;
import android.view.accessibility.AccessibilityEvent;

public class NotificationFetcherService extends AccessibilityService {


@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
if (event.getEventType() != AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED) {
return;
}

Parcelable data = event.getParcelableData();
if (!(data instanceof Notification)) {
return;
}
Notification localNotification = (Notification) data;

Intent intent = new Intent(getPackageName() + ".NOTIFICATION_EVENT");
intent.putExtra("NotifyData", localNotification);
intent.setPackage(getPackageName());
sendBroadcast(intent, getPackageName() + ".permission.INTERNAL_NOTIFICATION");

}

@Override
protected void onServiceConnected() {

// Historical event-based notification observation. Only request the required event.
AccessibilityServiceInfo info = new AccessibilityServiceInfo();
info.eventTypes = AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED;

info.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC;
setServiceInfo(info);
}

@Override
public void onInterrupt() {
// Stop accessibility feedback if the service provides any.
}

}

这几个回调的含义分别是:

  1. onServiceConnected() 在系统连接服务后执行,不是 Activity 与服务绑定的回调。这里只有通知事件处理,因此只订阅 TYPE_NOTIFICATION_STATE_CHANGED。
  2. onInterrupt() 要求中断无障碍反馈,不是服务断开通知。
  3. onAccessibilityEvent() 接收订阅范围内的事件。getParcelableData() 可能不是 Notification,所以要先检查类型。AccessibilityEvent 文档

拿到 Notification 之后,我这里是通过广播把它交给 Activity 处理。下面的代码补了应用包名和签名权限:包名限制广播去哪里,接收端的签名权限限制谁可以发来数据。

这里还碰到过一个问题:截图、未接来电等通知对象比较大,通过广播传递时会超出大小限制。完整 Notification 里的 RemoteViews、图片和 extras 都可能导致 TransactionTooLargeException。原文给的办法是把 contentView 暂存在 Application,但它解决不了跨进程、进程重启和其他大字段的问题。现在做这类功能,应在应用自己的数据层保留必要字段,让界面读取快照,避免反复广播整个通知对象。

2.2 在 Manifest 中注册 service

下面的声明假定 applicationId 为 com.example.notificationcenter;使用其他包名时,要同步修改自定义权限前缀。权限写在 <manifest> 中,服务写在 <application> 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- Direct children of <manifest>. -->
<permission
android:name="com.example.notificationcenter.permission.INTERNAL_NOTIFICATION"
android:protectionLevel="signature" />
<uses-permission
android:name="com.example.notificationcenter.permission.INTERNAL_NOTIFICATION" />

<!-- Inside <application>. -->
<service
android:name=".NotificationFetcherService"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>

<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibilityserviceconfig" />
</service>

BIND_ACCESSIBILITY_SERVICE 限制由系统绑定服务;meta-data 指向 res/xml/accessibilityserviceconfig.xml。该配置从 Android 4.0(API 14)起可用:

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeNotificationStateChanged"
android:accessibilityFeedbackType="feedbackGeneric"
/>

服务这里就配置好了。

2.3 接收并处理 Notification

下面的 Activity 节选读取收到的通知并尝试显示旧式自定义 RemoteViews。activity_main 需提供 TextView notify_test_textview 和 LinearLayout root_layout;示例使用 compileSdk 33 或以上,并在 Manifest 中声明该 Activity。

接收器在 onStart() 注册、onStop() 解除注册,因此 Activity 停止后不再接收通知。收发均限定为同签名应用;Android 13(API 33)及以上再加 RECEIVER_NOT_EXPORTED。这是动态广播接收器的生命周期处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import android.app.Activity;
import android.app.Notification;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.RemoteViews;
import android.widget.TextView;

public class NotificationActivity extends Activity {
private TextView textView;
private LinearLayout rootLayout;

private final BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Parcelable value = intent.getParcelableExtra("NotifyData");
if (!(value instanceof Notification)) return;
Notification notification = (Notification) value;
textView.setText(notification.tickerText == null ? "" : notification.tickerText);
rootLayout.removeAllViews();
RemoteViews remoteViews = notification.contentView;
if (remoteViews == null) return;
try {
View content = remoteViews.apply(NotificationActivity.this, rootLayout);
rootLayout.addView(content);
} catch (RuntimeException error) {
// A remote layout may no longer be available or compatible.
textView.setText("Notification layout unavailable");
}
}
};

@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.notify_test_textview);
rootLayout = findViewById(R.id.root_layout);
}

@Override
protected void onStart() {
super.onStart();
IntentFilter filter = new IntentFilter(getPackageName() + ".NOTIFICATION_EVENT");
String permission = getPackageName() + ".permission.INTERNAL_NOTIFICATION";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(receiver, filter, permission, null, Context.RECEIVER_NOT_EXPORTED);
} else {
registerReceiver(receiver, filter, permission, null);
}
}

@Override
protected void onStop() {
unregisterReceiver(receiver);
super.onStop();
}
}

用户需要在「设置 → 无障碍」中启用服务,Manifest 声明不会自动授予访问权。

RemoteViews 是远程布局描述,不能直接传给 addView();需要先调用 apply() 得到 View。Notification.contentView 在 API 24 已废弃,现代标准模板通知也可能没有这个字段,因此它不能用于还原所有通知。上述代码只保留了原文的展示方式及失败处理。

要模拟完整的通知中心,还是要费一番功夫的:更新、移除、排序、用户交互和进程重启都得处理。这里因为公司项目保密,没有贴出完整实现,有兴趣的同学可以私下和我交流。

3.总结和问题

AccessibilityService的实战就讲到这里,这一篇博文也是拖了一段时间才写完的,也算是为前一段时间的项目做个了结。

原项目当时遇到的限制包括:

  1. 无障碍事件不能提供完整的当前通知列表,更不能还原已被清除的历史通知。
  2. 当时的数据只保存在内存里,服务被杀掉后就丢了。我试过用 db4o 这类对象数据库保存整个通知对象,没走通。需要持久化的话,可以把文本、包名、时间等必要字段存进自己的数据库;整个 Notification/RemoteViews 不适合作为稳定的持久化格式。
  3. 并非所有系统变化都会产生可见的通知事件,例如 USB 状态应使用对应系统 API 观察,不能靠通知内容完整推断。

4. Android 4.3 以后的通知监听方案

NotificationListenerService 从 API 18 开始提供通知发布、移除回调和活动通知查询。下面同时使用 API 21 加入的 onListenerConnected() 和 API 24 加入的 onListenerDisconnected(),因此示例的最低运行版本为 Android 7.0。API 24 起这些回调统一在主线程执行。

在 <application> 中声明专用服务;此方案不需要前面的无障碍服务或内部广播:

1
2
3
4
5
6
7
8
<service
android:name=".NotificationListener"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
android:exported="false">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>

用户还要在系统设置中授予「通知使用权」。它与无障碍授权、Android 13 的发通知权限 POST_NOTIFICATIONS 是不同权限。

下面的监听器只保留通知 key,用于展示连接时同步、更新和移除的流程,不读取或输出通知正文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.util.Log;
import java.util.HashSet;
import java.util.Set;

public class NotificationListener extends NotificationListenerService {
private final Set<String> activeKeys = new HashSet<>();
private boolean snapshotAvailable;

@Override
public void onListenerConnected() {
snapshotAvailable = false;
activeKeys.clear();
StatusBarNotification[] active = getActiveNotifications();
if (active == null) {
Log.d("NotificationListener", "Notification state unavailable");
return;
}
for (StatusBarNotification item : active) {
activeKeys.add(item.getKey());
}
snapshotAvailable = true;
reportCount();
}

@Override
public void onNotificationPosted(StatusBarNotification item) {
if (!snapshotAvailable || item == null) return;
activeKeys.add(item.getKey());
reportCount();
}

@Override
public void onNotificationRemoved(StatusBarNotification item) {
if (!snapshotAvailable || item == null) return;
activeKeys.remove(item.getKey());
reportCount();
}

@Override
public void onListenerDisconnected() {
snapshotAvailable = false;
activeKeys.clear();
Log.d("NotificationListener", "Disconnected; notification state unavailable");
}

private void reportCount() {
Log.d("NotificationListener", "Active notifications: " + activeKeys.size());
}
}

连接成功后再调用 getActiveNotifications(),可以同步当前仍存在且监听器有权看到的通知;它不提供已经清除的通知历史。断开连接后的本地空集合表示状态不可用,不能当成「设备没有通知」。真实界面应从应用数据层观察有连接状态的快照,并把必要的持久化工作移出回调。

Android 15(API 35)起,系统会对不受信任的通知监听器隐藏检测到的一次性验证码等敏感内容。因此,通知使用权也不意味着可以读到所有通知字段。Android 15 通知内容保护

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球:付费社群已于 2026 年停止运营,不再招收新成员;此处保留历史介绍。

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

CATALOG
  1. 1. 1.辅助性服务实战介绍
  2. 2. 2.开发第三方通知中心
    1. 2.1. 2.1 继承 AccessibilityService
    2. 2.2. 2.2 在 Manifest 中注册 service
    3. 2.3. 2.3 接收并处理 Notification
  3. 3. 3.总结和问题
  4. 4. 4. Android 4.3 以后的通知监听方案
  • 关于我 && 博客