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