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