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