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