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