NotificationListenerService.java revision 5c507c1371d273cdff61d2395d47bd54fa26d614
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 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 @SystemApi 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 @SystemApi 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 "Do not disturb". 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