NotificationListenerService.java revision f953664dc17dca23bd724bd64f89189c16c83263
1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.service.notification;
18
19import android.annotation.SdkConstant;
20import android.app.INotificationManager;
21import android.app.Service;
22import android.content.Context;
23import android.content.Intent;
24import android.os.IBinder;
25import android.os.RemoteException;
26import android.os.ServiceManager;
27import android.util.Log;
28
29import java.util.Comparator;
30import java.util.HashMap;
31
32/**
33 * A service that receives calls from the system when new notifications are posted or removed.
34 * <p>To extend this class, you must declare the service in your manifest file with
35 * the {@link android.Manifest.permission#BIND_NOTIFICATION_LISTENER_SERVICE} permission
36 * and include an intent filter with the {@link #SERVICE_INTERFACE} action. For example:</p>
37 * <pre>
38 * &lt;service android:name=".NotificationListener"
39 *          android:label="&#64;string/service_name"
40 *          android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
41 *     &lt;intent-filter>
42 *         &lt;action android:name="android.service.notification.NotificationListenerService" />
43 *     &lt;/intent-filter>
44 * &lt;/service></pre>
45 */
46public abstract class NotificationListenerService extends Service {
47    // TAG = "NotificationListenerService[MySubclass]"
48    private final String TAG = NotificationListenerService.class.getSimpleName()
49            + "[" + getClass().getSimpleName() + "]";
50
51    private INotificationListenerWrapper mWrapper = null;
52    private String[] mNotificationKeys;
53
54    private INotificationManager mNoMan;
55
56    /**
57     * The {@link Intent} that must be declared as handled by the service.
58     */
59    @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
60    public static final String SERVICE_INTERFACE
61            = "android.service.notification.NotificationListenerService";
62
63    /**
64     * Implement this method to learn about new notifications as they are posted by apps.
65     *
66     * @param sbn A data structure encapsulating the original {@link android.app.Notification}
67     *            object as well as its identifying information (tag and id) and source
68     *            (package name).
69     */
70    public abstract void onNotificationPosted(StatusBarNotification sbn);
71
72    /**
73     * Implement this method to learn when notifications are removed.
74     * <P>
75     * This might occur because the user has dismissed the notification using system UI (or another
76     * notification listener) or because the app has withdrawn the notification.
77     * <P>
78     * NOTE: The {@link StatusBarNotification} object you receive will be "light"; that is, the
79     * result from {@link StatusBarNotification#getNotification} may be missing some heavyweight
80     * fields such as {@link android.app.Notification#contentView} and
81     * {@link android.app.Notification#largeIcon}. However, all other fields on
82     * {@link StatusBarNotification}, sufficient to match this call with a prior call to
83     * {@link #onNotificationPosted(StatusBarNotification)}, will be intact.
84     *
85     * @param sbn A data structure encapsulating at least the original information (tag and id)
86     *            and source (package name) used to post the {@link android.app.Notification} that
87     *            was just removed.
88     */
89    public abstract void onNotificationRemoved(StatusBarNotification sbn);
90
91    /**
92     * Implement this method to learn about when the listener is enabled and connected to
93     * the notification manager.  You are safe to call {@link #getActiveNotifications(String[])
94     * at this time.
95     *
96     * @param notificationKeys The notification keys for all currently posted notifications.
97     */
98    public void onListenerConnected(String[] notificationKeys) {
99        // optional
100    }
101
102    /**
103     * Implement this method to be notified when the notification order cahnges.
104     *
105     * Call {@link #getOrderedNotificationKeys()} to retrieve the new order.
106     */
107    public void onNotificationOrderUpdate() {
108        // optional
109    }
110
111    private final INotificationManager getNotificationInterface() {
112        if (mNoMan == null) {
113            mNoMan = INotificationManager.Stub.asInterface(
114                    ServiceManager.getService(Context.NOTIFICATION_SERVICE));
115        }
116        return mNoMan;
117    }
118
119    /**
120     * Inform the notification manager about dismissal of a single notification.
121     * <p>
122     * Use this if your listener has a user interface that allows the user to dismiss individual
123     * notifications, similar to the behavior of Android's status bar and notification panel.
124     * It should be called after the user dismisses a single notification using your UI;
125     * upon being informed, the notification manager will actually remove the notification
126     * and you will get an {@link #onNotificationRemoved(StatusBarNotification)} callback.
127     * <P>
128     * <b>Note:</b> If your listener allows the user to fire a notification's
129     * {@link android.app.Notification#contentIntent} by tapping/clicking/etc., you should call
130     * this method at that time <i>if</i> the Notification in question has the
131     * {@link android.app.Notification#FLAG_AUTO_CANCEL} flag set.
132     *
133     * @param pkg Package of the notifying app.
134     * @param tag Tag of the notification as specified by the notifying app in
135     *     {@link android.app.NotificationManager#notify(String, int, android.app.Notification)}.
136     * @param id  ID of the notification as specified by the notifying app in
137     *     {@link android.app.NotificationManager#notify(String, int, android.app.Notification)}.
138     * <p>
139     * @deprecated Use {@link #cancelNotification(String key)}
140     * instead. Beginning with {@link android.os.Build.VERSION_CODES#L} this method will no longer
141     * cancel the notification. It will continue to cancel the notification for applications
142     * whose {@code targetSdkVersion} is earlier than {@link android.os.Build.VERSION_CODES#L}.
143     */
144    public final void cancelNotification(String pkg, String tag, int id) {
145        if (!isBound()) return;
146        try {
147            getNotificationInterface().cancelNotificationFromListener(
148                    mWrapper, pkg, tag, id);
149        } catch (android.os.RemoteException ex) {
150            Log.v(TAG, "Unable to contact notification manager", ex);
151        }
152    }
153
154    /**
155     * Inform the notification manager about dismissal of a single notification.
156     * <p>
157     * Use this if your listener has a user interface that allows the user to dismiss individual
158     * notifications, similar to the behavior of Android's status bar and notification panel.
159     * It should be called after the user dismisses a single notification using your UI;
160     * upon being informed, the notification manager will actually remove the notification
161     * and you will get an {@link #onNotificationRemoved(StatusBarNotification)} callback.
162     * <P>
163     * <b>Note:</b> If your listener allows the user to fire a notification's
164     * {@link android.app.Notification#contentIntent} by tapping/clicking/etc., you should call
165     * this method at that time <i>if</i> the Notification in question has the
166     * {@link android.app.Notification#FLAG_AUTO_CANCEL} flag set.
167     * <p>
168     * @param key Notification to dismiss from {@link StatusBarNotification#getKey()}.
169     */
170    public final void cancelNotification(String key) {
171        if (!isBound()) return;
172        try {
173            getNotificationInterface().cancelNotificationsFromListener(mWrapper,
174                    new String[] {key});
175        } catch (android.os.RemoteException ex) {
176            Log.v(TAG, "Unable to contact notification manager", ex);
177        }
178    }
179
180    /**
181     * Inform the notification manager about dismissal of all notifications.
182     * <p>
183     * Use this if your listener has a user interface that allows the user to dismiss all
184     * notifications, similar to the behavior of Android's status bar and notification panel.
185     * It should be called after the user invokes the "dismiss all" function of your UI;
186     * upon being informed, the notification manager will actually remove all active notifications
187     * and you will get multiple {@link #onNotificationRemoved(StatusBarNotification)} callbacks.
188     *
189     * {@see #cancelNotification(String, String, int)}
190     */
191    public final void cancelAllNotifications() {
192        cancelNotifications(null /*all*/);
193    }
194
195    /**
196     * Inform the notification manager about dismissal of specific notifications.
197     * <p>
198     * Use this if your listener has a user interface that allows the user to dismiss
199     * multiple notifications at once.
200     *
201     * @param keys Notifications to dismiss, or {@code null} to dismiss all.
202     *
203     * {@see #cancelNotification(String, String, int)}
204     */
205    public final void cancelNotifications(String[] keys) {
206        if (!isBound()) return;
207        try {
208            getNotificationInterface().cancelNotificationsFromListener(mWrapper, keys);
209        } catch (android.os.RemoteException ex) {
210            Log.v(TAG, "Unable to contact notification manager", ex);
211        }
212    }
213
214    /**
215     * Request the list of outstanding notifications (that is, those that are visible to the
216     * current user). Useful when you don't know what's already been posted.
217     *
218     * @return An array of active notifications, sorted in natural order.
219     */
220    public StatusBarNotification[] getActiveNotifications() {
221        return getActiveNotifications(null /*all*/);
222    }
223
224    /**
225     * Request the list of outstanding notifications (that is, those that are visible to the
226     * current user). Useful when you don't know what's already been posted.
227     *
228     * @param keys A specific list of notification keys, or {@code null} for all.
229     * @return An array of active notifications, sorted in natural order
230     *   if {@code keys} is {@code null}.
231     */
232    public StatusBarNotification[] getActiveNotifications(String[] keys) {
233        if (!isBound()) return null;
234        try {
235            return getNotificationInterface().getActiveNotificationsFromListener(mWrapper, keys);
236        } catch (android.os.RemoteException ex) {
237            Log.v(TAG, "Unable to contact notification manager", ex);
238        }
239        return null;
240    }
241
242    /**
243     * Request the list of notification keys in their current natural order.
244     * You can use the notification keys for subsequent retrieval via
245     * {@link #getActiveNotifications(String[]) or dismissal via
246     * {@link #cancelNotifications(String[]).
247     *
248     * @return An array of active notification keys, in their natural order.
249     */
250    public String[] getOrderedNotificationKeys() {
251        return mNotificationKeys;
252    }
253
254    @Override
255    public IBinder onBind(Intent intent) {
256        if (mWrapper == null) {
257            mWrapper = new INotificationListenerWrapper();
258        }
259        return mWrapper;
260    }
261
262    private boolean isBound() {
263        if (mWrapper == null) {
264            Log.w(TAG, "Notification listener service not yet bound.");
265            return false;
266        }
267        return true;
268    }
269
270    private class INotificationListenerWrapper extends INotificationListener.Stub {
271        @Override
272        public void onNotificationPosted(StatusBarNotification sbn,
273                NotificationOrderUpdate update) {
274            try {
275                // protect subclass from concurrent modifications of (@link mNotificationKeys}.
276                synchronized (mWrapper) {
277                    updateNotificationKeys(update);
278                    NotificationListenerService.this.onNotificationPosted(sbn);
279                }
280            } catch (Throwable t) {
281                Log.w(TAG, "Error running onOrderedNotificationPosted", t);
282            }
283        }
284        @Override
285        public void onNotificationRemoved(StatusBarNotification sbn,
286                NotificationOrderUpdate update) {
287            try {
288                // protect subclass from concurrent modifications of (@link mNotificationKeys}.
289                synchronized (mWrapper) {
290                    updateNotificationKeys(update);
291                    NotificationListenerService.this.onNotificationRemoved(sbn);
292                }
293            } catch (Throwable t) {
294                Log.w(TAG, "Error running onNotificationRemoved", t);
295            }
296        }
297        @Override
298        public void onListenerConnected(NotificationOrderUpdate update) {
299            try {
300                // protect subclass from concurrent modifications of (@link mNotificationKeys}.
301                synchronized (mWrapper) {
302                    updateNotificationKeys(update);
303                    NotificationListenerService.this.onListenerConnected(mNotificationKeys);
304                }
305            } catch (Throwable t) {
306                Log.w(TAG, "Error running onListenerConnected", t);
307            }
308        }
309        @Override
310        public void onNotificationOrderUpdate(NotificationOrderUpdate update)
311                throws RemoteException {
312            try {
313                // protect subclass from concurrent modifications of (@link mNotificationKeys}.
314                synchronized (mWrapper) {
315                    updateNotificationKeys(update);
316                    NotificationListenerService.this.onNotificationOrderUpdate();
317                }
318            } catch (Throwable t) {
319                Log.w(TAG, "Error running onNotificationOrderUpdate", t);
320            }
321        }
322    }
323
324    private void updateNotificationKeys(NotificationOrderUpdate update) {
325        // TODO: avoid garbage by comparing the lists
326        mNotificationKeys = update.getOrderedKeys();
327    }
328}
329