blob: fd475cd56eba0b7ee937bf2447805b4669eacb10 [file] [log] [blame]
Daniel Sandler5feceeb2013-03-22 18:29:23 -07001/*
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
Jeff Brownd5a5b5a2014-06-05 17:14:39 -070019import android.annotation.SystemApi;
Daniel Sandler5feceeb2013-03-22 18:29:23 -070020import android.annotation.SdkConstant;
21import android.app.INotificationManager;
22import android.app.Service;
Chris Wren1941fc72014-05-14 15:20:51 -040023import android.content.ComponentName;
Daniel Sandler5feceeb2013-03-22 18:29:23 -070024import android.content.Context;
25import android.content.Intent;
Christoph Studercee44ba2014-05-20 18:36:43 +020026import android.content.pm.ParceledListSlice;
Daniel Sandler5feceeb2013-03-22 18:29:23 -070027import android.os.IBinder;
Christoph Studer05ad4822014-05-16 14:16:03 +020028import android.os.Parcel;
29import android.os.Parcelable;
Chris Wrenf9536642014-04-17 10:01:54 -040030import android.os.RemoteException;
Daniel Sandler5feceeb2013-03-22 18:29:23 -070031import android.os.ServiceManager;
32import android.util.Log;
33
Christoph Studercee44ba2014-05-20 18:36:43 +020034import java.util.List;
35
Scott Main04667da2013-04-25 16:57:16 -070036/**
Christoph Studer05ad4822014-05-16 14:16:03 +020037 * A service that receives calls from the system when new notifications are
38 * posted or removed, or their ranking changed.
Scott Main04667da2013-04-25 16:57:16 -070039 * <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 */
Daniel Sandler5feceeb2013-03-22 18:29:23 -070051public 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;
Christoph Studer05ad4822014-05-16 14:16:03 +020057 private Ranking mRanking;
Daniel Sandler5feceeb2013-03-22 18:29:23 -070058
59 private INotificationManager mNoMan;
60
Chris Wren1941fc72014-05-14 15:20:51 -040061 /** Only valid after a successful call to (@link registerAsService}. */
62 private int mCurrentUser;
63
Daniel Sandler5feceeb2013-03-22 18:29:23 -070064 /**
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.
Daniel Sandler1a497d32013-04-18 14:52:45 -040085 * <P>
86 * NOTE: The {@link StatusBarNotification} object you receive will be "light"; that is, the
Scott Main04667da2013-04-25 16:57:16 -070087 * result from {@link StatusBarNotification#getNotification} may be missing some heavyweight
Daniel Sandler1a497d32013-04-18 14:52:45 -040088 * 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.
Daniel Sandler5feceeb2013-03-22 18:29:23 -070092 *
Daniel Sandler1a497d32013-04-18 14:52:45 -040093 * @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.
Daniel Sandler5feceeb2013-03-22 18:29:23 -070096 */
97 public abstract void onNotificationRemoved(StatusBarNotification sbn);
98
John Spurlocka4294292014-03-24 18:02:32 -040099 /**
100 * Implement this method to learn about when the listener is enabled and connected to
Christoph Studercee44ba2014-05-20 18:36:43 +0200101 * the notification manager. You are safe to call {@link #getActiveNotifications()}
John Spurlocka4294292014-03-24 18:02:32 -0400102 * at this time.
John Spurlocka4294292014-03-24 18:02:32 -0400103 */
Christoph Studercee44ba2014-05-20 18:36:43 +0200104 public void onListenerConnected() {
John Spurlocka4294292014-03-24 18:02:32 -0400105 // optional
106 }
107
Chris Wrenf9536642014-04-17 10:01:54 -0400108 /**
Christoph Studer05ad4822014-05-16 14:16:03 +0200109 * Implement this method to be notified when the notification ranking changes.
110 * <P>
111 * Call {@link #getCurrentRanking()} to retrieve the new ranking.
Chris Wrenf9536642014-04-17 10:01:54 -0400112 */
Christoph Studer05ad4822014-05-16 14:16:03 +0200113 public void onNotificationRankingUpdate() {
Chris Wrenf9536642014-04-17 10:01:54 -0400114 // optional
115 }
116
Daniel Sandler5feceeb2013-03-22 18:29:23 -0700117 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)}.
Kenny Guya263e4e2014-03-03 18:24:03 +0000144 * <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}.
Daniel Sandler5feceeb2013-03-22 18:29:23 -0700149 */
Daniel Sandlere6f7f2e2013-04-25 15:44:16 -0400150 public final void cancelNotification(String pkg, String tag, int id) {
John Spurlockda9a3be2014-02-12 12:12:26 -0500151 if (!isBound()) return;
Daniel Sandler5feceeb2013-03-22 18:29:23 -0700152 try {
Kenny Guya263e4e2014-03-03 18:24:03 +0000153 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});
Daniel Sandler5feceeb2013-03-22 18:29:23 -0700181 } 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 *
Daniel Sandlere6f7f2e2013-04-25 15:44:16 -0400195 * {@see #cancelNotification(String, String, int)}
Daniel Sandler5feceeb2013-03-22 18:29:23 -0700196 */
Daniel Sandlere6f7f2e2013-04-25 15:44:16 -0400197 public final void cancelAllNotifications() {
John Spurlocka4294292014-03-24 18:02:32 -0400198 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) {
John Spurlockda9a3be2014-02-12 12:12:26 -0500212 if (!isBound()) return;
Daniel Sandler5feceeb2013-03-22 18:29:23 -0700213 try {
John Spurlocka4294292014-03-24 18:02:32 -0400214 getNotificationInterface().cancelNotificationsFromListener(mWrapper, keys);
Daniel Sandler5feceeb2013-03-22 18:29:23 -0700215 } catch (android.os.RemoteException ex) {
216 Log.v(TAG, "Unable to contact notification manager", ex);
217 }
218 }
219
Daniel Sandler25cf8ce2013-04-24 15:34:57 -0400220 /**
221 * Request the list of outstanding notifications (that is, those that are visible to the
John Spurlocka4294292014-03-24 18:02:32 -0400222 * current user). Useful when you don't know what's already been posted.
Daniel Sandler25cf8ce2013-04-24 15:34:57 -0400223 *
Chris Wrenf9536642014-04-17 10:01:54 -0400224 * @return An array of active notifications, sorted in natural order.
Daniel Sandler25cf8ce2013-04-24 15:34:57 -0400225 */
226 public StatusBarNotification[] getActiveNotifications() {
John Spurlockda9a3be2014-02-12 12:12:26 -0500227 if (!isBound()) return null;
Daniel Sandler25cf8ce2013-04-24 15:34:57 -0400228 try {
Christoph Studercee44ba2014-05-20 18:36:43 +0200229 ParceledListSlice<StatusBarNotification> parceledList =
230 getNotificationInterface().getActiveNotificationsFromListener(mWrapper);
231 List<StatusBarNotification> list = parceledList.getList();
232 return list.toArray(new StatusBarNotification[list.size()]);
John Spurlocka4294292014-03-24 18:02:32 -0400233 } catch (android.os.RemoteException ex) {
234 Log.v(TAG, "Unable to contact notification manager", ex);
235 }
236 return null;
237 }
238
239 /**
Christoph Studer05ad4822014-05-16 14:16:03 +0200240 * Returns current ranking information.
John Spurlocka4294292014-03-24 18:02:32 -0400241 *
Christoph Studer05ad4822014-05-16 14:16:03 +0200242 * <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
John Spurlocka4294292014-03-24 18:02:32 -0400251 */
Christoph Studer05ad4822014-05-16 14:16:03 +0200252 public Ranking getCurrentRanking() {
253 return mRanking;
Daniel Sandler25cf8ce2013-04-24 15:34:57 -0400254 }
255
Daniel Sandler5feceeb2013-03-22 18:29:23 -0700256 @Override
257 public IBinder onBind(Intent intent) {
258 if (mWrapper == null) {
259 mWrapper = new INotificationListenerWrapper();
260 }
261 return mWrapper;
262 }
263
John Spurlockda9a3be2014-02-12 12:12:26 -0500264 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
Chris Wren1941fc72014-05-14 15:20:51 -0400272 /**
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 */
Jeff Brownd5a5b5a2014-06-05 17:14:39 -0700282 @SystemApi
Chris Wren1941fc72014-05-14 15:20:51 -0400283 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 */
Jeff Brownd5a5b5a2014-06-05 17:14:39 -0700300 @SystemApi
Chris Wren1941fc72014-05-14 15:20:51 -0400301 public void unregisterAsSystemService() throws RemoteException {
302 if (mWrapper != null) {
303 INotificationManager noMan = getNotificationInterface();
304 noMan.unregisterListener(mWrapper, mCurrentUser);
305 }
306 }
307
Daniel Sandler5feceeb2013-03-22 18:29:23 -0700308 private class INotificationListenerWrapper extends INotificationListener.Stub {
309 @Override
Chris Wrenf9536642014-04-17 10:01:54 -0400310 public void onNotificationPosted(StatusBarNotification sbn,
Christoph Studer05ad4822014-05-16 14:16:03 +0200311 NotificationRankingUpdate update) {
312 // protect subclass from concurrent modifications of (@link mNotificationKeys}.
313 synchronized (mWrapper) {
314 applyUpdate(update);
315 try {
Chris Wrenf9536642014-04-17 10:01:54 -0400316 NotificationListenerService.this.onNotificationPosted(sbn);
Christoph Studer05ad4822014-05-16 14:16:03 +0200317 } catch (Throwable t) {
318 Log.w(TAG, "Error running onNotificationPosted", t);
Chris Wrenf9536642014-04-17 10:01:54 -0400319 }
John Spurlockc133ab82013-06-10 15:16:22 -0400320 }
Daniel Sandler5feceeb2013-03-22 18:29:23 -0700321 }
322 @Override
Chris Wrenf9536642014-04-17 10:01:54 -0400323 public void onNotificationRemoved(StatusBarNotification sbn,
Christoph Studer05ad4822014-05-16 14:16:03 +0200324 NotificationRankingUpdate update) {
325 // protect subclass from concurrent modifications of (@link mNotificationKeys}.
326 synchronized (mWrapper) {
327 applyUpdate(update);
328 try {
Chris Wrenf9536642014-04-17 10:01:54 -0400329 NotificationListenerService.this.onNotificationRemoved(sbn);
Christoph Studer05ad4822014-05-16 14:16:03 +0200330 } catch (Throwable t) {
331 Log.w(TAG, "Error running onNotificationRemoved", t);
Chris Wrenf9536642014-04-17 10:01:54 -0400332 }
John Spurlockc133ab82013-06-10 15:16:22 -0400333 }
Daniel Sandler5feceeb2013-03-22 18:29:23 -0700334 }
John Spurlocka4294292014-03-24 18:02:32 -0400335 @Override
Christoph Studer05ad4822014-05-16 14:16:03 +0200336 public void onListenerConnected(NotificationRankingUpdate update) {
337 // protect subclass from concurrent modifications of (@link mNotificationKeys}.
338 synchronized (mWrapper) {
339 applyUpdate(update);
340 try {
Christoph Studercee44ba2014-05-20 18:36:43 +0200341 NotificationListenerService.this.onListenerConnected();
Christoph Studer05ad4822014-05-16 14:16:03 +0200342 } catch (Throwable t) {
343 Log.w(TAG, "Error running onListenerConnected", t);
Chris Wrenf9536642014-04-17 10:01:54 -0400344 }
John Spurlocka4294292014-03-24 18:02:32 -0400345 }
346 }
Chris Wrenf9536642014-04-17 10:01:54 -0400347 @Override
Christoph Studer05ad4822014-05-16 14:16:03 +0200348 public void onNotificationRankingUpdate(NotificationRankingUpdate update)
Chris Wrenf9536642014-04-17 10:01:54 -0400349 throws RemoteException {
Christoph Studer05ad4822014-05-16 14:16:03 +0200350 // 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);
Chris Wrenf9536642014-04-17 10:01:54 -0400357 }
Chris Wrenf9536642014-04-17 10:01:54 -0400358 }
359 }
360 }
361
Christoph Studer05ad4822014-05-16 14:16:03 +0200362 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 */
Christoph Studercee44ba2014-05-20 18:36:43 +0200399 public int getRank(String key) {
Christoph Studer05ad4822014-05-16 14:16:03 +0200400 // 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 };
Daniel Sandler5feceeb2013-03-22 18:29:23 -0700468 }
469}