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