• 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.PendingIntent;
21 import android.content.BroadcastReceiver;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.IntentFilter;
25 import android.net.Uri;
26 import android.os.Binder;
27 import android.os.UserHandle;
28 import android.service.notification.StatusBarNotification;
29 import android.util.ArrayMap;
30 import android.util.IntArray;
31 import android.util.Log;
32 import android.util.Slog;
33 import android.util.TypedXmlPullParser;
34 import android.util.TypedXmlSerializer;
35 
36 import com.android.internal.annotations.VisibleForTesting;
37 import com.android.internal.logging.MetricsLogger;
38 import com.android.internal.logging.nano.MetricsProto;
39 import com.android.server.pm.PackageManagerService;
40 
41 import org.xmlpull.v1.XmlPullParser;
42 import org.xmlpull.v1.XmlPullParserException;
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.Date;
49 import java.util.List;
50 import java.util.Map;
51 import java.util.Objects;
52 import java.util.Set;
53 
54 /**
55  * NotificationManagerService helper for handling snoozed notifications.
56  */
57 public final class SnoozeHelper {
58     public static final int XML_SNOOZED_NOTIFICATION_VERSION = 1;
59 
60     static final int CONCURRENT_SNOOZE_LIMIT = 500;
61 
62     // A safe size for strings to be put in persistent storage, to avoid breaking the XML write.
63     static final int MAX_STRING_LENGTH = 1000;
64 
65     protected static final String XML_TAG_NAME = "snoozed-notifications";
66 
67     private static final String XML_SNOOZED_NOTIFICATION = "notification";
68     private static final String XML_SNOOZED_NOTIFICATION_CONTEXT = "context";
69     private static final String XML_SNOOZED_NOTIFICATION_KEY = "key";
70     //the time the snoozed notification should be reposted
71     private static final String XML_SNOOZED_NOTIFICATION_TIME = "time";
72     private static final String XML_SNOOZED_NOTIFICATION_CONTEXT_ID = "id";
73     private static final String XML_SNOOZED_NOTIFICATION_VERSION_LABEL = "version";
74 
75 
76     private static final String TAG = "SnoozeHelper";
77     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
78     private static final String INDENT = "    ";
79 
80     private static final String REPOST_ACTION = SnoozeHelper.class.getSimpleName() + ".EVALUATE";
81     private static final int REQUEST_CODE_REPOST = 1;
82     private static final String REPOST_SCHEME = "repost";
83     static final String EXTRA_KEY = "key";
84     private static final String EXTRA_USER_ID = "userId";
85 
86     private final Context mContext;
87     private AlarmManager mAm;
88     private final ManagedServices.UserProfiles mUserProfiles;
89 
90     // notification key : record.
91     private ArrayMap<String, NotificationRecord> mSnoozedNotifications = new ArrayMap<>();
92     // notification key : time-milliseconds .
93     // This member stores persisted snoozed notification trigger times. it persists through reboots
94     // It should have the notifications that haven't expired or re-posted yet
95     private final ArrayMap<String, Long> mPersistedSnoozedNotifications = new ArrayMap<>();
96     // notification key : creation ID.
97     // This member stores persisted snoozed notification trigger context for the assistant
98     // it persists through reboots.
99     // It should have the notifications that haven't expired or re-posted yet
100     private final ArrayMap<String, String>
101             mPersistedSnoozedNotificationsWithContext = new ArrayMap<>();
102 
103     private Callback mCallback;
104 
105     private final Object mLock = new Object();
106 
SnoozeHelper(Context context, Callback callback, ManagedServices.UserProfiles userProfiles)107     public SnoozeHelper(Context context, Callback callback,
108             ManagedServices.UserProfiles userProfiles) {
109         mContext = context;
110         IntentFilter filter = new IntentFilter(REPOST_ACTION);
111         filter.addDataScheme(REPOST_SCHEME);
112         mContext.registerReceiver(mBroadcastReceiver, filter,
113                 Context.RECEIVER_EXPORTED_UNAUDITED);
114         mAm = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
115         mCallback = callback;
116         mUserProfiles = userProfiles;
117     }
118 
canSnooze(int numberToSnooze)119     protected boolean canSnooze(int numberToSnooze) {
120         synchronized (mLock) {
121             if ((mSnoozedNotifications.size() + numberToSnooze) > CONCURRENT_SNOOZE_LIMIT) {
122                 return false;
123             }
124         }
125         return true;
126     }
127 
128     @NonNull
getSnoozeTimeForUnpostedNotification(int userId, String pkg, String key)129     protected Long getSnoozeTimeForUnpostedNotification(int userId, String pkg, String key) {
130         Long time = null;
131         synchronized (mLock) {
132             time = mPersistedSnoozedNotifications.get(getTrimmedString(key));
133         }
134         if (time == null) {
135             time = 0L;
136         }
137         return time;
138     }
139 
getSnoozeContextForUnpostedNotification(int userId, String pkg, String key)140     protected String getSnoozeContextForUnpostedNotification(int userId, String pkg, String key) {
141         synchronized (mLock) {
142             return mPersistedSnoozedNotificationsWithContext.get(getTrimmedString(key));
143         }
144     }
145 
isSnoozed(int userId, String pkg, String key)146     protected boolean isSnoozed(int userId, String pkg, String key) {
147         synchronized (mLock) {
148             return mSnoozedNotifications.containsKey(key);
149         }
150     }
151 
getSnoozed(int userId, String pkg)152     protected Collection<NotificationRecord> getSnoozed(int userId, String pkg) {
153         synchronized (mLock) {
154             ArrayList snoozed = new ArrayList();
155             for (NotificationRecord r : mSnoozedNotifications.values()) {
156                 if (r.getUserId() == userId && r.getSbn().getPackageName().equals(pkg)) {
157                     snoozed.add(r);
158                 }
159             }
160             return snoozed;
161         }
162     }
163 
164     @NonNull
getNotifications(String pkg, String groupKey, Integer userId)165     ArrayList<NotificationRecord> getNotifications(String pkg,
166             String groupKey, Integer userId) {
167         ArrayList<NotificationRecord> records =  new ArrayList<>();
168         synchronized (mLock) {
169             for (int i = 0; i < mSnoozedNotifications.size(); i++) {
170                 NotificationRecord r = mSnoozedNotifications.valueAt(i);
171                 if (r.getSbn().getPackageName().equals(pkg) && r.getUserId() == userId
172                         && Objects.equals(r.getSbn().getGroup(), groupKey)) {
173                     records.add(r);
174                 }
175             }
176         }
177         return records;
178     }
179 
getNotification(String key)180     protected NotificationRecord getNotification(String key) {
181         synchronized (mLock) {
182             return mSnoozedNotifications.get(key);
183         }
184     }
185 
getSnoozed()186     protected @NonNull List<NotificationRecord> getSnoozed() {
187         synchronized (mLock) {
188             // caller filters records based on the current user profiles and listener access,
189             // so just return everything
190             List<NotificationRecord> snoozed = new ArrayList<>();
191             snoozed.addAll(mSnoozedNotifications.values());
192             return snoozed;
193         }
194     }
195 
196     /**
197      * Snoozes a notification and schedules an alarm to repost at that time.
198      */
snooze(NotificationRecord record, long duration)199     protected void snooze(NotificationRecord record, long duration) {
200         String key = record.getKey();
201 
202         snooze(record);
203         scheduleRepost(key, duration);
204         Long activateAt = System.currentTimeMillis() + duration;
205         synchronized (mLock) {
206             mPersistedSnoozedNotifications.put(getTrimmedString(key), activateAt);
207         }
208     }
209 
210     /**
211      * Records a snoozed notification.
212      */
snooze(NotificationRecord record, String contextId)213     protected void snooze(NotificationRecord record, String contextId) {
214         if (contextId != null) {
215             synchronized (mLock) {
216                 mPersistedSnoozedNotificationsWithContext.put(
217                         getTrimmedString(record.getKey()),
218                         getTrimmedString(contextId)
219                 );
220             }
221         }
222         snooze(record);
223     }
224 
snooze(NotificationRecord record)225     private void snooze(NotificationRecord record) {
226         if (DEBUG) {
227             Slog.d(TAG, "Snoozing " + record.getKey());
228         }
229         synchronized (mLock) {
230             mSnoozedNotifications.put(record.getKey(), record);
231         }
232     }
233 
getTrimmedString(String key)234     private String getTrimmedString(String key) {
235         if (key != null && key.length() > MAX_STRING_LENGTH) {
236             return key.substring(0, MAX_STRING_LENGTH);
237         }
238         return key;
239     }
240 
cancel(int userId, String pkg, String tag, int id)241     protected boolean cancel(int userId, String pkg, String tag, int id) {
242         synchronized (mLock) {
243             final Set<Map.Entry<String, NotificationRecord>> records =
244                     mSnoozedNotifications.entrySet();
245             for (Map.Entry<String, NotificationRecord> record : records) {
246                 final StatusBarNotification sbn = record.getValue().getSbn();
247                 if (sbn.getPackageName().equals(pkg) && sbn.getUserId() == userId
248                         && Objects.equals(sbn.getTag(), tag) && sbn.getId() == id) {
249                     record.getValue().isCanceled = true;
250                     return true;
251                 }
252             }
253         }
254         return false;
255     }
256 
cancel(int userId, boolean includeCurrentProfiles)257     protected void cancel(int userId, boolean includeCurrentProfiles) {
258         synchronized (mLock) {
259             if (mSnoozedNotifications.size() == 0) {
260                 return;
261             }
262             IntArray userIds = new IntArray();
263             userIds.add(userId);
264             if (includeCurrentProfiles) {
265                 userIds = mUserProfiles.getCurrentProfileIds();
266             }
267             for (NotificationRecord r : mSnoozedNotifications.values()) {
268                 if (userIds.binarySearch(r.getUserId()) >= 0) {
269                     r.isCanceled = true;
270                 }
271             }
272         }
273     }
274 
cancel(int userId, String pkg)275     protected boolean cancel(int userId, String pkg) {
276         synchronized (mLock) {
277             int n = mSnoozedNotifications.size();
278             for (int i = 0; i < n; i++) {
279                 final NotificationRecord r = mSnoozedNotifications.valueAt(i);
280                 if (r.getSbn().getPackageName().equals(pkg) && r.getUserId() == userId) {
281                     r.isCanceled = true;
282                 }
283             }
284             return true;
285         }
286     }
287 
288     /**
289      * Updates the notification record so the most up to date information is shown on re-post.
290      */
update(int userId, NotificationRecord record)291     protected void update(int userId, NotificationRecord record) {
292         synchronized (mLock) {
293             if (mSnoozedNotifications.containsKey(record.getKey())) {
294                 mSnoozedNotifications.put(record.getKey(), record);
295             }
296         }
297     }
298 
repost(String key, boolean muteOnReturn)299     protected void repost(String key, boolean muteOnReturn) {
300         synchronized (mLock) {
301             final NotificationRecord r = mSnoozedNotifications.get(key);
302             if (r != null) {
303                 repost(key, r.getUserId(), muteOnReturn);
304             }
305         }
306     }
307 
repost(String key, int userId, boolean muteOnReturn)308     protected void repost(String key, int userId, boolean muteOnReturn) {
309         final String trimmedKey = getTrimmedString(key);
310 
311         NotificationRecord record;
312         synchronized (mLock) {
313             mPersistedSnoozedNotifications.remove(trimmedKey);
314             mPersistedSnoozedNotificationsWithContext.remove(trimmedKey);
315             record = mSnoozedNotifications.remove(key);
316         }
317 
318         if (record != null && !record.isCanceled) {
319             final PendingIntent pi = createPendingIntent(record.getKey());
320             mAm.cancel(pi);
321             MetricsLogger.action(record.getLogMaker()
322                     .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED)
323                     .setType(MetricsProto.MetricsEvent.TYPE_OPEN));
324             mCallback.repost(record.getUserId(), record, muteOnReturn);
325         }
326     }
327 
repostGroupSummary(String pkg, int userId, String groupKey)328     protected void repostGroupSummary(String pkg, int userId, String groupKey) {
329         synchronized (mLock) {
330             String groupSummaryKey = null;
331             int n = mSnoozedNotifications.size();
332             for (int i = 0; i < n; i++) {
333                 final NotificationRecord potentialGroupSummary = mSnoozedNotifications.valueAt(i);
334                 if (potentialGroupSummary.getSbn().getPackageName().equals(pkg)
335                         && potentialGroupSummary.getUserId() == userId
336                         && potentialGroupSummary.getSbn().isGroup()
337                         && potentialGroupSummary.getNotification().isGroupSummary()
338                         && groupKey.equals(potentialGroupSummary.getGroupKey())) {
339                     groupSummaryKey = potentialGroupSummary.getKey();
340                     break;
341                 }
342             }
343 
344             if (groupSummaryKey != null) {
345                 NotificationRecord record = mSnoozedNotifications.remove(groupSummaryKey);
346 
347                 if (record != null && !record.isCanceled) {
348                     Runnable runnable = () -> {
349                         MetricsLogger.action(record.getLogMaker()
350                                 .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED)
351                                 .setType(MetricsProto.MetricsEvent.TYPE_OPEN));
352                         mCallback.repost(record.getUserId(), record, false);
353                     };
354                     runnable.run();
355                 }
356             }
357         }
358     }
359 
clearData(int userId, String pkg)360     protected void clearData(int userId, String pkg) {
361         synchronized (mLock) {
362             int n = mSnoozedNotifications.size();
363             for (int i = n - 1; i >= 0; i--) {
364                 final NotificationRecord record = mSnoozedNotifications.valueAt(i);
365                 if (record.getUserId() == userId && record.getSbn().getPackageName().equals(pkg)) {
366                     mSnoozedNotifications.removeAt(i);
367                     String trimmedKey = getTrimmedString(record.getKey());
368                     mPersistedSnoozedNotificationsWithContext.remove(trimmedKey);
369                     mPersistedSnoozedNotifications.remove(trimmedKey);
370                     Runnable runnable = () -> {
371                         final PendingIntent pi = createPendingIntent(record.getKey());
372                         mAm.cancel(pi);
373                         MetricsLogger.action(record.getLogMaker()
374                                 .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED)
375                                 .setType(MetricsProto.MetricsEvent.TYPE_DISMISS));
376                     };
377                     runnable.run();
378                 }
379             }
380         }
381     }
382 
clearData(int userId)383     protected void clearData(int userId) {
384         synchronized (mLock) {
385             int n = mSnoozedNotifications.size();
386             for (int i = n - 1; i >= 0; i--) {
387                 final NotificationRecord record = mSnoozedNotifications.valueAt(i);
388                 if (record.getUserId() == userId) {
389                     mSnoozedNotifications.removeAt(i);
390                     String trimmedKey = getTrimmedString(record.getKey());
391                     mPersistedSnoozedNotificationsWithContext.remove(trimmedKey);
392                     mPersistedSnoozedNotifications.remove(trimmedKey);
393 
394                     Runnable runnable = () -> {
395                         final PendingIntent pi = createPendingIntent(record.getKey());
396                         mAm.cancel(pi);
397                         MetricsLogger.action(record.getLogMaker()
398                                 .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED)
399                                 .setType(MetricsProto.MetricsEvent.TYPE_DISMISS));
400                     };
401                     runnable.run();
402                 }
403             }
404         }
405     }
406 
createPendingIntent(String key)407     private PendingIntent createPendingIntent(String key) {
408         return PendingIntent.getBroadcast(mContext,
409                 REQUEST_CODE_REPOST,
410                 new Intent(REPOST_ACTION)
411                         .setPackage(PackageManagerService.PLATFORM_PACKAGE_NAME)
412                         .setData(new Uri.Builder().scheme(REPOST_SCHEME).appendPath(key).build())
413                         .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
414                         .putExtra(EXTRA_KEY, key),
415                 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
416     }
417 
scheduleRepostsForPersistedNotifications(long currentTime)418     public void scheduleRepostsForPersistedNotifications(long currentTime) {
419         synchronized (mLock) {
420             for (int i = 0; i < mPersistedSnoozedNotifications.size(); i++) {
421                 String key = mPersistedSnoozedNotifications.keyAt(i);
422                 Long time = mPersistedSnoozedNotifications.valueAt(i);
423                 if (time != null && time > currentTime) {
424                     scheduleRepostAtTime(key, time);
425                 }
426             }
427         }
428     }
429 
scheduleRepost(String key, long duration)430     private void scheduleRepost(String key, long duration) {
431         scheduleRepostAtTime(key, System.currentTimeMillis() + duration);
432     }
433 
scheduleRepostAtTime(String key, long time)434     private void scheduleRepostAtTime(String key, long time) {
435         Runnable runnable = () -> {
436             final long identity = Binder.clearCallingIdentity();
437             try {
438                 final PendingIntent pi = createPendingIntent(key);
439                 mAm.cancel(pi);
440                 if (DEBUG) Slog.d(TAG, "Scheduling evaluate for " + new Date(time));
441                 mAm.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time, pi);
442             } finally {
443                 Binder.restoreCallingIdentity(identity);
444             }
445         };
446         runnable.run();
447     }
448 
dump(PrintWriter pw, NotificationManagerService.DumpFilter filter)449     public void dump(PrintWriter pw, NotificationManagerService.DumpFilter filter) {
450         synchronized (mLock) {
451             pw.println("\n  Snoozed notifications:");
452             for (String key : mSnoozedNotifications.keySet()) {
453                 pw.print(INDENT);
454                 pw.println("key: " + key);
455             }
456             pw.println("\n Pending snoozed notifications");
457             for (String key : mPersistedSnoozedNotifications.keySet()) {
458                 pw.print(INDENT);
459                 pw.println("key: " + key + " until: " + mPersistedSnoozedNotifications.get(key));
460             }
461         }
462     }
463 
writeXml(TypedXmlSerializer out)464     protected void writeXml(TypedXmlSerializer out) throws IOException {
465         synchronized (mLock) {
466             final long currentTime = System.currentTimeMillis();
467             out.startTag(null, XML_TAG_NAME);
468             writeXml(out, mPersistedSnoozedNotifications, XML_SNOOZED_NOTIFICATION,
469                     value -> {
470                         if (value < currentTime) {
471                             return;
472                         }
473                         out.attributeLong(null, XML_SNOOZED_NOTIFICATION_TIME,
474                                 value);
475                     });
476             writeXml(out, mPersistedSnoozedNotificationsWithContext,
477                     XML_SNOOZED_NOTIFICATION_CONTEXT,
478                     value -> {
479                         out.attribute(null, XML_SNOOZED_NOTIFICATION_CONTEXT_ID,
480                                 value);
481                     });
482             out.endTag(null, XML_TAG_NAME);
483         }
484     }
485 
486     private interface Inserter<T> {
insert(T t)487         void insert(T t) throws IOException;
488     }
489 
writeXml(TypedXmlSerializer out, ArrayMap<String, T> targets, String tag, Inserter<T> attributeInserter)490     private <T> void writeXml(TypedXmlSerializer out, ArrayMap<String, T> targets, String tag,
491             Inserter<T> attributeInserter) throws IOException {
492         for (int j = 0; j < targets.size(); j++) {
493             String key = targets.keyAt(j);
494             // T is a String (snoozed until context) or Long (snoozed until time)
495             T value = targets.valueAt(j);
496 
497             out.startTag(null, tag);
498 
499             attributeInserter.insert(value);
500 
501             out.attributeInt(null, XML_SNOOZED_NOTIFICATION_VERSION_LABEL,
502                     XML_SNOOZED_NOTIFICATION_VERSION);
503             out.attribute(null, XML_SNOOZED_NOTIFICATION_KEY, key);
504 
505             out.endTag(null, tag);
506         }
507     }
508 
readXml(TypedXmlPullParser parser, long currentTime)509     protected void readXml(TypedXmlPullParser parser, long currentTime)
510             throws XmlPullParserException, IOException {
511         int type;
512         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
513             String tag = parser.getName();
514             if (type == XmlPullParser.END_TAG
515                     && XML_TAG_NAME.equals(tag)) {
516                 break;
517             }
518             if (type == XmlPullParser.START_TAG
519                     && (XML_SNOOZED_NOTIFICATION.equals(tag)
520                         || tag.equals(XML_SNOOZED_NOTIFICATION_CONTEXT))
521                     && parser.getAttributeInt(null, XML_SNOOZED_NOTIFICATION_VERSION_LABEL, -1)
522                         == XML_SNOOZED_NOTIFICATION_VERSION) {
523                 try {
524                     final String key = parser.getAttributeValue(null, XML_SNOOZED_NOTIFICATION_KEY);
525                     if (tag.equals(XML_SNOOZED_NOTIFICATION)) {
526                         final Long time = parser.getAttributeLong(
527                                 null, XML_SNOOZED_NOTIFICATION_TIME, 0);
528                         if (time > currentTime) { //only read new stuff
529                             synchronized (mLock) {
530                                 mPersistedSnoozedNotifications.put(key, time);
531                             }
532                         }
533                     }
534                     if (tag.equals(XML_SNOOZED_NOTIFICATION_CONTEXT)) {
535                         final String creationId = parser.getAttributeValue(
536                                 null, XML_SNOOZED_NOTIFICATION_CONTEXT_ID);
537                         synchronized (mLock) {
538                             mPersistedSnoozedNotificationsWithContext.put(key, creationId);
539                         }
540                     }
541                 } catch (Exception e) {
542                     Slog.e(TAG,  "Exception in reading snooze data from policy xml", e);
543                 }
544             }
545         }
546     }
547 
548     @VisibleForTesting
setAlarmManager(AlarmManager am)549     void setAlarmManager(AlarmManager am) {
550         mAm = am;
551     }
552 
553     protected interface Callback {
repost(int userId, NotificationRecord r, boolean muteOnReturn)554         void repost(int userId, NotificationRecord r, boolean muteOnReturn);
555     }
556 
557     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
558         @Override
559         public void onReceive(Context context, Intent intent) {
560             if (DEBUG) {
561                 Slog.d(TAG, "Reposting notification");
562             }
563             if (REPOST_ACTION.equals(intent.getAction())) {
564                 repost(intent.getStringExtra(EXTRA_KEY), intent.getIntExtra(EXTRA_USER_ID,
565                         UserHandle.USER_SYSTEM), false);
566             }
567         }
568     };
569 }
570