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