• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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 package com.android.server.notification;
17 
18 import android.annotation.NonNull;
19 import android.app.AlarmManager;
20 import android.app.Notification;
21 import android.app.PendingIntent;
22 import android.content.BroadcastReceiver;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.IntentFilter;
26 import android.net.Uri;
27 import android.os.Binder;
28 import android.os.SystemClock;
29 import android.os.UserHandle;
30 import android.service.notification.StatusBarNotification;
31 import android.util.ArrayMap;
32 import android.util.IntArray;
33 import android.util.Log;
34 import android.util.Slog;
35 
36 import com.android.internal.annotations.VisibleForTesting;
37 import com.android.internal.logging.MetricsLogger;
38 import com.android.internal.logging.nano.MetricsProto;
39 
40 import org.xmlpull.v1.XmlPullParser;
41 import org.xmlpull.v1.XmlPullParserException;
42 import org.xmlpull.v1.XmlSerializer;
43 
44 import java.io.IOException;
45 import java.io.PrintWriter;
46 import java.util.ArrayList;
47 import java.util.Collection;
48 import java.util.Collections;
49 import java.util.Date;
50 import java.util.List;
51 import java.util.Map;
52 import java.util.Objects;
53 import java.util.Set;
54 
55 /**
56  * NotificationManagerService helper for handling snoozed notifications.
57  */
58 public class SnoozeHelper {
59     private static final String TAG = "SnoozeHelper";
60     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
61     private static final String INDENT = "    ";
62 
63     private static final String REPOST_ACTION = SnoozeHelper.class.getSimpleName() + ".EVALUATE";
64     private static final int REQUEST_CODE_REPOST = 1;
65     private static final String REPOST_SCHEME = "repost";
66     private static final String EXTRA_KEY = "key";
67     private static final String EXTRA_USER_ID = "userId";
68 
69     private final Context mContext;
70     private AlarmManager mAm;
71     private final ManagedServices.UserProfiles mUserProfiles;
72 
73     // User id : package name : notification key : record.
74     private ArrayMap<Integer, ArrayMap<String, ArrayMap<String, NotificationRecord>>>
75             mSnoozedNotifications = new ArrayMap<>();
76     // notification key : package.
77     private ArrayMap<String, String> mPackages = new ArrayMap<>();
78     // key : userId
79     private ArrayMap<String, Integer> mUsers = new ArrayMap<>();
80     private Callback mCallback;
81 
SnoozeHelper(Context context, Callback callback, ManagedServices.UserProfiles userProfiles)82     public SnoozeHelper(Context context, Callback callback,
83             ManagedServices.UserProfiles userProfiles) {
84         mContext = context;
85         IntentFilter filter = new IntentFilter(REPOST_ACTION);
86         filter.addDataScheme(REPOST_SCHEME);
87         mContext.registerReceiver(mBroadcastReceiver, filter);
88         mAm = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
89         mCallback = callback;
90         mUserProfiles = userProfiles;
91     }
92 
isSnoozed(int userId, String pkg, String key)93     protected boolean isSnoozed(int userId, String pkg, String key) {
94         return mSnoozedNotifications.containsKey(userId)
95                 && mSnoozedNotifications.get(userId).containsKey(pkg)
96                 && mSnoozedNotifications.get(userId).get(pkg).containsKey(key);
97     }
98 
getSnoozed(int userId, String pkg)99     protected Collection<NotificationRecord> getSnoozed(int userId, String pkg) {
100         if (mSnoozedNotifications.containsKey(userId)
101                 && mSnoozedNotifications.get(userId).containsKey(pkg)) {
102             return mSnoozedNotifications.get(userId).get(pkg).values();
103         }
104         return Collections.EMPTY_LIST;
105     }
106 
getSnoozed()107     protected @NonNull List<NotificationRecord> getSnoozed() {
108         List<NotificationRecord> snoozedForUser = new ArrayList<>();
109         IntArray userIds = mUserProfiles.getCurrentProfileIds();
110         if (userIds != null) {
111             final int N = userIds.size();
112             for (int i = 0; i < N; i++) {
113                 final ArrayMap<String, ArrayMap<String, NotificationRecord>> snoozedPkgs =
114                         mSnoozedNotifications.get(userIds.get(i));
115                 if (snoozedPkgs != null) {
116                     final int M = snoozedPkgs.size();
117                     for (int j = 0; j < M; j++) {
118                         final ArrayMap<String, NotificationRecord> records = snoozedPkgs.valueAt(j);
119                         if (records != null) {
120                             snoozedForUser.addAll(records.values());
121                         }
122                     }
123                 }
124             }
125         }
126         return snoozedForUser;
127     }
128 
129     /**
130      * Snoozes a notification and schedules an alarm to repost at that time.
131      */
snooze(NotificationRecord record, long duration)132     protected void snooze(NotificationRecord record, long duration) {
133         snooze(record);
134         scheduleRepost(record.sbn.getPackageName(), record.getKey(), record.getUserId(), duration);
135     }
136 
137     /**
138      * Records a snoozed notification.
139      */
snooze(NotificationRecord record)140     protected void snooze(NotificationRecord record) {
141         int userId = record.getUser().getIdentifier();
142         if (DEBUG) {
143             Slog.d(TAG, "Snoozing " + record.getKey());
144         }
145         ArrayMap<String, ArrayMap<String, NotificationRecord>> records =
146                 mSnoozedNotifications.get(userId);
147         if (records == null) {
148             records = new ArrayMap<>();
149         }
150         ArrayMap<String, NotificationRecord> pkgRecords = records.get(record.sbn.getPackageName());
151         if (pkgRecords == null) {
152             pkgRecords = new ArrayMap<>();
153         }
154         pkgRecords.put(record.getKey(), record);
155         records.put(record.sbn.getPackageName(), pkgRecords);
156         mSnoozedNotifications.put(userId, records);
157         mPackages.put(record.getKey(), record.sbn.getPackageName());
158         mUsers.put(record.getKey(), userId);
159     }
160 
cancel(int userId, String pkg, String tag, int id)161     protected boolean cancel(int userId, String pkg, String tag, int id) {
162         if (mSnoozedNotifications.containsKey(userId)) {
163             ArrayMap<String, NotificationRecord> recordsForPkg =
164                     mSnoozedNotifications.get(userId).get(pkg);
165             if (recordsForPkg != null) {
166                 final Set<Map.Entry<String, NotificationRecord>> records = recordsForPkg.entrySet();
167                 for (Map.Entry<String, NotificationRecord> record : records) {
168                     final StatusBarNotification sbn = record.getValue().sbn;
169                     if (Objects.equals(sbn.getTag(), tag) && sbn.getId() == id) {
170                         record.getValue().isCanceled = true;
171                         return true;
172                     }
173                 }
174             }
175         }
176         return false;
177     }
178 
cancel(int userId, boolean includeCurrentProfiles)179     protected boolean cancel(int userId, boolean includeCurrentProfiles) {
180         int[] userIds = {userId};
181         if (includeCurrentProfiles) {
182             userIds = mUserProfiles.getCurrentProfileIds().toArray();
183         }
184         final int N = userIds.length;
185         for (int i = 0; i < N; i++) {
186             final ArrayMap<String, ArrayMap<String, NotificationRecord>> snoozedPkgs =
187                     mSnoozedNotifications.get(userIds[i]);
188             if (snoozedPkgs != null) {
189                 final int M = snoozedPkgs.size();
190                 for (int j = 0; j < M; j++) {
191                     final ArrayMap<String, NotificationRecord> records = snoozedPkgs.valueAt(j);
192                     if (records != null) {
193                         int P = records.size();
194                         for (int k = 0; k < P; k++) {
195                             records.valueAt(k).isCanceled = true;
196                         }
197                     }
198                 }
199                 return true;
200             }
201         }
202         return false;
203     }
204 
cancel(int userId, String pkg)205     protected boolean cancel(int userId, String pkg) {
206         if (mSnoozedNotifications.containsKey(userId)) {
207             if (mSnoozedNotifications.get(userId).containsKey(pkg)) {
208                 ArrayMap<String, NotificationRecord> records =
209                         mSnoozedNotifications.get(userId).get(pkg);
210                 int N = records.size();
211                 for (int i = 0; i < N; i++) {
212                     records.valueAt(i).isCanceled = true;
213                 }
214                 return true;
215             }
216         }
217         return false;
218     }
219 
220     /**
221      * Updates the notification record so the most up to date information is shown on re-post.
222      */
update(int userId, NotificationRecord record)223     protected void update(int userId, NotificationRecord record) {
224         ArrayMap<String, ArrayMap<String, NotificationRecord>> records =
225                 mSnoozedNotifications.get(userId);
226         if (records == null) {
227             return;
228         }
229         ArrayMap<String, NotificationRecord> pkgRecords = records.get(record.sbn.getPackageName());
230         if (pkgRecords == null) {
231             return;
232         }
233         NotificationRecord existing = pkgRecords.get(record.getKey());
234         if (existing != null && existing.isCanceled) {
235             return;
236         }
237         pkgRecords.put(record.getKey(), record);
238     }
239 
repost(String key)240     protected void repost(String key) {
241         Integer userId = mUsers.get(key);
242         if (userId != null) {
243             repost(key, userId);
244         }
245     }
246 
repost(String key, int userId)247     protected void repost(String key, int userId) {
248         final String pkg = mPackages.remove(key);
249         ArrayMap<String, ArrayMap<String, NotificationRecord>> records =
250                 mSnoozedNotifications.get(userId);
251         if (records == null) {
252             return;
253         }
254         ArrayMap<String, NotificationRecord> pkgRecords = records.get(pkg);
255         if (pkgRecords == null) {
256             return;
257         }
258         final NotificationRecord record = pkgRecords.remove(key);
259         mPackages.remove(key);
260         mUsers.remove(key);
261 
262         if (record != null && !record.isCanceled) {
263             MetricsLogger.action(record.getLogMaker()
264                     .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED)
265                     .setType(MetricsProto.MetricsEvent.TYPE_OPEN));
266             mCallback.repost(userId, record);
267         }
268     }
269 
repostGroupSummary(String pkg, int userId, String groupKey)270     protected void repostGroupSummary(String pkg, int userId, String groupKey) {
271         if (mSnoozedNotifications.containsKey(userId)) {
272             ArrayMap<String, ArrayMap<String, NotificationRecord>> keysByPackage
273                     = mSnoozedNotifications.get(userId);
274 
275             if (keysByPackage != null && keysByPackage.containsKey(pkg)) {
276                 ArrayMap<String, NotificationRecord> recordsByKey = keysByPackage.get(pkg);
277 
278                 if (recordsByKey != null) {
279                     String groupSummaryKey = null;
280                     int N = recordsByKey.size();
281                     for (int i = 0; i < N; i++) {
282                         final NotificationRecord potentialGroupSummary = recordsByKey.valueAt(i);
283                         if (potentialGroupSummary.sbn.isGroup()
284                                 && potentialGroupSummary.getNotification().isGroupSummary()
285                                 && groupKey.equals(potentialGroupSummary.getGroupKey())) {
286                             groupSummaryKey = potentialGroupSummary.getKey();
287                             break;
288                         }
289                     }
290 
291                     if (groupSummaryKey != null) {
292                         NotificationRecord record = recordsByKey.remove(groupSummaryKey);
293                         mPackages.remove(groupSummaryKey);
294                         mUsers.remove(groupSummaryKey);
295 
296                         if (record != null && !record.isCanceled) {
297                             MetricsLogger.action(record.getLogMaker()
298                                     .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED)
299                                     .setType(MetricsProto.MetricsEvent.TYPE_OPEN));
300                             mCallback.repost(userId, record);
301                         }
302                     }
303                 }
304             }
305         }
306     }
307 
clearData(int userId, String pkg)308     protected void clearData(int userId, String pkg) {
309         ArrayMap<String, ArrayMap<String, NotificationRecord>> records =
310                 mSnoozedNotifications.get(userId);
311         if (records == null) {
312             return;
313         }
314         ArrayMap<String, NotificationRecord> pkgRecords = records.get(pkg);
315         if (pkgRecords == null) {
316             return;
317         }
318         for (int i = pkgRecords.size() - 1; i >= 0; i--) {
319             final NotificationRecord r = pkgRecords.removeAt(i);
320             if (r != null) {
321                 mPackages.remove(r.getKey());
322                 mUsers.remove(r.getKey());
323                 final PendingIntent pi = createPendingIntent(pkg, r.getKey(), userId);
324                 mAm.cancel(pi);
325                 MetricsLogger.action(r.getLogMaker()
326                         .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED)
327                         .setType(MetricsProto.MetricsEvent.TYPE_DISMISS));
328             }
329         }
330     }
331 
createPendingIntent(String pkg, String key, int userId)332     private PendingIntent createPendingIntent(String pkg, String key, int userId) {
333         return PendingIntent.getBroadcast(mContext,
334                 REQUEST_CODE_REPOST,
335                 new Intent(REPOST_ACTION)
336                         .setData(new Uri.Builder().scheme(REPOST_SCHEME).appendPath(key).build())
337                         .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
338                         .putExtra(EXTRA_KEY, key)
339                         .putExtra(EXTRA_USER_ID, userId),
340                 PendingIntent.FLAG_UPDATE_CURRENT);
341     }
342 
scheduleRepost(String pkg, String key, int userId, long duration)343     private void scheduleRepost(String pkg, String key, int userId, long duration) {
344         long identity = Binder.clearCallingIdentity();
345         try {
346             final PendingIntent pi = createPendingIntent(pkg, key, userId);
347             mAm.cancel(pi);
348             long time = SystemClock.elapsedRealtime() + duration;
349             if (DEBUG) Slog.d(TAG, "Scheduling evaluate for " + new Date(time));
350             mAm.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, time, pi);
351         } finally {
352             Binder.restoreCallingIdentity(identity);
353         }
354     }
355 
dump(PrintWriter pw, NotificationManagerService.DumpFilter filter)356     public void dump(PrintWriter pw, NotificationManagerService.DumpFilter filter) {
357         pw.println("\n  Snoozed notifications:");
358         for (int userId : mSnoozedNotifications.keySet()) {
359             pw.print(INDENT);
360             pw.println("user: " + userId);
361             ArrayMap<String, ArrayMap<String, NotificationRecord>> snoozedPkgs =
362                     mSnoozedNotifications.get(userId);
363             for (String pkg : snoozedPkgs.keySet()) {
364                 pw.print(INDENT);
365                 pw.print(INDENT);
366                 pw.println("package: " + pkg);
367                 Set<String> snoozedKeys = snoozedPkgs.get(pkg).keySet();
368                 for (String key : snoozedKeys) {
369                     pw.print(INDENT);
370                     pw.print(INDENT);
371                     pw.print(INDENT);
372                     pw.println(key);
373                 }
374             }
375         }
376     }
377 
writeXml(XmlSerializer out, boolean forBackup)378     protected void writeXml(XmlSerializer out, boolean forBackup) throws IOException {
379 
380     }
381 
readXml(XmlPullParser parser, boolean forRestore)382     public void readXml(XmlPullParser parser, boolean forRestore)
383             throws XmlPullParserException, IOException {
384 
385     }
386 
387     @VisibleForTesting
setAlarmManager(AlarmManager am)388     void setAlarmManager(AlarmManager am) {
389         mAm = am;
390     }
391 
392     protected interface Callback {
repost(int userId, NotificationRecord r)393         void repost(int userId, NotificationRecord r);
394     }
395 
396     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
397         @Override
398         public void onReceive(Context context, Intent intent) {
399             if (DEBUG) {
400                 Slog.d(TAG, "Reposting notification");
401             }
402             if (REPOST_ACTION.equals(intent.getAction())) {
403                 repost(intent.getStringExtra(EXTRA_KEY), intent.getIntExtra(EXTRA_USER_ID,
404                         UserHandle.USER_SYSTEM));
405             }
406         }
407     };
408 }
409