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 a notification change (expanded / collapsed) is visible to user.
186      *
187      * @param key the notification key
188      * @param isUserAction whether the expanded change is caused by user action.
189      * @param isExpanded whether the notification is expanded.
190      */
onNotificationExpansionChanged( @onNull String key, boolean isUserAction, boolean isExpanded)191     public void onNotificationExpansionChanged(
192             @NonNull String key, boolean isUserAction, boolean isExpanded) {}
193 
194     /**
195      * Implement this to know when a direct reply is sent from a notification.
196      * @param key the notification key
197      */
onNotificationDirectReplied(@onNull String key)198     public void onNotificationDirectReplied(@NonNull String key) {}
199 
200     /**
201      * Implement this to know when a suggested reply is sent.
202      * @param key the notification key
203      * @param reply the reply that is just sent
204      * @param source the source that provided the reply, e.g. SOURCE_FROM_APP
205      */
onSuggestedReplySent(@onNull String key, @NonNull CharSequence reply, @Source int source)206     public void onSuggestedReplySent(@NonNull String key, @NonNull CharSequence reply,
207             @Source int source) {
208     }
209 
210     /**
211      * Implement this to know when an action is clicked.
212      * @param key the notification key
213      * @param action the action that is just clicked
214      * @param source the source that provided the action, e.g. SOURCE_FROM_APP
215      */
onActionInvoked(@onNull String key, @NonNull Notification.Action action, @Source int source)216     public void onActionInvoked(@NonNull String key, @NonNull Notification.Action action,
217             @Source int source) {
218     }
219 
220     /**
221      * Implement this to know when a user has changed which features of
222      * their notifications the assistant can modify.
223      * <p> Query {@link NotificationManager#getAllowedAssistantAdjustments()} to see what
224      * {@link Adjustment adjustments} you are currently allowed to make.</p>
225      */
onAllowedAdjustmentsChanged()226     public void onAllowedAdjustmentsChanged() {
227     }
228 
229     /**
230      * Updates a notification.  N.B. this won’t cause
231      * an existing notification to alert, but might allow a future update to
232      * this notification to alert.
233      *
234      * @param adjustment the adjustment with an explanation
235      */
adjustNotification(@onNull Adjustment adjustment)236     public final void adjustNotification(@NonNull Adjustment adjustment) {
237         if (!isBound()) return;
238         try {
239             setAdjustmentIssuer(adjustment);
240             getNotificationInterface().applyEnqueuedAdjustmentFromAssistant(mWrapper, adjustment);
241         } catch (android.os.RemoteException ex) {
242             Log.v(TAG, "Unable to contact notification manager", ex);
243             throw ex.rethrowFromSystemServer();
244         }
245     }
246 
247     /**
248      * Updates existing notifications. Re-ranking won't occur until all adjustments are applied.
249      * N.B. this won’t cause an existing notification to alert, but might allow a future update to
250      * these notifications to alert.
251      *
252      * @param adjustments a list of adjustments with explanations
253      */
adjustNotifications(@onNull List<Adjustment> adjustments)254     public final void adjustNotifications(@NonNull List<Adjustment> adjustments) {
255         if (!isBound()) return;
256         try {
257             for (Adjustment adjustment : adjustments) {
258                 setAdjustmentIssuer(adjustment);
259             }
260             getNotificationInterface().applyAdjustmentsFromAssistant(mWrapper, adjustments);
261         } catch (android.os.RemoteException ex) {
262             Log.v(TAG, "Unable to contact notification manager", ex);
263             throw ex.rethrowFromSystemServer();
264         }
265     }
266 
267     /**
268      * Inform the notification manager about un-snoozing a specific notification.
269      * <p>
270      * This should only be used for notifications snoozed because of a contextual snooze suggestion
271      * you provided via {@link Adjustment#KEY_SNOOZE_CRITERIA}. Once un-snoozed, you will get a
272      * {@link #onNotificationPosted(StatusBarNotification, RankingMap)} callback for the
273      * notification.
274      * @param key The key of the notification to snooze
275      */
unsnoozeNotification(@onNull String key)276     public final void unsnoozeNotification(@NonNull String key) {
277         if (!isBound()) return;
278         try {
279             getNotificationInterface().unsnoozeNotificationFromAssistant(mWrapper, key);
280         } catch (android.os.RemoteException ex) {
281             Log.v(TAG, "Unable to contact notification manager", ex);
282         }
283     }
284 
285     private class NotificationAssistantServiceWrapper extends NotificationListenerWrapper {
286         @Override
onNotificationEnqueuedWithChannel(IStatusBarNotificationHolder sbnHolder, NotificationChannel channel)287         public void onNotificationEnqueuedWithChannel(IStatusBarNotificationHolder sbnHolder,
288                 NotificationChannel channel) {
289             StatusBarNotification sbn;
290             try {
291                 sbn = sbnHolder.get();
292             } catch (RemoteException e) {
293                 Log.w(TAG, "onNotificationEnqueued: Error receiving StatusBarNotification", e);
294                 return;
295             }
296 
297             SomeArgs args = SomeArgs.obtain();
298             args.arg1 = sbn;
299             args.arg2 = channel;
300             mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_ENQUEUED,
301                     args).sendToTarget();
302         }
303 
304         @Override
onNotificationSnoozedUntilContext( IStatusBarNotificationHolder sbnHolder, String snoozeCriterionId)305         public void onNotificationSnoozedUntilContext(
306                 IStatusBarNotificationHolder sbnHolder, String snoozeCriterionId) {
307             StatusBarNotification sbn;
308             try {
309                 sbn = sbnHolder.get();
310             } catch (RemoteException e) {
311                 Log.w(TAG, "onNotificationSnoozed: Error receiving StatusBarNotification", e);
312                 return;
313             }
314 
315             SomeArgs args = SomeArgs.obtain();
316             args.arg1 = sbn;
317             args.arg2 = snoozeCriterionId;
318             mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_SNOOZED,
319                     args).sendToTarget();
320         }
321 
322         @Override
onNotificationsSeen(List<String> keys)323         public void onNotificationsSeen(List<String> keys) {
324             SomeArgs args = SomeArgs.obtain();
325             args.arg1 = keys;
326             mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATIONS_SEEN,
327                     args).sendToTarget();
328         }
329 
330         @Override
onNotificationExpansionChanged(String key, boolean isUserAction, boolean isExpanded)331         public void onNotificationExpansionChanged(String key, boolean isUserAction,
332                 boolean isExpanded) {
333             SomeArgs args = SomeArgs.obtain();
334             args.arg1 = key;
335             args.argi1 = isUserAction ? 1 : 0;
336             args.argi2 = isExpanded ? 1 : 0;
337             mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_EXPANSION_CHANGED, args)
338                     .sendToTarget();
339         }
340 
341         @Override
onNotificationDirectReply(String key)342         public void onNotificationDirectReply(String key) {
343             SomeArgs args = SomeArgs.obtain();
344             args.arg1 = key;
345             mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_DIRECT_REPLY_SENT, args)
346                     .sendToTarget();
347         }
348 
349         @Override
onSuggestedReplySent(String key, CharSequence reply, int source)350         public void onSuggestedReplySent(String key, CharSequence reply, int source) {
351             SomeArgs args = SomeArgs.obtain();
352             args.arg1 = key;
353             args.arg2 = reply;
354             args.argi2 = source;
355             mHandler.obtainMessage(MyHandler.MSG_ON_SUGGESTED_REPLY_SENT, args).sendToTarget();
356         }
357 
358         @Override
onActionClicked(String key, Notification.Action action, int source)359         public void onActionClicked(String key, Notification.Action action, int source) {
360             SomeArgs args = SomeArgs.obtain();
361             args.arg1 = key;
362             args.arg2 = action;
363             args.argi2 = source;
364             mHandler.obtainMessage(MyHandler.MSG_ON_ACTION_INVOKED, args).sendToTarget();
365         }
366 
367         @Override
onAllowedAdjustmentsChanged()368         public void onAllowedAdjustmentsChanged() {
369             mHandler.obtainMessage(MyHandler.MSG_ON_ALLOWED_ADJUSTMENTS_CHANGED).sendToTarget();
370         }
371     }
372 
setAdjustmentIssuer(@ullable Adjustment adjustment)373     private void setAdjustmentIssuer(@Nullable Adjustment adjustment) {
374         if (adjustment != null) {
375             adjustment.setIssuer(getOpPackageName() + "/" + getClass().getName());
376         }
377     }
378 
379     private final class MyHandler extends Handler {
380         public static final int MSG_ON_NOTIFICATION_ENQUEUED = 1;
381         public static final int MSG_ON_NOTIFICATION_SNOOZED = 2;
382         public static final int MSG_ON_NOTIFICATIONS_SEEN = 3;
383         public static final int MSG_ON_NOTIFICATION_EXPANSION_CHANGED = 4;
384         public static final int MSG_ON_NOTIFICATION_DIRECT_REPLY_SENT = 5;
385         public static final int MSG_ON_SUGGESTED_REPLY_SENT = 6;
386         public static final int MSG_ON_ACTION_INVOKED = 7;
387         public static final int MSG_ON_ALLOWED_ADJUSTMENTS_CHANGED = 8;
388 
MyHandler(Looper looper)389         public MyHandler(Looper looper) {
390             super(looper, null, false);
391         }
392 
393         @Override
handleMessage(Message msg)394         public void handleMessage(Message msg) {
395             switch (msg.what) {
396                 case MSG_ON_NOTIFICATION_ENQUEUED: {
397                     SomeArgs args = (SomeArgs) msg.obj;
398                     StatusBarNotification sbn = (StatusBarNotification) args.arg1;
399                     NotificationChannel channel = (NotificationChannel) args.arg2;
400                     args.recycle();
401                     Adjustment adjustment = onNotificationEnqueued(sbn, channel);
402                     setAdjustmentIssuer(adjustment);
403                     if (adjustment != null) {
404                         if (!isBound()) {
405                             Log.w(TAG, "MSG_ON_NOTIFICATION_ENQUEUED: service not bound, skip.");
406                             return;
407                         }
408                         try {
409                             getNotificationInterface().applyEnqueuedAdjustmentFromAssistant(
410                                     mWrapper, adjustment);
411                         } catch (android.os.RemoteException ex) {
412                             Log.v(TAG, "Unable to contact notification manager", ex);
413                             throw ex.rethrowFromSystemServer();
414                         } catch (SecurityException e) {
415                             // app cannot catch and recover from this, so do on their behalf
416                             Log.w(TAG, "Enqueue adjustment failed; no longer connected", e);
417                         }
418                     }
419                     break;
420                 }
421                 case MSG_ON_NOTIFICATION_SNOOZED: {
422                     SomeArgs args = (SomeArgs) msg.obj;
423                     StatusBarNotification sbn = (StatusBarNotification) args.arg1;
424                     String snoozeCriterionId = (String) args.arg2;
425                     args.recycle();
426                     onNotificationSnoozedUntilContext(sbn, snoozeCriterionId);
427                     break;
428                 }
429                 case MSG_ON_NOTIFICATIONS_SEEN: {
430                     SomeArgs args = (SomeArgs) msg.obj;
431                     List<String> keys = (List<String>) args.arg1;
432                     args.recycle();
433                     onNotificationsSeen(keys);
434                     break;
435                 }
436                 case MSG_ON_NOTIFICATION_EXPANSION_CHANGED: {
437                     SomeArgs args = (SomeArgs) msg.obj;
438                     String key = (String) args.arg1;
439                     boolean isUserAction = args.argi1 == 1;
440                     boolean isExpanded = args.argi2 == 1;
441                     args.recycle();
442                     onNotificationExpansionChanged(key, isUserAction, isExpanded);
443                     break;
444                 }
445                 case MSG_ON_NOTIFICATION_DIRECT_REPLY_SENT: {
446                     SomeArgs args = (SomeArgs) msg.obj;
447                     String key = (String) args.arg1;
448                     args.recycle();
449                     onNotificationDirectReplied(key);
450                     break;
451                 }
452                 case MSG_ON_SUGGESTED_REPLY_SENT: {
453                     SomeArgs args = (SomeArgs) msg.obj;
454                     String key = (String) args.arg1;
455                     CharSequence reply = (CharSequence) args.arg2;
456                     int source = args.argi2;
457                     args.recycle();
458                     onSuggestedReplySent(key, reply, source);
459                     break;
460                 }
461                 case MSG_ON_ACTION_INVOKED: {
462                     SomeArgs args = (SomeArgs) msg.obj;
463                     String key = (String) args.arg1;
464                     Notification.Action action = (Notification.Action) args.arg2;
465                     int source = args.argi2;
466                     args.recycle();
467                     onActionInvoked(key, action, source);
468                     break;
469                 }
470                 case MSG_ON_ALLOWED_ADJUSTMENTS_CHANGED: {
471                     onAllowedAdjustmentsChanged();
472                     break;
473                 }
474             }
475         }
476     }
477 }
478