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