• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 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 static java.lang.annotation.RetentionPolicy.SOURCE;
20 
21 import android.annotation.IntDef;
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.annotation.SdkConstant;
25 import android.annotation.SystemApi;
26 import android.annotation.TestApi;
27 import android.app.Notification;
28 import android.app.NotificationChannel;
29 import android.app.NotificationManager;
30 import android.app.admin.DevicePolicyManager;
31 import android.content.ComponentName;
32 import android.content.Context;
33 import android.content.Intent;
34 import android.os.Handler;
35 import android.os.IBinder;
36 import android.os.Looper;
37 import android.os.Message;
38 import android.os.RemoteException;
39 import android.util.Log;
40 
41 import com.android.internal.os.SomeArgs;
42 
43 import java.lang.annotation.Retention;
44 import java.util.List;
45 
46 /**
47  * A service that helps the user manage notifications.
48  * <p>
49  * Only one notification assistant can be active at a time. Unlike notification listener services,
50  * assistant services can additionally modify certain aspects about notifications
51  * (see {@link Adjustment}) before they are posted.
52  *<p>
53  * A note about managed profiles: Unlike {@link NotificationListenerService listener services},
54  * NotificationAssistantServices are allowed to run in managed profiles
55  * (see {@link DevicePolicyManager#isManagedProfile(ComponentName)}), so they can access the
56  * information they need to create good {@link Adjustment adjustments}. To maintain the contract
57  * with {@link NotificationListenerService}, an assistant service will receive all of the
58  * callbacks from {@link NotificationListenerService} for the current user, managed profiles of
59  * that user, and ones that affect all users. However,
60  * {@link #onNotificationEnqueued(StatusBarNotification)} will only be called for notifications
61  * sent to the current user, and {@link Adjustment adjuments} will only be accepted for the
62  * current user.
63  * <p>
64  *     All callbacks are called on the main thread.
65  * </p>
66  * @hide
67  */
68 @SystemApi
69 @TestApi
70 public abstract class NotificationAssistantService extends NotificationListenerService {
71     private static final String TAG = "NotificationAssistants";
72 
73     /** @hide */
74     @Retention(SOURCE)
75     @IntDef({SOURCE_FROM_APP, SOURCE_FROM_ASSISTANT})
76     public @interface Source {}
77 
78     /**
79      * To indicate an adjustment is from an app.
80      */
81     public static final int SOURCE_FROM_APP = 0;
82     /**
83      * To indicate an adjustment is from a {@link NotificationAssistantService}.
84      */
85     public static final int SOURCE_FROM_ASSISTANT = 1;
86 
87     /**
88      * The {@link Intent} that must be declared as handled by the service.
89      */
90     @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
91     public static final String SERVICE_INTERFACE
92             = "android.service.notification.NotificationAssistantService";
93 
94     /**
95      * @hide
96      */
97     protected Handler mHandler;
98 
99     @Override
attachBaseContext(Context base)100     protected void attachBaseContext(Context base) {
101         super.attachBaseContext(base);
102         mHandler = new MyHandler(getContext().getMainLooper());
103     }
104 
105     @Override
onBind(@ullable Intent intent)106     public final @NonNull IBinder onBind(@Nullable Intent intent) {
107         if (mWrapper == null) {
108             mWrapper = new NotificationAssistantServiceWrapper();
109         }
110         return mWrapper;
111     }
112 
113     /**
114      * A notification was snoozed until a context. For use with
115      * {@link Adjustment#KEY_SNOOZE_CRITERIA}. When the device reaches the given context, the
116      * assistant should restore the notification with {@link #unsnoozeNotification(String)}.
117      *
118      * @param sbn the notification to snooze
119      * @param snoozeCriterionId the {@link SnoozeCriterion#getId()} representing a device context.
120      */
onNotificationSnoozedUntilContext(@onNull StatusBarNotification sbn, @NonNull String snoozeCriterionId)121     abstract public void onNotificationSnoozedUntilContext(@NonNull StatusBarNotification sbn,
122             @NonNull String snoozeCriterionId);
123 
124     /**
125      * A notification was posted by an app. Called before post.
126      *
127      * <p>Note: this method is only called if you don't override
128      * {@link #onNotificationEnqueued(StatusBarNotification, NotificationChannel)}.</p>
129      *
130      * @param sbn the new notification
131      * @return an adjustment or null to take no action, within 100ms.
132      */
onNotificationEnqueued(@onNull StatusBarNotification sbn)133     abstract public @Nullable Adjustment onNotificationEnqueued(@NonNull StatusBarNotification sbn);
134 
135     /**
136      * A notification was posted by an app. Called before post.
137      *
138      * @param sbn the new notification
139      * @param channel the channel the notification was posted to
140      * @return an adjustment or null to take no action, within 100ms.
141      */
onNotificationEnqueued(@onNull StatusBarNotification sbn, @NonNull NotificationChannel channel)142     public @Nullable Adjustment onNotificationEnqueued(@NonNull StatusBarNotification sbn,
143             @NonNull NotificationChannel channel) {
144         return onNotificationEnqueued(sbn);
145     }
146 
147     /**
148      * Implement this method to learn when notifications are removed, how they were interacted with
149      * before removal, and why they were removed.
150      * <p>
151      * This might occur because the user has dismissed the notification using system UI (or another
152      * notification listener) or because the app has withdrawn the notification.
153      * <p>
154      * NOTE: The {@link StatusBarNotification} object you receive will be "light"; that is, the
155      * result from {@link StatusBarNotification#getNotification} may be missing some heavyweight
156      * fields such as {@link android.app.Notification#contentView} and
157      * {@link android.app.Notification#largeIcon}. However, all other fields on
158      * {@link StatusBarNotification}, sufficient to match this call with a prior call to
159      * {@link #onNotificationPosted(StatusBarNotification)}, will be intact.
160      *
161      ** @param sbn A data structure encapsulating at least the original information (tag and id)
162      *            and source (package name) used to post the {@link android.app.Notification} that
163      *            was just removed.
164      * @param rankingMap The current ranking map that can be used to retrieve ranking information
165      *                   for active notifications.
166      * @param stats Stats about how the user interacted with the notification before it was removed.
167      * @param reason see {@link #REASON_LISTENER_CANCEL}, etc.
168      */
169     @Override
onNotificationRemoved(@onNull StatusBarNotification sbn, @NonNull RankingMap rankingMap, @NonNull NotificationStats stats, int reason)170     public void onNotificationRemoved(@NonNull StatusBarNotification sbn,
171             @NonNull RankingMap rankingMap,
172             @NonNull NotificationStats stats, int reason) {
173         onNotificationRemoved(sbn, rankingMap, reason);
174     }
175 
176     /**
177      * Implement this to know when a user has seen notifications, as triggered by
178      * {@link #setNotificationsShown(String[])}.
179      */
onNotificationsSeen(@onNull List<String> keys)180     public void onNotificationsSeen(@NonNull List<String> keys) {
181 
182     }
183 
184     /**
185      * Implement this to know when the notification panel is revealed
186      *
187      * @param items Number of notifications on the panel at time of opening
188      */
onPanelRevealed(int items)189     public void onPanelRevealed(int items) {
190 
191     }
192 
193     /**
194      * Implement this to know when the notification panel is hidden
195      */
onPanelHidden()196     public void onPanelHidden() {
197 
198     }
199 
200     /**
201      * Implement this to know when a notification becomes visible or hidden from the user.
202      *
203      * @param key the notification key
204      * @param isVisible whether the notification is visible.
205      */
onNotificationVisibilityChanged(@onNull String key, boolean isVisible)206     public void onNotificationVisibilityChanged(@NonNull String key, boolean isVisible) {
207 
208     }
209 
210     /**
211      * Implement this to know when a notification change (expanded / collapsed) is visible to user.
212      *
213      * @param key the notification key
214      * @param isUserAction whether the expanded change is caused by user action.
215      * @param isExpanded whether the notification is expanded.
216      */
onNotificationExpansionChanged( @onNull String key, boolean isUserAction, boolean isExpanded)217     public void onNotificationExpansionChanged(
218             @NonNull String key, boolean isUserAction, boolean isExpanded) {}
219 
220     /**
221      * Implement this to know when a direct reply is sent from a notification.
222      * @param key the notification key
223      */
onNotificationDirectReplied(@onNull String key)224     public void onNotificationDirectReplied(@NonNull String key) {}
225 
226     /**
227      * Implement this to know when a suggested reply is sent.
228      * @param key the notification key
229      * @param reply the reply that is just sent
230      * @param source the source that provided the reply, e.g. SOURCE_FROM_APP
231      */
onSuggestedReplySent(@onNull String key, @NonNull CharSequence reply, @Source int source)232     public void onSuggestedReplySent(@NonNull String key, @NonNull CharSequence reply,
233             @Source int source) {
234     }
235 
236     /**
237      * Implement this to know when an action is clicked.
238      * @param key the notification key
239      * @param action the action that is just clicked
240      * @param source the source that provided the action, e.g. SOURCE_FROM_APP
241      */
onActionInvoked(@onNull String key, @NonNull Notification.Action action, @Source int source)242     public void onActionInvoked(@NonNull String key, @NonNull Notification.Action action,
243             @Source int source) {
244     }
245 
246     /**
247      * Implement this to know when a user has changed which features of
248      * their notifications the assistant can modify.
249      * <p> Query {@link NotificationManager#getAllowedAssistantAdjustments()} to see what
250      * {@link Adjustment adjustments} you are currently allowed to make.</p>
251      */
onAllowedAdjustmentsChanged()252     public void onAllowedAdjustmentsChanged() {
253     }
254 
255     /**
256      * Updates a notification.  N.B. this won’t cause
257      * an existing notification to alert, but might allow a future update to
258      * this notification to alert.
259      *
260      * @param adjustment the adjustment with an explanation
261      */
adjustNotification(@onNull Adjustment adjustment)262     public final void adjustNotification(@NonNull Adjustment adjustment) {
263         if (!isBound()) return;
264         try {
265             setAdjustmentIssuer(adjustment);
266             getNotificationInterface().applyEnqueuedAdjustmentFromAssistant(mWrapper, adjustment);
267         } catch (android.os.RemoteException ex) {
268             Log.v(TAG, "Unable to contact notification manager", ex);
269             throw ex.rethrowFromSystemServer();
270         }
271     }
272 
273     /**
274      * Updates existing notifications. Re-ranking won't occur until all adjustments are applied.
275      * N.B. this won’t cause an existing notification to alert, but might allow a future update to
276      * these notifications to alert.
277      *
278      * @param adjustments a list of adjustments with explanations
279      */
adjustNotifications(@onNull List<Adjustment> adjustments)280     public final void adjustNotifications(@NonNull List<Adjustment> adjustments) {
281         if (!isBound()) return;
282         try {
283             for (Adjustment adjustment : adjustments) {
284                 setAdjustmentIssuer(adjustment);
285             }
286             getNotificationInterface().applyAdjustmentsFromAssistant(mWrapper, adjustments);
287         } catch (android.os.RemoteException ex) {
288             Log.v(TAG, "Unable to contact notification manager", ex);
289             throw ex.rethrowFromSystemServer();
290         }
291     }
292 
293     /**
294      * Inform the notification manager about un-snoozing a specific notification.
295      * <p>
296      * This should only be used for notifications snoozed because of a contextual snooze suggestion
297      * you provided via {@link Adjustment#KEY_SNOOZE_CRITERIA}. Once un-snoozed, you will get a
298      * {@link #onNotificationPosted(StatusBarNotification, RankingMap)} callback for the
299      * notification.
300      * @param key The key of the notification to snooze
301      */
unsnoozeNotification(@onNull String key)302     public final void unsnoozeNotification(@NonNull String key) {
303         if (!isBound()) return;
304         try {
305             getNotificationInterface().unsnoozeNotificationFromAssistant(mWrapper, key);
306         } catch (android.os.RemoteException ex) {
307             Log.v(TAG, "Unable to contact notification manager", ex);
308         }
309     }
310 
311     private class NotificationAssistantServiceWrapper extends NotificationListenerWrapper {
312         @Override
onNotificationEnqueuedWithChannel(IStatusBarNotificationHolder sbnHolder, NotificationChannel channel)313         public void onNotificationEnqueuedWithChannel(IStatusBarNotificationHolder sbnHolder,
314                 NotificationChannel channel) {
315             StatusBarNotification sbn;
316             try {
317                 sbn = sbnHolder.get();
318             } catch (RemoteException e) {
319                 Log.w(TAG, "onNotificationEnqueued: Error receiving StatusBarNotification", e);
320                 return;
321             }
322             if (sbn == null) {
323                 Log.w(TAG, "onNotificationEnqueuedWithChannel: "
324                         + "Error receiving StatusBarNotification");
325                 return;
326             }
327 
328             SomeArgs args = SomeArgs.obtain();
329             args.arg1 = sbn;
330             args.arg2 = channel;
331             mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_ENQUEUED,
332                     args).sendToTarget();
333         }
334 
335         @Override
onNotificationSnoozedUntilContext( IStatusBarNotificationHolder sbnHolder, String snoozeCriterionId)336         public void onNotificationSnoozedUntilContext(
337                 IStatusBarNotificationHolder sbnHolder, String snoozeCriterionId) {
338             StatusBarNotification sbn;
339             try {
340                 sbn = sbnHolder.get();
341             } catch (RemoteException e) {
342                 Log.w(TAG, "onNotificationSnoozed: Error receiving StatusBarNotification", e);
343                 return;
344             }
345             if (sbn == null) {
346                 Log.w(TAG, "onNotificationSnoozed: Error receiving StatusBarNotification");
347                 return;
348             }
349 
350             SomeArgs args = SomeArgs.obtain();
351             args.arg1 = sbn;
352             args.arg2 = snoozeCriterionId;
353             mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_SNOOZED,
354                     args).sendToTarget();
355         }
356 
357         @Override
onNotificationsSeen(List<String> keys)358         public void onNotificationsSeen(List<String> keys) {
359             SomeArgs args = SomeArgs.obtain();
360             args.arg1 = keys;
361             mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATIONS_SEEN,
362                     args).sendToTarget();
363         }
364 
365         @Override
onPanelRevealed(int items)366         public void onPanelRevealed(int items) {
367             SomeArgs args = SomeArgs.obtain();
368             args.argi1 = items;
369             mHandler.obtainMessage(MyHandler.MSG_ON_PANEL_REVEALED,
370                     args).sendToTarget();
371         }
372 
373         @Override
onPanelHidden()374         public void onPanelHidden() {
375             SomeArgs args = SomeArgs.obtain();
376             mHandler.obtainMessage(MyHandler.MSG_ON_PANEL_HIDDEN,
377                     args).sendToTarget();
378         }
379 
380         @Override
onNotificationVisibilityChanged(String key, boolean isVisible)381         public void onNotificationVisibilityChanged(String key, boolean isVisible) {
382             SomeArgs args = SomeArgs.obtain();
383             args.arg1 = key;
384             args.argi1 = isVisible ? 1 : 0;
385             mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_VISIBILITY_CHANGED,
386                     args).sendToTarget();
387         }
388 
389         @Override
onNotificationExpansionChanged(String key, boolean isUserAction, boolean isExpanded)390         public void onNotificationExpansionChanged(String key, boolean isUserAction,
391                 boolean isExpanded) {
392             SomeArgs args = SomeArgs.obtain();
393             args.arg1 = key;
394             args.argi1 = isUserAction ? 1 : 0;
395             args.argi2 = isExpanded ? 1 : 0;
396             mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_EXPANSION_CHANGED, args)
397                     .sendToTarget();
398         }
399 
400         @Override
onNotificationDirectReply(String key)401         public void onNotificationDirectReply(String key) {
402             SomeArgs args = SomeArgs.obtain();
403             args.arg1 = key;
404             mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_DIRECT_REPLY_SENT, args)
405                     .sendToTarget();
406         }
407 
408         @Override
onSuggestedReplySent(String key, CharSequence reply, int source)409         public void onSuggestedReplySent(String key, CharSequence reply, int source) {
410             SomeArgs args = SomeArgs.obtain();
411             args.arg1 = key;
412             args.arg2 = reply;
413             args.argi2 = source;
414             mHandler.obtainMessage(MyHandler.MSG_ON_SUGGESTED_REPLY_SENT, args).sendToTarget();
415         }
416 
417         @Override
onActionClicked(String key, Notification.Action action, int source)418         public void onActionClicked(String key, Notification.Action action, int source) {
419             SomeArgs args = SomeArgs.obtain();
420             args.arg1 = key;
421             args.arg2 = action;
422             args.argi2 = source;
423             mHandler.obtainMessage(MyHandler.MSG_ON_ACTION_INVOKED, args).sendToTarget();
424         }
425 
426         @Override
onAllowedAdjustmentsChanged()427         public void onAllowedAdjustmentsChanged() {
428             mHandler.obtainMessage(MyHandler.MSG_ON_ALLOWED_ADJUSTMENTS_CHANGED).sendToTarget();
429         }
430     }
431 
setAdjustmentIssuer(@ullable Adjustment adjustment)432     private void setAdjustmentIssuer(@Nullable Adjustment adjustment) {
433         if (adjustment != null) {
434             adjustment.setIssuer(getOpPackageName() + "/" + getClass().getName());
435         }
436     }
437 
438     private final class MyHandler extends Handler {
439         public static final int MSG_ON_NOTIFICATION_ENQUEUED = 1;
440         public static final int MSG_ON_NOTIFICATION_SNOOZED = 2;
441         public static final int MSG_ON_NOTIFICATIONS_SEEN = 3;
442         public static final int MSG_ON_NOTIFICATION_EXPANSION_CHANGED = 4;
443         public static final int MSG_ON_NOTIFICATION_DIRECT_REPLY_SENT = 5;
444         public static final int MSG_ON_SUGGESTED_REPLY_SENT = 6;
445         public static final int MSG_ON_ACTION_INVOKED = 7;
446         public static final int MSG_ON_ALLOWED_ADJUSTMENTS_CHANGED = 8;
447         public static final int MSG_ON_PANEL_REVEALED = 9;
448         public static final int MSG_ON_PANEL_HIDDEN = 10;
449         public static final int MSG_ON_NOTIFICATION_VISIBILITY_CHANGED = 11;
450 
MyHandler(Looper looper)451         public MyHandler(Looper looper) {
452             super(looper, null, false);
453         }
454 
455         @Override
handleMessage(Message msg)456         public void handleMessage(Message msg) {
457             switch (msg.what) {
458                 case MSG_ON_NOTIFICATION_ENQUEUED: {
459                     SomeArgs args = (SomeArgs) msg.obj;
460                     StatusBarNotification sbn = (StatusBarNotification) args.arg1;
461                     NotificationChannel channel = (NotificationChannel) args.arg2;
462                     args.recycle();
463                     Adjustment adjustment = onNotificationEnqueued(sbn, channel);
464                     setAdjustmentIssuer(adjustment);
465                     if (adjustment != null) {
466                         if (!isBound()) {
467                             Log.w(TAG, "MSG_ON_NOTIFICATION_ENQUEUED: service not bound, skip.");
468                             return;
469                         }
470                         try {
471                             getNotificationInterface().applyEnqueuedAdjustmentFromAssistant(
472                                     mWrapper, adjustment);
473                         } catch (android.os.RemoteException ex) {
474                             Log.v(TAG, "Unable to contact notification manager", ex);
475                             throw ex.rethrowFromSystemServer();
476                         } catch (SecurityException e) {
477                             // app cannot catch and recover from this, so do on their behalf
478                             Log.w(TAG, "Enqueue adjustment failed; no longer connected", e);
479                         }
480                     }
481                     break;
482                 }
483                 case MSG_ON_NOTIFICATION_SNOOZED: {
484                     SomeArgs args = (SomeArgs) msg.obj;
485                     StatusBarNotification sbn = (StatusBarNotification) args.arg1;
486                     String snoozeCriterionId = (String) args.arg2;
487                     args.recycle();
488                     onNotificationSnoozedUntilContext(sbn, snoozeCriterionId);
489                     break;
490                 }
491                 case MSG_ON_NOTIFICATIONS_SEEN: {
492                     SomeArgs args = (SomeArgs) msg.obj;
493                     List<String> keys = (List<String>) args.arg1;
494                     args.recycle();
495                     onNotificationsSeen(keys);
496                     break;
497                 }
498                 case MSG_ON_NOTIFICATION_EXPANSION_CHANGED: {
499                     SomeArgs args = (SomeArgs) msg.obj;
500                     String key = (String) args.arg1;
501                     boolean isUserAction = args.argi1 == 1;
502                     boolean isExpanded = args.argi2 == 1;
503                     args.recycle();
504                     onNotificationExpansionChanged(key, isUserAction, isExpanded);
505                     break;
506                 }
507                 case MSG_ON_NOTIFICATION_DIRECT_REPLY_SENT: {
508                     SomeArgs args = (SomeArgs) msg.obj;
509                     String key = (String) args.arg1;
510                     args.recycle();
511                     onNotificationDirectReplied(key);
512                     break;
513                 }
514                 case MSG_ON_SUGGESTED_REPLY_SENT: {
515                     SomeArgs args = (SomeArgs) msg.obj;
516                     String key = (String) args.arg1;
517                     CharSequence reply = (CharSequence) args.arg2;
518                     int source = args.argi2;
519                     args.recycle();
520                     onSuggestedReplySent(key, reply, source);
521                     break;
522                 }
523                 case MSG_ON_ACTION_INVOKED: {
524                     SomeArgs args = (SomeArgs) msg.obj;
525                     String key = (String) args.arg1;
526                     Notification.Action action = (Notification.Action) args.arg2;
527                     int source = args.argi2;
528                     args.recycle();
529                     onActionInvoked(key, action, source);
530                     break;
531                 }
532                 case MSG_ON_ALLOWED_ADJUSTMENTS_CHANGED: {
533                     onAllowedAdjustmentsChanged();
534                     break;
535                 }
536                 case MSG_ON_PANEL_REVEALED: {
537                     SomeArgs args = (SomeArgs) msg.obj;
538                     int items = args.argi1;
539                     args.recycle();
540                     onPanelRevealed(items);
541                     break;
542                 }
543                 case MSG_ON_PANEL_HIDDEN: {
544                     onPanelHidden();
545                     break;
546                 }
547                 case MSG_ON_NOTIFICATION_VISIBILITY_CHANGED: {
548                     SomeArgs args = (SomeArgs) msg.obj;
549                     String key = (String) args.arg1;
550                     boolean isVisible = args.argi1 == 1;
551                     args.recycle();
552                     onNotificationVisibilityChanged(key, isVisible);
553                     break;
554                 }
555             }
556         }
557     }
558 }
559