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