NotificationListenerService.java revision 1d599da8424cef8d07cb4c533bd212d992d8f676
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.SystemApi;
20import android.annotation.SdkConstant;
21import android.app.INotificationManager;
22import android.app.Service;
23import android.content.ComponentName;
24import android.content.Context;
25import android.content.Intent;
26import android.content.pm.ParceledListSlice;
27import android.os.IBinder;
28import android.os.Parcel;
29import android.os.Parcelable;
30import android.os.RemoteException;
31import android.os.ServiceManager;
32import android.util.Log;
33
34import java.util.List;
35
36/**
37 * A service that receives calls from the system when new notifications are
38 * posted or removed, or their ranking changed.
39 * <p>To extend this class, you must declare the service in your manifest file with
40 * the {@link android.Manifest.permission#BIND_NOTIFICATION_LISTENER_SERVICE} permission
41 * and include an intent filter with the {@link #SERVICE_INTERFACE} action. For example:</p>
42 * <pre>
43 * &lt;service android:name=".NotificationListener"
44 *          android:label="&#64;string/service_name"
45 *          android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
46 *     &lt;intent-filter>
47 *         &lt;action android:name="android.service.notification.NotificationListenerService" />
48 *     &lt;/intent-filter>
49 * &lt;/service></pre>
50 */
51public abstract class NotificationListenerService extends Service {
52    // TAG = "NotificationListenerService[MySubclass]"
53    private final String TAG = NotificationListenerService.class.getSimpleName()
54            + "[" + getClass().getSimpleName() + "]";
55
56    private INotificationListenerWrapper mWrapper = null;
57    private RankingMap mRankingMap;
58
59    private INotificationManager mNoMan;
60
61    /** Only valid after a successful call to (@link registerAsService}. */
62    private int mCurrentUser;
63
64    /**
65     * The {@link Intent} that must be declared as handled by the service.
66     */
67    @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
68    public static final String SERVICE_INTERFACE
69            = "android.service.notification.NotificationListenerService";
70
71    /**
72     * Implement this method to learn about new notifications as they are posted by apps.
73     *
74     * @param sbn A data structure encapsulating the original {@link android.app.Notification}
75     *            object as well as its identifying information (tag and id) and source
76     *            (package name).
77     */
78    public void onNotificationPosted(StatusBarNotification sbn) {
79        // optional
80    }
81
82    /**
83     * Implement this method to learn about new notifications as they are posted by apps.
84     *
85     * @param sbn A data structure encapsulating the original {@link android.app.Notification}
86     *            object as well as its identifying information (tag and id) and source
87     *            (package name).
88     * @param rankingMap The current ranking map that can be used to retrieve ranking information
89     *                   for active notifications, including the newly posted one.
90     */
91    public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
92        onNotificationPosted(sbn);
93    }
94
95    /**
96     * Implement this method to learn when notifications are removed.
97     * <P>
98     * This might occur because the user has dismissed the notification using system UI (or another
99     * notification listener) or because the app has withdrawn the notification.
100     * <P>
101     * NOTE: The {@link StatusBarNotification} object you receive will be "light"; that is, the
102     * result from {@link StatusBarNotification#getNotification} may be missing some heavyweight
103     * fields such as {@link android.app.Notification#contentView} and
104     * {@link android.app.Notification#largeIcon}. However, all other fields on
105     * {@link StatusBarNotification}, sufficient to match this call with a prior call to
106     * {@link #onNotificationPosted(StatusBarNotification)}, will be intact.
107     *
108     * @param sbn A data structure encapsulating at least the original information (tag and id)
109     *            and source (package name) used to post the {@link android.app.Notification} that
110     *            was just removed.
111     */
112    public void onNotificationRemoved(StatusBarNotification sbn) {
113        // optional
114    }
115
116    /**
117     * Implement this method to learn when notifications are removed.
118     * <P>
119     * This might occur because the user has dismissed the notification using system UI (or another
120     * notification listener) or because the app has withdrawn the notification.
121     * <P>
122     * NOTE: The {@link StatusBarNotification} object you receive will be "light"; that is, the
123     * result from {@link StatusBarNotification#getNotification} may be missing some heavyweight
124     * fields such as {@link android.app.Notification#contentView} and
125     * {@link android.app.Notification#largeIcon}. However, all other fields on
126     * {@link StatusBarNotification}, sufficient to match this call with a prior call to
127     * {@link #onNotificationPosted(StatusBarNotification)}, will be intact.
128     *
129     * @param sbn A data structure encapsulating at least the original information (tag and id)
130     *            and source (package name) used to post the {@link android.app.Notification} that
131     *            was just removed.
132     * @param rankingMap The current ranking map that can be used to retrieve ranking information
133     *                   for active notifications.
134     *
135     */
136    public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap) {
137        onNotificationRemoved(sbn);
138    }
139
140    /**
141     * Implement this method to learn about when the listener is enabled and connected to
142     * the notification manager.  You are safe to call {@link #getActiveNotifications()}
143     * at this time.
144     */
145    public void onListenerConnected() {
146        // optional
147    }
148
149    /**
150     * Implement this method to be notified when the notification ranking changes.
151     *
152     * @param rankingMap The current ranking map that can be used to retrieve ranking information
153     *                   for active notifications.
154     */
155    public void onNotificationRankingUpdate(RankingMap rankingMap) {
156        // optional
157    }
158
159    private final INotificationManager getNotificationInterface() {
160        if (mNoMan == null) {
161            mNoMan = INotificationManager.Stub.asInterface(
162                    ServiceManager.getService(Context.NOTIFICATION_SERVICE));
163        }
164        return mNoMan;
165    }
166
167    /**
168     * Inform the notification manager about dismissal of a single notification.
169     * <p>
170     * Use this if your listener has a user interface that allows the user to dismiss individual
171     * notifications, similar to the behavior of Android's status bar and notification panel.
172     * It should be called after the user dismisses a single notification using your UI;
173     * upon being informed, the notification manager will actually remove the notification
174     * and you will get an {@link #onNotificationRemoved(StatusBarNotification)} callback.
175     * <P>
176     * <b>Note:</b> If your listener allows the user to fire a notification's
177     * {@link android.app.Notification#contentIntent} by tapping/clicking/etc., you should call
178     * this method at that time <i>if</i> the Notification in question has the
179     * {@link android.app.Notification#FLAG_AUTO_CANCEL} flag set.
180     *
181     * @param pkg Package of the notifying app.
182     * @param tag Tag of the notification as specified by the notifying app in
183     *     {@link android.app.NotificationManager#notify(String, int, android.app.Notification)}.
184     * @param id  ID of the notification as specified by the notifying app in
185     *     {@link android.app.NotificationManager#notify(String, int, android.app.Notification)}.
186     * <p>
187     * @deprecated Use {@link #cancelNotification(String key)}
188     * instead. Beginning with {@link android.os.Build.VERSION_CODES#L} this method will no longer
189     * cancel the notification. It will continue to cancel the notification for applications
190     * whose {@code targetSdkVersion} is earlier than {@link android.os.Build.VERSION_CODES#L}.
191     */
192    public final void cancelNotification(String pkg, String tag, int id) {
193        if (!isBound()) return;
194        try {
195            getNotificationInterface().cancelNotificationFromListener(
196                    mWrapper, pkg, tag, id);
197        } catch (android.os.RemoteException ex) {
198            Log.v(TAG, "Unable to contact notification manager", ex);
199        }
200    }
201
202    /**
203     * Inform the notification manager about dismissal of a single notification.
204     * <p>
205     * Use this if your listener has a user interface that allows the user to dismiss individual
206     * notifications, similar to the behavior of Android's status bar and notification panel.
207     * It should be called after the user dismisses a single notification using your UI;
208     * upon being informed, the notification manager will actually remove the notification
209     * and you will get an {@link #onNotificationRemoved(StatusBarNotification)} callback.
210     * <P>
211     * <b>Note:</b> If your listener allows the user to fire a notification's
212     * {@link android.app.Notification#contentIntent} by tapping/clicking/etc., you should call
213     * this method at that time <i>if</i> the Notification in question has the
214     * {@link android.app.Notification#FLAG_AUTO_CANCEL} flag set.
215     * <p>
216     * @param key Notification to dismiss from {@link StatusBarNotification#getKey()}.
217     */
218    public final void cancelNotification(String key) {
219        if (!isBound()) return;
220        try {
221            getNotificationInterface().cancelNotificationsFromListener(mWrapper,
222                    new String[] {key});
223        } catch (android.os.RemoteException ex) {
224            Log.v(TAG, "Unable to contact notification manager", ex);
225        }
226    }
227
228    /**
229     * Inform the notification manager about dismissal of all notifications.
230     * <p>
231     * Use this if your listener has a user interface that allows the user to dismiss all
232     * notifications, similar to the behavior of Android's status bar and notification panel.
233     * It should be called after the user invokes the "dismiss all" function of your UI;
234     * upon being informed, the notification manager will actually remove all active notifications
235     * and you will get multiple {@link #onNotificationRemoved(StatusBarNotification)} callbacks.
236     *
237     * {@see #cancelNotification(String, String, int)}
238     */
239    public final void cancelAllNotifications() {
240        cancelNotifications(null /*all*/);
241    }
242
243    /**
244     * Inform the notification manager about dismissal of specific notifications.
245     * <p>
246     * Use this if your listener has a user interface that allows the user to dismiss
247     * multiple notifications at once.
248     *
249     * @param keys Notifications to dismiss, or {@code null} to dismiss all.
250     *
251     * {@see #cancelNotification(String, String, int)}
252     */
253    public final void cancelNotifications(String[] keys) {
254        if (!isBound()) return;
255        try {
256            getNotificationInterface().cancelNotificationsFromListener(mWrapper, keys);
257        } catch (android.os.RemoteException ex) {
258            Log.v(TAG, "Unable to contact notification manager", ex);
259        }
260    }
261
262    /**
263     * Request the list of outstanding notifications (that is, those that are visible to the
264     * current user). Useful when you don't know what's already been posted.
265     *
266     * @return An array of active notifications, sorted in natural order.
267     */
268    public StatusBarNotification[] getActiveNotifications() {
269        if (!isBound()) return null;
270        try {
271            ParceledListSlice<StatusBarNotification> parceledList =
272                    getNotificationInterface().getActiveNotificationsFromListener(mWrapper);
273            List<StatusBarNotification> list = parceledList.getList();
274            return list.toArray(new StatusBarNotification[list.size()]);
275        } catch (android.os.RemoteException ex) {
276            Log.v(TAG, "Unable to contact notification manager", ex);
277        }
278        return null;
279    }
280
281    /**
282     * Returns current ranking information.
283     *
284     * <p>
285     * The returned object represents the current ranking snapshot and only
286     * applies for currently active notifications.
287     * <p>
288     * Generally you should use the RankingMap that is passed with events such
289     * as {@link #onNotificationPosted(StatusBarNotification, RankingMap)},
290     * {@link #onNotificationRemoved(StatusBarNotification, RankingMap)}, and
291     * so on. This method should only be used when needing access outside of
292     * such events, for example to retrieve the RankingMap right after
293     * initialization.
294     *
295     * @return A {@link RankingMap} object providing access to ranking information
296     */
297    public RankingMap getCurrentRanking() {
298        return mRankingMap;
299    }
300
301    @Override
302    public IBinder onBind(Intent intent) {
303        if (mWrapper == null) {
304            mWrapper = new INotificationListenerWrapper();
305        }
306        return mWrapper;
307    }
308
309    private boolean isBound() {
310        if (mWrapper == null) {
311            Log.w(TAG, "Notification listener service not yet bound.");
312            return false;
313        }
314        return true;
315    }
316
317    /**
318     * Directly register this service with the Notification Manager.
319     *
320     * <p>Only system services may use this call. It will fail for non-system callers.
321     * Apps should ask the user to add their listener in Settings.
322     *
323     * @param componentName the component that will consume the notification information
324     * @param currentUser the user to use as the stream filter
325     * @hide
326     */
327    @SystemApi
328    public void registerAsSystemService(ComponentName componentName, int currentUser)
329            throws RemoteException {
330        if (mWrapper == null) {
331            mWrapper = new INotificationListenerWrapper();
332        }
333        INotificationManager noMan = getNotificationInterface();
334        noMan.registerListener(mWrapper, componentName, currentUser);
335        mCurrentUser = currentUser;
336    }
337
338    /**
339     * Directly unregister this service from the Notification Manager.
340     *
341     * <P>This method will fail for listeners that were not registered
342     * with (@link registerAsService).
343     * @hide
344     */
345    @SystemApi
346    public void unregisterAsSystemService() throws RemoteException {
347        if (mWrapper != null) {
348            INotificationManager noMan = getNotificationInterface();
349            noMan.unregisterListener(mWrapper, mCurrentUser);
350        }
351    }
352
353    private class INotificationListenerWrapper extends INotificationListener.Stub {
354        @Override
355        public void onNotificationPosted(StatusBarNotification sbn,
356                NotificationRankingUpdate update) {
357            // protect subclass from concurrent modifications of (@link mNotificationKeys}.
358            synchronized (mWrapper) {
359                applyUpdate(update);
360                try {
361                    NotificationListenerService.this.onNotificationPosted(sbn, mRankingMap);
362                } catch (Throwable t) {
363                    Log.w(TAG, "Error running onNotificationPosted", t);
364                }
365            }
366        }
367        @Override
368        public void onNotificationRemoved(StatusBarNotification sbn,
369                NotificationRankingUpdate update) {
370            // protect subclass from concurrent modifications of (@link mNotificationKeys}.
371            synchronized (mWrapper) {
372                applyUpdate(update);
373                try {
374                    NotificationListenerService.this.onNotificationRemoved(sbn, mRankingMap);
375                } catch (Throwable t) {
376                    Log.w(TAG, "Error running onNotificationRemoved", t);
377                }
378            }
379        }
380        @Override
381        public void onListenerConnected(NotificationRankingUpdate update) {
382            // protect subclass from concurrent modifications of (@link mNotificationKeys}.
383            synchronized (mWrapper) {
384                applyUpdate(update);
385                try {
386                    NotificationListenerService.this.onListenerConnected();
387                } catch (Throwable t) {
388                    Log.w(TAG, "Error running onListenerConnected", t);
389                }
390            }
391        }
392        @Override
393        public void onNotificationRankingUpdate(NotificationRankingUpdate update)
394                throws RemoteException {
395            // protect subclass from concurrent modifications of (@link mNotificationKeys}.
396            synchronized (mWrapper) {
397                applyUpdate(update);
398                try {
399                    NotificationListenerService.this.onNotificationRankingUpdate(mRankingMap);
400                } catch (Throwable t) {
401                    Log.w(TAG, "Error running onNotificationRankingUpdate", t);
402                }
403            }
404        }
405    }
406
407    private void applyUpdate(NotificationRankingUpdate update) {
408        mRankingMap = new RankingMap(update);
409    }
410
411    /**
412     * Stores ranking related information on a currently active notification.
413     *
414     * <p>
415     * Ranking objects aren't automatically updated as notification events
416     * occur. Instead, ranking information has to be retrieved again via the
417     * current {@link RankingMap}.
418     */
419    public static class Ranking {
420        private String mKey;
421        private int mRank = -1;
422        private boolean mIsAmbient;
423        private boolean mMeetsInterruptionFilter;
424
425        public Ranking() {}
426
427        /**
428         * Returns the key of the notification this Ranking applies to.
429         */
430        public String getKey() {
431            return mKey;
432        }
433
434        /**
435         * Returns the rank of the notification.
436         *
437         * @return the rank of the notification, that is the 0-based index in
438         *     the list of active notifications.
439         */
440        public int getRank() {
441            return mRank;
442        }
443
444        /**
445         * Returns whether the notification is an ambient notification, that is
446         * a notification that doesn't require the user's immediate attention.
447         */
448        public boolean isAmbient() {
449            return mIsAmbient;
450        }
451
452        /**
453         * Returns whether the notification meets the user's interruption
454         * filter.
455         */
456        public boolean meetsInterruptionFilter() {
457            return mMeetsInterruptionFilter;
458        }
459
460        private void populate(String key, int rank, boolean isAmbient,
461                boolean meetsInterruptionFilter) {
462            mKey = key;
463            mRank = rank;
464            mIsAmbient = isAmbient;
465            mMeetsInterruptionFilter = meetsInterruptionFilter;
466        }
467    }
468
469    /**
470     * Provides access to ranking information on currently active
471     * notifications.
472     *
473     * <p>
474     * Note that this object represents a ranking snapshot that only applies to
475     * notifications active at the time of retrieval.
476     */
477    public static class RankingMap implements Parcelable {
478        private final NotificationRankingUpdate mRankingUpdate;
479
480        private RankingMap(NotificationRankingUpdate rankingUpdate) {
481            mRankingUpdate = rankingUpdate;
482        }
483
484        /**
485         * Request the list of notification keys in their current ranking
486         * order.
487         *
488         * @return An array of active notification keys, in their ranking order.
489         */
490        public String[] getOrderedKeys() {
491            return mRankingUpdate.getOrderedKeys();
492        }
493
494        /**
495         * Populates outRanking with ranking information for the notification
496         * with the given key.
497         *
498         * @return true if a valid key has been passed and outRanking has
499         *     been populated; false otherwise
500         */
501        public boolean getRanking(String key, Ranking outRanking) {
502            int rank = getRank(key);
503            outRanking.populate(key, rank, isAmbient(key), !isIntercepted(key));
504            return rank >= 0;
505        }
506
507        private int getRank(String key) {
508            // TODO: Optimize.
509            String[] orderedKeys = mRankingUpdate.getOrderedKeys();
510            for (int i = 0; i < orderedKeys.length; i++) {
511                if (orderedKeys[i].equals(key)) {
512                    return i;
513                }
514            }
515            return -1;
516        }
517
518        private boolean isAmbient(String key) {
519            int rank = getRank(key);
520            return rank >= 0 && rank >= mRankingUpdate.getFirstAmbientIndex();
521        }
522
523        private boolean isIntercepted(String key) {
524            // TODO: Optimize.
525            for (String interceptedKey : mRankingUpdate.getInterceptedKeys()) {
526                if (interceptedKey.equals(key)) {
527                    return true;
528                }
529            }
530            return false;
531        }
532
533        // ----------- Parcelable
534
535        @Override
536        public int describeContents() {
537            return 0;
538        }
539
540        @Override
541        public void writeToParcel(Parcel dest, int flags) {
542            dest.writeParcelable(mRankingUpdate, flags);
543        }
544
545        public static final Creator<RankingMap> CREATOR = new Creator<RankingMap>() {
546            @Override
547            public RankingMap createFromParcel(Parcel source) {
548                NotificationRankingUpdate rankingUpdate = source.readParcelable(null);
549                return new RankingMap(rankingUpdate);
550            }
551
552            @Override
553            public RankingMap[] newArray(int size) {
554                return new RankingMap[size];
555            }
556        };
557    }
558}
559