CN

Android Service: Building Your Own Notification Center (2) - Accessibility Se...

Word count: 1.8kReading time: 11 min
2014/04/01
loading

Updated 2026-10-13: This article records the historical approach of observing notifications through accessibility events before Android 4.3. That approach is outdated for a general notification center. Android 4.3 (API 18) introduced NotificationListenerService; section 4 adds a minimal listener for Android 7.0 (API 24) and later. The legacy examples explain the earlier implementation and do not provide a complete notification list.

1. Accessibility Service in Practice

The previous article introduced accessibility services and their basic configuration. This article follows the original implementation of a simple notification center. Android’s system notification UI displays notifications from apps and system components, such as missed calls, screenshots, weather, and news updates. Before Android 4.3, ordinary third-party apps did not have a public API to query the active notification list. Accessibility events offered a limited way to observe notifications; Android 4.3 added the dedicated NotificationListenerService.getActiveNotifications() API.

The following sections preserve the accessibility-based implementation. Layout resources came from the original project, so these excerpts are not a complete Android project. New notification centers should use the dedicated API in section 4.

2. Building a Third-Party Notification Center

2.1 Extending AccessibilityService

Following the previous article, create a service extending AccessibilityService. Check the event and payload types before forwarding the notification to this application’s interface:

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.
}

}

The callbacks have distinct purposes:

  1. onServiceConnected() runs when the system connects the service, not when an Activity binds to it. This implementation handles only notifications, so it subscribes only to TYPE_NOTIFICATION_STATE_CHANGED.
  2. onInterrupt() asks the service to interrupt accessibility feedback; it is not a disconnection callback.
  3. onAccessibilityEvent() receives matching events. Check the type of getParcelableData() before casting it to Notification. See the AccessibilityEvent reference.

Once I had the Notification, I sent it to the Activity through a broadcast. The updated example restricts delivery to the app’s package and uses a signature permission on both sender and receiver. Package targeting limits where the broadcast goes; the receiver’s permission limits who can send data to it.

I also ran into the broadcast payload-size limit with large notifications, including screenshots and missed calls. A full Notification can contain large RemoteViews, images, or extras and trigger TransactionTooLargeException. The original post suggested keeping contentView temporarily in Application, but that doesn’t handle cross-process access, process restarts, or other large fields. For a current implementation, keep the fields you need in the app’s data layer and let the UI read snapshots instead of repeatedly broadcasting whole notification objects.

2.2 Registering the Service in Manifest

This example assumes an applicationId of com.example.notificationcenter. Change the custom permission prefix if using another package. Permission declarations belong under <manifest>, while the service belongs under <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 restricts service binding to the system. The metadata refers to res/xml/accessibilityserviceconfig.xml; XML service configuration is available from 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"
/>

This selects the notification events handled by the service.

2.3 Receiving and Displaying Notifications

The Activity excerpt below reads the notification and attempts to display its legacy custom RemoteViews. The activity_main layout must contain a TextView named notify_test_textview and a LinearLayout named root_layout. Compile against SDK 33 or higher and declare this Activity in the manifest.

The receiver is registered in onStart() and unregistered in onStop(), so it stops receiving notifications when the Activity stops. A signature permission protects both ends. Android 13 (API 33) and later also use RECEIVER_NOT_EXPORTED. See the context-registered receiver lifecycle.

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();
}
}

The user must enable the service in Settings → Accessibility. The manifest declaration alone does not grant access.

RemoteViews is a description of a remote layout, not a View that can be passed directly to addView(). Call apply() to obtain a View first. Notification.contentView was deprecated in API 24 and may be absent for modern template notifications, so this cannot reproduce every notification. The excerpt preserves the original display approach with failure handling.

A complete notification center still takes some work: updates, removals, ordering, user interaction, and process restarts all need handling. I couldn’t share the full implementation because it was a company project. If you’re interested, feel free to get in touch privately.

3. Summary and Limitations

This post took me a while to finish. It also gave me a chance to wrap up the work from that project. These were the problems left in the implementation at the time:

  1. Accessibility events do not supply the complete current notification list or reconstruct notifications that have already been dismissed.
  2. I kept the data only in memory, so it was lost when the service was killed. I tried saving whole notification objects with an object database, db4o, but couldn’t get it working. An app can persist the text, package names, timestamps, and other fields it needs in its own database; whole Notification or RemoteViews objects aren’t a stable persistence format.
  3. Not every system change produces an observable notification event. USB state, for example, should be observed through the relevant system API rather than inferred entirely from notifications.

4. Notification Listening After Android 4.3

NotificationListenerService has supplied posted/removed callbacks and active-notification queries since API 18. The following example uses both onListenerConnected() from API 21 and onListenerDisconnected() from API 24, so its minimum runtime version is Android 7.0. From API 24, these callbacks run on the main thread.

Declare the dedicated service inside <application>. This approach does not need the accessibility service or internal broadcast shown earlier:

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>

The user must also grant notification access in system settings. This is separate from accessibility authorization and Android 13’s POST_NOTIFICATIONS permission to post notifications.

This listener retains only notification keys to demonstrate synchronization on connection, updates, and removals. It does not read or log notification bodies:

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());
}
}

Call getActiveNotifications() only after connection. It synchronizes notifications that are still active and visible to this listener, not a history of dismissed notifications. After disconnection, an empty local set represents unavailable state, not proof that the device has no notifications. A real UI should observe data-layer snapshots that include connection status and move persistence work out of callbacks.

From Android 15 (API 35), untrusted notification listeners cannot read unredacted content from notifications in which an OTP is detected. Notification access therefore does not guarantee access to every field. See Android 15 notification-content protections.

About Me && Blog

Below is my personal intro and related links. I look forward to exchanging ideas with fellow professionals. “When three walk together, one can always be my teacher!”

  1. Blogger Intro
  2. Blog Content Navigation: A guide for my blog content.
  3. Curated Excellent Blog Articles - Android Performance Optimization Must-Knows
  4. Android Performance Optimization Knowledge Planet: The paid community stopped operating in 2026 and no longer accepts new members; this link preserves its history.

One walks faster alone, but a group walks further together.

Scan WeChat QR Code

CATALOG
  1. 1. 1. Accessibility Service in Practice
  2. 2. 2. Building a Third-Party Notification Center
    1. 2.1. 2.1 Extending AccessibilityService
    2. 2.2. 2.2 Registering the Service in Manifest
    3. 2.3. 2.3 Receiving and Displaying Notifications
  3. 3. 3. Summary and Limitations
  4. 4. 4. Notification Listening After Android 4.3
  • About Me && Blog