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