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