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 
17 package android.service.notification;
18 
19 import android.annotation.SystemApi;
20 import android.annotation.SdkConstant;
21 import android.app.INotificationManager;
22 import android.app.Notification;
23 import android.app.Notification.Builder;
24 import android.app.NotificationManager;
25 import android.app.Service;
26 import android.content.ComponentName;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.pm.ParceledListSlice;
30 import android.graphics.drawable.BitmapDrawable;
31 import android.graphics.drawable.Drawable;
32 import android.graphics.drawable.Icon;
33 import android.graphics.Bitmap;
34 import android.os.Bundle;
35 import android.os.IBinder;
36 import android.os.Parcel;
37 import android.os.Parcelable;
38 import android.os.RemoteException;
39 import android.os.ServiceManager;
40 import android.util.ArrayMap;
41 import android.util.ArraySet;
42 import android.util.Log;
43 
44 import java.util.ArrayList;
45 import java.util.Collections;
46 import java.util.List;
47 
48 /**
49  * A service that receives calls from the system when new notifications are
50  * posted or removed, or their ranking changed.
51  * <p>To extend this class, you must declare the service in your manifest file with
52  * the {@link android.Manifest.permission#BIND_NOTIFICATION_LISTENER_SERVICE} permission
53  * and include an intent filter with the {@link #SERVICE_INTERFACE} action. For example:</p>
54  * <pre>
55  * &lt;service android:name=".NotificationListener"
56  *          android:label="&#64;string/service_name"
57  *          android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
58  *     &lt;intent-filter>
59  *         &lt;action android:name="android.service.notification.NotificationListenerService" />
60  *     &lt;/intent-filter>
61  * &lt;/service></pre>
62  */
63 public abstract class NotificationListenerService extends Service {
64     // TAG = "NotificationListenerService[MySubclass]"
65     private final String TAG = NotificationListenerService.class.getSimpleName()
66             + "[" + getClass().getSimpleName() + "]";
67 
68     /**
69      * {@link #getCurrentInterruptionFilter() Interruption filter} constant -
70      *     Normal interruption filter.
71      */
72     public static final int INTERRUPTION_FILTER_ALL
73             = NotificationManager.INTERRUPTION_FILTER_ALL;
74 
75     /**
76      * {@link #getCurrentInterruptionFilter() Interruption filter} constant -
77      *     Priority interruption filter.
78      */
79     public static final int INTERRUPTION_FILTER_PRIORITY
80             = NotificationManager.INTERRUPTION_FILTER_PRIORITY;
81 
82     /**
83      * {@link #getCurrentInterruptionFilter() Interruption filter} constant -
84      *     No interruptions filter.
85      */
86     public static final int INTERRUPTION_FILTER_NONE
87             = NotificationManager.INTERRUPTION_FILTER_NONE;
88 
89     /**
90      * {@link #getCurrentInterruptionFilter() Interruption filter} constant -
91      *     Alarms only interruption filter.
92      */
93     public static final int INTERRUPTION_FILTER_ALARMS
94             = NotificationManager.INTERRUPTION_FILTER_ALARMS;
95 
96     /** {@link #getCurrentInterruptionFilter() Interruption filter} constant - returned when
97      * the value is unavailable for any reason.  For example, before the notification listener
98      * is connected.
99      *
100      * {@see #onListenerConnected()}
101      */
102     public static final int INTERRUPTION_FILTER_UNKNOWN
103             = NotificationManager.INTERRUPTION_FILTER_UNKNOWN;
104 
105     /** {@link #getCurrentListenerHints() Listener hints} constant - the primary device UI
106      * should disable notification sound, vibrating and other visual or aural effects.
107      * This does not change the interruption filter, only the effects. **/
108     public static final int HINT_HOST_DISABLE_EFFECTS = 1;
109 
110     /**
111      * The full trim of the StatusBarNotification including all its features.
112      *
113      * @hide
114      */
115     @SystemApi
116     public static final int TRIM_FULL = 0;
117 
118     /**
119      * A light trim of the StatusBarNotification excluding the following features:
120      *
121      * <ol>
122      *     <li>{@link Notification#tickerView tickerView}</li>
123      *     <li>{@link Notification#contentView contentView}</li>
124      *     <li>{@link Notification#largeIcon largeIcon}</li>
125      *     <li>{@link Notification#bigContentView bigContentView}</li>
126      *     <li>{@link Notification#headsUpContentView headsUpContentView}</li>
127      *     <li>{@link Notification#EXTRA_LARGE_ICON extras[EXTRA_LARGE_ICON]}</li>
128      *     <li>{@link Notification#EXTRA_LARGE_ICON_BIG extras[EXTRA_LARGE_ICON_BIG]}</li>
129      *     <li>{@link Notification#EXTRA_PICTURE extras[EXTRA_PICTURE]}</li>
130      *     <li>{@link Notification#EXTRA_BIG_TEXT extras[EXTRA_BIG_TEXT]}</li>
131      * </ol>
132      *
133      * @hide
134      */
135     @SystemApi
136     public static final int TRIM_LIGHT = 1;
137 
138     private INotificationListenerWrapper mWrapper = null;
139     private RankingMap mRankingMap;
140 
141     private INotificationManager mNoMan;
142 
143     /** Only valid after a successful call to (@link registerAsService}. */
144     private int mCurrentUser;
145 
146 
147     // This context is required for system services since NotificationListenerService isn't
148     // started as a real Service and hence no context is available.
149     private Context mSystemContext;
150 
151     /**
152      * The {@link Intent} that must be declared as handled by the service.
153      */
154     @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
155     public static final String SERVICE_INTERFACE
156             = "android.service.notification.NotificationListenerService";
157 
158     /**
159      * Implement this method to learn about new notifications as they are posted by apps.
160      *
161      * @param sbn A data structure encapsulating the original {@link android.app.Notification}
162      *            object as well as its identifying information (tag and id) and source
163      *            (package name).
164      */
onNotificationPosted(StatusBarNotification sbn)165     public void onNotificationPosted(StatusBarNotification sbn) {
166         // optional
167     }
168 
169     /**
170      * Implement this method to learn about new notifications as they are posted by apps.
171      *
172      * @param sbn A data structure encapsulating the original {@link android.app.Notification}
173      *            object as well as its identifying information (tag and id) and source
174      *            (package name).
175      * @param rankingMap The current ranking map that can be used to retrieve ranking information
176      *                   for active notifications, including the newly posted one.
177      */
onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap)178     public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
179         onNotificationPosted(sbn);
180     }
181 
182     /**
183      * Implement this method to learn when notifications are removed.
184      * <P>
185      * This might occur because the user has dismissed the notification using system UI (or another
186      * notification listener) or because the app has withdrawn the notification.
187      * <P>
188      * NOTE: The {@link StatusBarNotification} object you receive will be "light"; that is, the
189      * result from {@link StatusBarNotification#getNotification} may be missing some heavyweight
190      * fields such as {@link android.app.Notification#contentView} and
191      * {@link android.app.Notification#largeIcon}. However, all other fields on
192      * {@link StatusBarNotification}, sufficient to match this call with a prior call to
193      * {@link #onNotificationPosted(StatusBarNotification)}, will be intact.
194      *
195      * @param sbn A data structure encapsulating at least the original information (tag and id)
196      *            and source (package name) used to post the {@link android.app.Notification} that
197      *            was just removed.
198      */
onNotificationRemoved(StatusBarNotification sbn)199     public void onNotificationRemoved(StatusBarNotification sbn) {
200         // optional
201     }
202 
203     /**
204      * Implement this method to learn when notifications are removed.
205      * <P>
206      * This might occur because the user has dismissed the notification using system UI (or another
207      * notification listener) or because the app has withdrawn the notification.
208      * <P>
209      * NOTE: The {@link StatusBarNotification} object you receive will be "light"; that is, the
210      * result from {@link StatusBarNotification#getNotification} may be missing some heavyweight
211      * fields such as {@link android.app.Notification#contentView} and
212      * {@link android.app.Notification#largeIcon}. However, all other fields on
213      * {@link StatusBarNotification}, sufficient to match this call with a prior call to
214      * {@link #onNotificationPosted(StatusBarNotification)}, will be intact.
215      *
216      * @param sbn A data structure encapsulating at least the original information (tag and id)
217      *            and source (package name) used to post the {@link android.app.Notification} that
218      *            was just removed.
219      * @param rankingMap The current ranking map that can be used to retrieve ranking information
220      *                   for active notifications.
221      *
222      */
onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap)223     public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap) {
224         onNotificationRemoved(sbn);
225     }
226 
227     /**
228      * Implement this method to learn about when the listener is enabled and connected to
229      * the notification manager.  You are safe to call {@link #getActiveNotifications()}
230      * at this time.
231      */
onListenerConnected()232     public void onListenerConnected() {
233         // optional
234     }
235 
236     /**
237      * Implement this method to be notified when the notification ranking changes.
238      *
239      * @param rankingMap The current ranking map that can be used to retrieve ranking information
240      *                   for active notifications.
241      */
onNotificationRankingUpdate(RankingMap rankingMap)242     public void onNotificationRankingUpdate(RankingMap rankingMap) {
243         // optional
244     }
245 
246     /**
247      * Implement this method to be notified when the
248      * {@link #getCurrentListenerHints() Listener hints} change.
249      *
250      * @param hints The current {@link #getCurrentListenerHints() listener hints}.
251      */
onListenerHintsChanged(int hints)252     public void onListenerHintsChanged(int hints) {
253         // optional
254     }
255 
256     /**
257      * Implement this method to be notified when the
258      * {@link #getCurrentInterruptionFilter() interruption filter} changed.
259      *
260      * @param interruptionFilter The current
261      *     {@link #getCurrentInterruptionFilter() interruption filter}.
262      */
onInterruptionFilterChanged(int interruptionFilter)263     public void onInterruptionFilterChanged(int interruptionFilter) {
264         // optional
265     }
266 
getNotificationInterface()267     private final INotificationManager getNotificationInterface() {
268         if (mNoMan == null) {
269             mNoMan = INotificationManager.Stub.asInterface(
270                     ServiceManager.getService(Context.NOTIFICATION_SERVICE));
271         }
272         return mNoMan;
273     }
274 
275     /**
276      * Inform the notification manager about dismissal of a single notification.
277      * <p>
278      * Use this if your listener has a user interface that allows the user to dismiss individual
279      * notifications, similar to the behavior of Android's status bar and notification panel.
280      * It should be called after the user dismisses a single notification using your UI;
281      * upon being informed, the notification manager will actually remove the notification
282      * and you will get an {@link #onNotificationRemoved(StatusBarNotification)} callback.
283      * <P>
284      * <b>Note:</b> If your listener allows the user to fire a notification's
285      * {@link android.app.Notification#contentIntent} by tapping/clicking/etc., you should call
286      * this method at that time <i>if</i> the Notification in question has the
287      * {@link android.app.Notification#FLAG_AUTO_CANCEL} flag set.
288      *
289      * @param pkg Package of the notifying app.
290      * @param tag Tag of the notification as specified by the notifying app in
291      *     {@link android.app.NotificationManager#notify(String, int, android.app.Notification)}.
292      * @param id  ID of the notification as specified by the notifying app in
293      *     {@link android.app.NotificationManager#notify(String, int, android.app.Notification)}.
294      * <p>
295      * @deprecated Use {@link #cancelNotification(String key)}
296      * instead. Beginning with {@link android.os.Build.VERSION_CODES#LOLLIPOP} this method will no longer
297      * cancel the notification. It will continue to cancel the notification for applications
298      * whose {@code targetSdkVersion} is earlier than {@link android.os.Build.VERSION_CODES#LOLLIPOP}.
299      */
cancelNotification(String pkg, String tag, int id)300     public final void cancelNotification(String pkg, String tag, int id) {
301         if (!isBound()) return;
302         try {
303             getNotificationInterface().cancelNotificationFromListener(
304                     mWrapper, pkg, tag, id);
305         } catch (android.os.RemoteException ex) {
306             Log.v(TAG, "Unable to contact notification manager", ex);
307         }
308     }
309 
310     /**
311      * Inform the notification manager about dismissal of a single notification.
312      * <p>
313      * Use this if your listener has a user interface that allows the user to dismiss individual
314      * notifications, similar to the behavior of Android's status bar and notification panel.
315      * It should be called after the user dismisses a single notification using your UI;
316      * upon being informed, the notification manager will actually remove the notification
317      * and you will get an {@link #onNotificationRemoved(StatusBarNotification)} callback.
318      * <P>
319      * <b>Note:</b> If your listener allows the user to fire a notification's
320      * {@link android.app.Notification#contentIntent} by tapping/clicking/etc., you should call
321      * this method at that time <i>if</i> the Notification in question has the
322      * {@link android.app.Notification#FLAG_AUTO_CANCEL} flag set.
323      * <p>
324      * @param key Notification to dismiss from {@link StatusBarNotification#getKey()}.
325      */
cancelNotification(String key)326     public final void cancelNotification(String key) {
327         if (!isBound()) return;
328         try {
329             getNotificationInterface().cancelNotificationsFromListener(mWrapper,
330                     new String[] { key });
331         } catch (android.os.RemoteException ex) {
332             Log.v(TAG, "Unable to contact notification manager", ex);
333         }
334     }
335 
336     /**
337      * Inform the notification manager about dismissal of all notifications.
338      * <p>
339      * Use this if your listener has a user interface that allows the user to dismiss all
340      * notifications, similar to the behavior of Android's status bar and notification panel.
341      * It should be called after the user invokes the "dismiss all" function of your UI;
342      * upon being informed, the notification manager will actually remove all active notifications
343      * and you will get multiple {@link #onNotificationRemoved(StatusBarNotification)} callbacks.
344      *
345      * {@see #cancelNotification(String, String, int)}
346      */
cancelAllNotifications()347     public final void cancelAllNotifications() {
348         cancelNotifications(null /*all*/);
349     }
350 
351     /**
352      * Inform the notification manager about dismissal of specific notifications.
353      * <p>
354      * Use this if your listener has a user interface that allows the user to dismiss
355      * multiple notifications at once.
356      *
357      * @param keys Notifications to dismiss, or {@code null} to dismiss all.
358      *
359      * {@see #cancelNotification(String, String, int)}
360      */
cancelNotifications(String[] keys)361     public final void cancelNotifications(String[] keys) {
362         if (!isBound()) return;
363         try {
364             getNotificationInterface().cancelNotificationsFromListener(mWrapper, keys);
365         } catch (android.os.RemoteException ex) {
366             Log.v(TAG, "Unable to contact notification manager", ex);
367         }
368     }
369 
370     /**
371      * Inform the notification manager that these notifications have been viewed by the
372      * user. This should only be called when there is sufficient confidence that the user is
373      * looking at the notifications, such as when the notifications appear on the screen due to
374      * an explicit user interaction.
375      * @param keys Notifications to mark as seen.
376      */
setNotificationsShown(String[] keys)377     public final void setNotificationsShown(String[] keys) {
378         if (!isBound()) return;
379         try {
380             getNotificationInterface().setNotificationsShownFromListener(mWrapper, keys);
381         } catch (android.os.RemoteException ex) {
382             Log.v(TAG, "Unable to contact notification manager", ex);
383         }
384     }
385 
386     /**
387      * Sets the notification trim that will be received via {@link #onNotificationPosted}.
388      *
389      * <p>
390      * Setting a trim other than {@link #TRIM_FULL} enables listeners that don't need access to the
391      * full notification features right away to reduce their memory footprint. Full notifications
392      * can be requested on-demand via {@link #getActiveNotifications(int)}.
393      *
394      * <p>
395      * Set to {@link #TRIM_FULL} initially.
396      *
397      * @hide
398      *
399      * @param trim trim of the notifications to be passed via {@link #onNotificationPosted}.
400      *             See <code>TRIM_*</code> constants.
401      */
402     @SystemApi
setOnNotificationPostedTrim(int trim)403     public final void setOnNotificationPostedTrim(int trim) {
404         if (!isBound()) return;
405         try {
406             getNotificationInterface().setOnNotificationPostedTrimFromListener(mWrapper, trim);
407         } catch (RemoteException ex) {
408             Log.v(TAG, "Unable to contact notification manager", ex);
409         }
410     }
411 
412     /**
413      * Request the list of outstanding notifications (that is, those that are visible to the
414      * current user). Useful when you don't know what's already been posted.
415      *
416      * @return An array of active notifications, sorted in natural order.
417      */
getActiveNotifications()418     public StatusBarNotification[] getActiveNotifications() {
419         return getActiveNotifications(null, TRIM_FULL);
420     }
421 
422     /**
423      * Request the list of outstanding notifications (that is, those that are visible to the
424      * current user). Useful when you don't know what's already been posted.
425      *
426      * @hide
427      *
428      * @param trim trim of the notifications to be returned. See <code>TRIM_*</code> constants.
429      * @return An array of active notifications, sorted in natural order.
430      */
431     @SystemApi
getActiveNotifications(int trim)432     public StatusBarNotification[] getActiveNotifications(int trim) {
433         return getActiveNotifications(null, trim);
434     }
435 
436     /**
437      * Request one or more notifications by key. Useful if you have been keeping track of
438      * notifications but didn't want to retain the bits, and now need to go back and extract
439      * more data out of those notifications.
440      *
441      * @param keys the keys of the notifications to request
442      * @return An array of notifications corresponding to the requested keys, in the
443      * same order as the key list.
444      */
getActiveNotifications(String[] keys)445     public StatusBarNotification[] getActiveNotifications(String[] keys) {
446         return getActiveNotifications(keys, TRIM_FULL);
447     }
448 
449     /**
450      * Request one or more notifications by key. Useful if you have been keeping track of
451      * notifications but didn't want to retain the bits, and now need to go back and extract
452      * more data out of those notifications.
453      *
454      * @hide
455      *
456      * @param keys the keys of the notifications to request
457      * @param trim trim of the notifications to be returned. See <code>TRIM_*</code> constants.
458      * @return An array of notifications corresponding to the requested keys, in the
459      * same order as the key list.
460      */
461     @SystemApi
getActiveNotifications(String[] keys, int trim)462     public StatusBarNotification[] getActiveNotifications(String[] keys, int trim) {
463         if (!isBound())
464             return null;
465         try {
466             ParceledListSlice<StatusBarNotification> parceledList = getNotificationInterface()
467                     .getActiveNotificationsFromListener(mWrapper, keys, trim);
468             List<StatusBarNotification> list = parceledList.getList();
469             ArrayList<StatusBarNotification> corruptNotifications = null;
470             int N = list.size();
471             for (int i = 0; i < N; i++) {
472                 StatusBarNotification sbn = list.get(i);
473                 Notification notification = sbn.getNotification();
474                 try {
475                     Builder.rebuild(getContext(), notification);
476                     // convert icon metadata to legacy format for older clients
477                     createLegacyIconExtras(notification);
478                 } catch (IllegalArgumentException e) {
479                     if (corruptNotifications == null) {
480                         corruptNotifications = new ArrayList<>(N);
481                     }
482                     corruptNotifications.add(sbn);
483                     Log.w(TAG, "onNotificationPosted: can't rebuild notification from " +
484                             sbn.getPackageName());
485                 }
486             }
487             if (corruptNotifications != null) {
488                 list.removeAll(corruptNotifications);
489             }
490             return list.toArray(new StatusBarNotification[list.size()]);
491         } catch (android.os.RemoteException ex) {
492             Log.v(TAG, "Unable to contact notification manager", ex);
493         }
494         return null;
495     }
496 
497     /**
498      * Gets the set of hints representing current state.
499      *
500      * <p>
501      * The current state may differ from the requested state if the hint represents state
502      * shared across all listeners or a feature the notification host does not support or refuses
503      * to grant.
504      *
505      * @return Zero or more of the HINT_ constants.
506      */
getCurrentListenerHints()507     public final int getCurrentListenerHints() {
508         if (!isBound()) return 0;
509         try {
510             return getNotificationInterface().getHintsFromListener(mWrapper);
511         } catch (android.os.RemoteException ex) {
512             Log.v(TAG, "Unable to contact notification manager", ex);
513             return 0;
514         }
515     }
516 
517     /**
518      * Gets the current notification interruption filter active on the host.
519      *
520      * <p>
521      * The interruption filter defines which notifications are allowed to interrupt the user
522      * (e.g. via sound &amp; vibration) and is applied globally. Listeners can find out whether
523      * a specific notification matched the interruption filter via
524      * {@link Ranking#matchesInterruptionFilter()}.
525      * <p>
526      * The current filter may differ from the previously requested filter if the notification host
527      * does not support or refuses to apply the requested filter, or if another component changed
528      * the filter in the meantime.
529      * <p>
530      * Listen for updates using {@link #onInterruptionFilterChanged(int)}.
531      *
532      * @return One of the INTERRUPTION_FILTER_ constants, or INTERRUPTION_FILTER_UNKNOWN when
533      * unavailable.
534      */
getCurrentInterruptionFilter()535     public final int getCurrentInterruptionFilter() {
536         if (!isBound()) return INTERRUPTION_FILTER_UNKNOWN;
537         try {
538             return getNotificationInterface().getInterruptionFilterFromListener(mWrapper);
539         } catch (android.os.RemoteException ex) {
540             Log.v(TAG, "Unable to contact notification manager", ex);
541             return INTERRUPTION_FILTER_UNKNOWN;
542         }
543     }
544 
545     /**
546      * Sets the desired {@link #getCurrentListenerHints() listener hints}.
547      *
548      * <p>
549      * This is merely a request, the host may or may not choose to take action depending
550      * on other listener requests or other global state.
551      * <p>
552      * Listen for updates using {@link #onListenerHintsChanged(int)}.
553      *
554      * @param hints One or more of the HINT_ constants.
555      */
requestListenerHints(int hints)556     public final void requestListenerHints(int hints) {
557         if (!isBound()) return;
558         try {
559             getNotificationInterface().requestHintsFromListener(mWrapper, hints);
560         } catch (android.os.RemoteException ex) {
561             Log.v(TAG, "Unable to contact notification manager", ex);
562         }
563     }
564 
565     /**
566      * Sets the desired {@link #getCurrentInterruptionFilter() interruption filter}.
567      *
568      * <p>
569      * This is merely a request, the host may or may not choose to apply the requested
570      * interruption filter depending on other listener requests or other global state.
571      * <p>
572      * Listen for updates using {@link #onInterruptionFilterChanged(int)}.
573      *
574      * @param interruptionFilter One of the INTERRUPTION_FILTER_ constants.
575      */
requestInterruptionFilter(int interruptionFilter)576     public final void requestInterruptionFilter(int interruptionFilter) {
577         if (!isBound()) return;
578         try {
579             getNotificationInterface()
580                     .requestInterruptionFilterFromListener(mWrapper, interruptionFilter);
581         } catch (android.os.RemoteException ex) {
582             Log.v(TAG, "Unable to contact notification manager", ex);
583         }
584     }
585 
586     /**
587      * Returns current ranking information.
588      *
589      * <p>
590      * The returned object represents the current ranking snapshot and only
591      * applies for currently active notifications.
592      * <p>
593      * Generally you should use the RankingMap that is passed with events such
594      * as {@link #onNotificationPosted(StatusBarNotification, RankingMap)},
595      * {@link #onNotificationRemoved(StatusBarNotification, RankingMap)}, and
596      * so on. This method should only be used when needing access outside of
597      * such events, for example to retrieve the RankingMap right after
598      * initialization.
599      *
600      * @return A {@link RankingMap} object providing access to ranking information
601      */
getCurrentRanking()602     public RankingMap getCurrentRanking() {
603         return mRankingMap;
604     }
605 
606     @Override
onBind(Intent intent)607     public IBinder onBind(Intent intent) {
608         if (mWrapper == null) {
609             mWrapper = new INotificationListenerWrapper();
610         }
611         return mWrapper;
612     }
613 
isBound()614     private boolean isBound() {
615         if (mWrapper == null) {
616             Log.w(TAG, "Notification listener service not yet bound.");
617             return false;
618         }
619         return true;
620     }
621 
622     /**
623      * Directly register this service with the Notification Manager.
624      *
625      * <p>Only system services may use this call. It will fail for non-system callers.
626      * Apps should ask the user to add their listener in Settings.
627      *
628      * @param context Context required for accessing resources. Since this service isn't
629      *    launched as a real Service when using this method, a context has to be passed in.
630      * @param componentName the component that will consume the notification information
631      * @param currentUser the user to use as the stream filter
632      * @hide
633      */
634     @SystemApi
registerAsSystemService(Context context, ComponentName componentName, int currentUser)635     public void registerAsSystemService(Context context, ComponentName componentName,
636             int currentUser) throws RemoteException {
637         mSystemContext = context;
638         if (mWrapper == null) {
639             mWrapper = new INotificationListenerWrapper();
640         }
641         INotificationManager noMan = getNotificationInterface();
642         noMan.registerListener(mWrapper, componentName, currentUser);
643         mCurrentUser = currentUser;
644     }
645 
646     /**
647      * Directly unregister this service from the Notification Manager.
648      *
649      * <P>This method will fail for listeners that were not registered
650      * with (@link registerAsService).
651      * @hide
652      */
653     @SystemApi
unregisterAsSystemService()654     public void unregisterAsSystemService() throws RemoteException {
655         if (mWrapper != null) {
656             INotificationManager noMan = getNotificationInterface();
657             noMan.unregisterListener(mWrapper, mCurrentUser);
658         }
659     }
660 
661     /** Convert new-style Icons to legacy representations for pre-M clients. */
createLegacyIconExtras(Notification n)662     private void createLegacyIconExtras(Notification n) {
663         Icon smallIcon = n.getSmallIcon();
664         Icon largeIcon = n.getLargeIcon();
665         if (smallIcon != null && smallIcon.getType() == Icon.TYPE_RESOURCE) {
666             n.extras.putInt(Notification.EXTRA_SMALL_ICON, smallIcon.getResId());
667             n.icon = smallIcon.getResId();
668         }
669         if (largeIcon != null) {
670             Drawable d = largeIcon.loadDrawable(getContext());
671             if (d != null && d instanceof BitmapDrawable) {
672                 final Bitmap largeIconBits = ((BitmapDrawable) d).getBitmap();
673                 n.extras.putParcelable(Notification.EXTRA_LARGE_ICON, largeIconBits);
674                 n.largeIcon = largeIconBits;
675             }
676         }
677     }
678 
679     private class INotificationListenerWrapper extends INotificationListener.Stub {
680         @Override
onNotificationPosted(IStatusBarNotificationHolder sbnHolder, NotificationRankingUpdate update)681         public void onNotificationPosted(IStatusBarNotificationHolder sbnHolder,
682                 NotificationRankingUpdate update) {
683             StatusBarNotification sbn;
684             try {
685                 sbn = sbnHolder.get();
686             } catch (RemoteException e) {
687                 Log.w(TAG, "onNotificationPosted: Error receiving StatusBarNotification", e);
688                 return;
689             }
690 
691             try {
692                 Notification.Builder.rebuild(getContext(), sbn.getNotification());
693                 // convert icon metadata to legacy format for older clients
694                 createLegacyIconExtras(sbn.getNotification());
695             } catch (IllegalArgumentException e) {
696                 // drop corrupt notification
697                 sbn = null;
698                 Log.w(TAG, "onNotificationPosted: can't rebuild notification from " +
699                         sbn.getPackageName());
700             }
701 
702             // protect subclass from concurrent modifications of (@link mNotificationKeys}.
703             synchronized (mWrapper) {
704                 applyUpdate(update);
705                 try {
706                     if (sbn != null) {
707                         NotificationListenerService.this.onNotificationPosted(sbn, mRankingMap);
708                     } else {
709                         // still pass along the ranking map, it may contain other information
710                         NotificationListenerService.this.onNotificationRankingUpdate(mRankingMap);
711                     }
712                 } catch (Throwable t) {
713                     Log.w(TAG, "Error running onNotificationPosted", t);
714                 }
715             }
716         }
717         @Override
onNotificationRemoved(IStatusBarNotificationHolder sbnHolder, NotificationRankingUpdate update)718         public void onNotificationRemoved(IStatusBarNotificationHolder sbnHolder,
719                 NotificationRankingUpdate update) {
720             StatusBarNotification sbn;
721             try {
722                 sbn = sbnHolder.get();
723             } catch (RemoteException e) {
724                 Log.w(TAG, "onNotificationRemoved: Error receiving StatusBarNotification", e);
725                 return;
726             }
727             // protect subclass from concurrent modifications of (@link mNotificationKeys}.
728             synchronized (mWrapper) {
729                 applyUpdate(update);
730                 try {
731                     NotificationListenerService.this.onNotificationRemoved(sbn, mRankingMap);
732                 } catch (Throwable t) {
733                     Log.w(TAG, "Error running onNotificationRemoved", t);
734                 }
735             }
736         }
737         @Override
onListenerConnected(NotificationRankingUpdate update)738         public void onListenerConnected(NotificationRankingUpdate update) {
739             // protect subclass from concurrent modifications of (@link mNotificationKeys}.
740             synchronized (mWrapper) {
741                 applyUpdate(update);
742                 try {
743                     NotificationListenerService.this.onListenerConnected();
744                 } catch (Throwable t) {
745                     Log.w(TAG, "Error running onListenerConnected", t);
746                 }
747             }
748         }
749         @Override
onNotificationRankingUpdate(NotificationRankingUpdate update)750         public void onNotificationRankingUpdate(NotificationRankingUpdate update)
751                 throws RemoteException {
752             // protect subclass from concurrent modifications of (@link mNotificationKeys}.
753             synchronized (mWrapper) {
754                 applyUpdate(update);
755                 try {
756                     NotificationListenerService.this.onNotificationRankingUpdate(mRankingMap);
757                 } catch (Throwable t) {
758                     Log.w(TAG, "Error running onNotificationRankingUpdate", t);
759                 }
760             }
761         }
762         @Override
onListenerHintsChanged(int hints)763         public void onListenerHintsChanged(int hints) throws RemoteException {
764             try {
765                 NotificationListenerService.this.onListenerHintsChanged(hints);
766             } catch (Throwable t) {
767                 Log.w(TAG, "Error running onListenerHintsChanged", t);
768             }
769         }
770 
771         @Override
onInterruptionFilterChanged(int interruptionFilter)772         public void onInterruptionFilterChanged(int interruptionFilter) throws RemoteException {
773             try {
774                 NotificationListenerService.this.onInterruptionFilterChanged(interruptionFilter);
775             } catch (Throwable t) {
776                 Log.w(TAG, "Error running onInterruptionFilterChanged", t);
777             }
778         }
779     }
780 
applyUpdate(NotificationRankingUpdate update)781     private void applyUpdate(NotificationRankingUpdate update) {
782         mRankingMap = new RankingMap(update);
783     }
784 
getContext()785     private Context getContext() {
786         if (mSystemContext != null) {
787             return mSystemContext;
788         }
789         return this;
790     }
791 
792     /**
793      * Stores ranking related information on a currently active notification.
794      *
795      * <p>
796      * Ranking objects aren't automatically updated as notification events
797      * occur. Instead, ranking information has to be retrieved again via the
798      * current {@link RankingMap}.
799      */
800     public static class Ranking {
801         /** Value signifying that the user has not expressed a per-app visibility override value.
802          * @hide */
803         public static final int VISIBILITY_NO_OVERRIDE = -1000;
804 
805         private String mKey;
806         private int mRank = -1;
807         private boolean mIsAmbient;
808         private boolean mMatchesInterruptionFilter;
809         private int mVisibilityOverride;
810 
Ranking()811         public Ranking() {}
812 
813         /**
814          * Returns the key of the notification this Ranking applies to.
815          */
getKey()816         public String getKey() {
817             return mKey;
818         }
819 
820         /**
821          * Returns the rank of the notification.
822          *
823          * @return the rank of the notification, that is the 0-based index in
824          *     the list of active notifications.
825          */
getRank()826         public int getRank() {
827             return mRank;
828         }
829 
830         /**
831          * Returns whether the notification is an ambient notification, that is
832          * a notification that doesn't require the user's immediate attention.
833          */
isAmbient()834         public boolean isAmbient() {
835             return mIsAmbient;
836         }
837 
838         /**
839          * Returns the user specificed visibility for the package that posted
840          * this notification, or
841          * {@link NotificationListenerService.Ranking#VISIBILITY_NO_OVERRIDE} if
842          * no such preference has been expressed.
843          * @hide
844          */
getVisibilityOverride()845         public int getVisibilityOverride() {
846             return mVisibilityOverride;
847         }
848 
849 
850         /**
851          * Returns whether the notification matches the user's interruption
852          * filter.
853          *
854          * @return {@code true} if the notification is allowed by the filter, or
855          * {@code false} if it is blocked.
856          */
matchesInterruptionFilter()857         public boolean matchesInterruptionFilter() {
858             return mMatchesInterruptionFilter;
859         }
860 
populate(String key, int rank, boolean isAmbient, boolean matchesInterruptionFilter, int visibilityOverride)861         private void populate(String key, int rank, boolean isAmbient,
862                 boolean matchesInterruptionFilter, int visibilityOverride) {
863             mKey = key;
864             mRank = rank;
865             mIsAmbient = isAmbient;
866             mMatchesInterruptionFilter = matchesInterruptionFilter;
867             mVisibilityOverride = visibilityOverride;
868         }
869     }
870 
871     /**
872      * Provides access to ranking information on currently active
873      * notifications.
874      *
875      * <p>
876      * Note that this object represents a ranking snapshot that only applies to
877      * notifications active at the time of retrieval.
878      */
879     public static class RankingMap implements Parcelable {
880         private final NotificationRankingUpdate mRankingUpdate;
881         private ArrayMap<String,Integer> mRanks;
882         private ArraySet<Object> mIntercepted;
883         private ArrayMap<String, Integer> mVisibilityOverrides;
884 
RankingMap(NotificationRankingUpdate rankingUpdate)885         private RankingMap(NotificationRankingUpdate rankingUpdate) {
886             mRankingUpdate = rankingUpdate;
887         }
888 
889         /**
890          * Request the list of notification keys in their current ranking
891          * order.
892          *
893          * @return An array of active notification keys, in their ranking order.
894          */
getOrderedKeys()895         public String[] getOrderedKeys() {
896             return mRankingUpdate.getOrderedKeys();
897         }
898 
899         /**
900          * Populates outRanking with ranking information for the notification
901          * with the given key.
902          *
903          * @return true if a valid key has been passed and outRanking has
904          *     been populated; false otherwise
905          */
getRanking(String key, Ranking outRanking)906         public boolean getRanking(String key, Ranking outRanking) {
907             int rank = getRank(key);
908             outRanking.populate(key, rank, isAmbient(key), !isIntercepted(key),
909                     getVisibilityOverride(key));
910             return rank >= 0;
911         }
912 
getRank(String key)913         private int getRank(String key) {
914             synchronized (this) {
915                 if (mRanks == null) {
916                     buildRanksLocked();
917                 }
918             }
919             Integer rank = mRanks.get(key);
920             return rank != null ? rank : -1;
921         }
922 
isAmbient(String key)923         private boolean isAmbient(String key) {
924             int firstAmbientIndex = mRankingUpdate.getFirstAmbientIndex();
925             if (firstAmbientIndex < 0) {
926                 return false;
927             }
928             int rank = getRank(key);
929             return rank >= 0 && rank >= firstAmbientIndex;
930         }
931 
isIntercepted(String key)932         private boolean isIntercepted(String key) {
933             synchronized (this) {
934                 if (mIntercepted == null) {
935                     buildInterceptedSetLocked();
936                 }
937             }
938             return mIntercepted.contains(key);
939         }
940 
getVisibilityOverride(String key)941         private int getVisibilityOverride(String key) {
942             synchronized (this) {
943                 if (mVisibilityOverrides == null) {
944                     buildVisibilityOverridesLocked();
945                 }
946             }
947             Integer overide = mVisibilityOverrides.get(key);
948             if (overide == null) {
949                 return Ranking.VISIBILITY_NO_OVERRIDE;
950             }
951             return overide.intValue();
952         }
953 
954         // Locked by 'this'
buildRanksLocked()955         private void buildRanksLocked() {
956             String[] orderedKeys = mRankingUpdate.getOrderedKeys();
957             mRanks = new ArrayMap<>(orderedKeys.length);
958             for (int i = 0; i < orderedKeys.length; i++) {
959                 String key = orderedKeys[i];
960                 mRanks.put(key, i);
961             }
962         }
963 
964         // Locked by 'this'
buildInterceptedSetLocked()965         private void buildInterceptedSetLocked() {
966             String[] dndInterceptedKeys = mRankingUpdate.getInterceptedKeys();
967             mIntercepted = new ArraySet<>(dndInterceptedKeys.length);
968             Collections.addAll(mIntercepted, dndInterceptedKeys);
969         }
970 
971         // Locked by 'this'
buildVisibilityOverridesLocked()972         private void buildVisibilityOverridesLocked() {
973             Bundle visibilityBundle = mRankingUpdate.getVisibilityOverrides();
974             mVisibilityOverrides = new ArrayMap<>(visibilityBundle.size());
975             for (String key: visibilityBundle.keySet()) {
976                mVisibilityOverrides.put(key, visibilityBundle.getInt(key));
977             }
978         }
979 
980         // ----------- Parcelable
981 
982         @Override
describeContents()983         public int describeContents() {
984             return 0;
985         }
986 
987         @Override
writeToParcel(Parcel dest, int flags)988         public void writeToParcel(Parcel dest, int flags) {
989             dest.writeParcelable(mRankingUpdate, flags);
990         }
991 
992         public static final Creator<RankingMap> CREATOR = new Creator<RankingMap>() {
993             @Override
994             public RankingMap createFromParcel(Parcel source) {
995                 NotificationRankingUpdate rankingUpdate = source.readParcelable(null);
996                 return new RankingMap(rankingUpdate);
997             }
998 
999             @Override
1000             public RankingMap[] newArray(int size) {
1001                 return new RankingMap[size];
1002             }
1003         };
1004     }
1005 }
1006