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 android.annotation.SdkConstant;
20 import android.annotation.SystemApi;
21 import android.annotation.TestApi;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.os.Handler;
25 import android.os.IBinder;
26 import android.os.Looper;
27 import android.os.Message;
28 import android.os.RemoteException;
29 import android.util.Log;
30 
31 import com.android.internal.os.SomeArgs;
32 
33 import java.util.List;
34 
35 /**
36  * A service that helps the user manage notifications.
37  * @hide
38  */
39 @SystemApi
40 @TestApi
41 public abstract class NotificationAssistantService extends NotificationListenerService {
42     private static final String TAG = "NotificationAssistants";
43 
44     /**
45      * The {@link Intent} that must be declared as handled by the service.
46      */
47     @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
48     public static final String SERVICE_INTERFACE
49             = "android.service.notification.NotificationAssistantService";
50 
51     /**
52      * @hide
53      */
54     protected Handler mHandler;
55 
56     @Override
attachBaseContext(Context base)57     protected void attachBaseContext(Context base) {
58         super.attachBaseContext(base);
59         mHandler = new MyHandler(getContext().getMainLooper());
60     }
61 
62     @Override
onBind(Intent intent)63     public final IBinder onBind(Intent intent) {
64         if (mWrapper == null) {
65             mWrapper = new NotificationAssistantServiceWrapper();
66         }
67         return mWrapper;
68     }
69 
70     /**
71      * A notification was snoozed until a context. For use with
72      * {@link Adjustment#KEY_SNOOZE_CRITERIA}. When the device reaches the given context, the
73      * assistant should restore the notification with {@link #unsnoozeNotification(String)}.
74      *
75      * @param sbn the notification to snooze
76      * @param snoozeCriterionId the {@link SnoozeCriterion#getId()} representing a device context.
77      */
onNotificationSnoozedUntilContext(StatusBarNotification sbn, String snoozeCriterionId)78     abstract public void onNotificationSnoozedUntilContext(StatusBarNotification sbn,
79             String snoozeCriterionId);
80 
81     /**
82      * A notification was posted by an app. Called before post.
83      *
84      * @param sbn the new notification
85      * @return an adjustment or null to take no action, within 100ms.
86      */
onNotificationEnqueued(StatusBarNotification sbn)87     abstract public Adjustment onNotificationEnqueued(StatusBarNotification sbn);
88 
89     /**
90      * Implement this method to learn when notifications are removed, how they were interacted with
91      * before removal, and why they were removed.
92      * <p>
93      * This might occur because the user has dismissed the notification using system UI (or another
94      * notification listener) or because the app has withdrawn the notification.
95      * <p>
96      * NOTE: The {@link StatusBarNotification} object you receive will be "light"; that is, the
97      * result from {@link StatusBarNotification#getNotification} may be missing some heavyweight
98      * fields such as {@link android.app.Notification#contentView} and
99      * {@link android.app.Notification#largeIcon}. However, all other fields on
100      * {@link StatusBarNotification}, sufficient to match this call with a prior call to
101      * {@link #onNotificationPosted(StatusBarNotification)}, will be intact.
102      *
103      ** @param sbn A data structure encapsulating at least the original information (tag and id)
104      *            and source (package name) used to post the {@link android.app.Notification} that
105      *            was just removed.
106      * @param rankingMap The current ranking map that can be used to retrieve ranking information
107      *                   for active notifications.
108      * @param stats Stats about how the user interacted with the notification before it was removed.
109      * @param reason see {@link #REASON_LISTENER_CANCEL}, etc.
110      */
111     @Override
onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap, NotificationStats stats, int reason)112     public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap,
113             NotificationStats stats, int reason) {
114         onNotificationRemoved(sbn, rankingMap, reason);
115     }
116 
117     /**
118      * Updates a notification.  N.B. this won’t cause
119      * an existing notification to alert, but might allow a future update to
120      * this notification to alert.
121      *
122      * @param adjustment the adjustment with an explanation
123      */
adjustNotification(Adjustment adjustment)124     public final void adjustNotification(Adjustment adjustment) {
125         if (!isBound()) return;
126         try {
127             getNotificationInterface().applyAdjustmentFromAssistant(mWrapper, adjustment);
128         } catch (android.os.RemoteException ex) {
129             Log.v(TAG, "Unable to contact notification manager", ex);
130             throw ex.rethrowFromSystemServer();
131         }
132     }
133 
134     /**
135      * Updates existing notifications. Re-ranking won't occur until all adjustments are applied.
136      * N.B. this won’t cause an existing notification to alert, but might allow a future update to
137      * these notifications to alert.
138      *
139      * @param adjustments a list of adjustments with explanations
140      */
adjustNotifications(List<Adjustment> adjustments)141     public final void adjustNotifications(List<Adjustment> adjustments) {
142         if (!isBound()) return;
143         try {
144             getNotificationInterface().applyAdjustmentsFromAssistant(mWrapper, adjustments);
145         } catch (android.os.RemoteException ex) {
146             Log.v(TAG, "Unable to contact notification manager", ex);
147             throw ex.rethrowFromSystemServer();
148         }
149     }
150 
151     /**
152      * Inform the notification manager about un-snoozing a specific notification.
153      * <p>
154      * This should only be used for notifications snoozed by this listener using
155      * {@link #snoozeNotification(String, String)}. Once un-snoozed, you will get a
156      * {@link #onNotificationPosted(StatusBarNotification, RankingMap)} callback for the
157      * notification.
158      * @param key The key of the notification to snooze
159      */
unsnoozeNotification(String key)160     public final void unsnoozeNotification(String key) {
161         if (!isBound()) return;
162         try {
163             getNotificationInterface().unsnoozeNotificationFromAssistant(mWrapper, key);
164         } catch (android.os.RemoteException ex) {
165             Log.v(TAG, "Unable to contact notification manager", ex);
166         }
167     }
168 
169     private class NotificationAssistantServiceWrapper extends NotificationListenerWrapper {
170         @Override
onNotificationEnqueued(IStatusBarNotificationHolder sbnHolder)171         public void onNotificationEnqueued(IStatusBarNotificationHolder sbnHolder) {
172             StatusBarNotification sbn;
173             try {
174                 sbn = sbnHolder.get();
175             } catch (RemoteException e) {
176                 Log.w(TAG, "onNotificationEnqueued: Error receiving StatusBarNotification", e);
177                 return;
178             }
179 
180             SomeArgs args = SomeArgs.obtain();
181             args.arg1 = sbn;
182             mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_ENQUEUED,
183                     args).sendToTarget();
184         }
185 
186         @Override
onNotificationSnoozedUntilContext( IStatusBarNotificationHolder sbnHolder, String snoozeCriterionId)187         public void onNotificationSnoozedUntilContext(
188                 IStatusBarNotificationHolder sbnHolder, String snoozeCriterionId)
189                 throws RemoteException {
190             StatusBarNotification sbn;
191             try {
192                 sbn = sbnHolder.get();
193             } catch (RemoteException e) {
194                 Log.w(TAG, "onNotificationSnoozed: Error receiving StatusBarNotification", e);
195                 return;
196             }
197 
198             SomeArgs args = SomeArgs.obtain();
199             args.arg1 = sbn;
200             args.arg2 = snoozeCriterionId;
201             mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_SNOOZED,
202                     args).sendToTarget();
203         }
204     }
205 
206     private final class MyHandler extends Handler {
207         public static final int MSG_ON_NOTIFICATION_ENQUEUED = 1;
208         public static final int MSG_ON_NOTIFICATION_SNOOZED = 2;
209 
MyHandler(Looper looper)210         public MyHandler(Looper looper) {
211             super(looper, null, false);
212         }
213 
214         @Override
handleMessage(Message msg)215         public void handleMessage(Message msg) {
216             switch (msg.what) {
217                 case MSG_ON_NOTIFICATION_ENQUEUED: {
218                     SomeArgs args = (SomeArgs) msg.obj;
219                     StatusBarNotification sbn = (StatusBarNotification) args.arg1;
220                     args.recycle();
221                     Adjustment adjustment = onNotificationEnqueued(sbn);
222                     if (adjustment != null) {
223                         if (!isBound()) return;
224                         try {
225                             getNotificationInterface().applyEnqueuedAdjustmentFromAssistant(
226                                     mWrapper, adjustment);
227                         } catch (android.os.RemoteException ex) {
228                             Log.v(TAG, "Unable to contact notification manager", ex);
229                             throw ex.rethrowFromSystemServer();
230                         }
231                     }
232                     break;
233                 }
234                 case MSG_ON_NOTIFICATION_SNOOZED: {
235                     SomeArgs args = (SomeArgs) msg.obj;
236                     StatusBarNotification sbn = (StatusBarNotification) args.arg1;
237                     String snoozeCriterionId = (String) args.arg2;
238                     args.recycle();
239                     onNotificationSnoozedUntilContext(sbn, snoozeCriterionId);
240                     break;
241                 }
242             }
243         }
244     }
245 }
246