• 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 com.android.internal.annotations.VisibleForTesting;
19 import com.android.internal.logging.MetricsLogger;
20 import com.android.internal.logging.nano.MetricsProto;
21 
22 import org.xmlpull.v1.XmlPullParser;
23 import org.xmlpull.v1.XmlPullParserException;
24 import org.xmlpull.v1.XmlSerializer;
25 
26 import android.annotation.NonNull;
27 import android.app.AlarmManager;
28 import android.app.Notification;
29 import android.app.PendingIntent;
30 import android.content.BroadcastReceiver;
31 import android.content.Context;
32 import android.content.Intent;
33 import android.content.IntentFilter;
34 import android.net.Uri;
35 import android.os.Binder;
36 import android.os.SystemClock;
37 import android.os.UserHandle;
38 import android.service.notification.StatusBarNotification;
39 import android.util.ArrayMap;
40 import android.util.Log;
41 import android.util.Slog;
42 
43 import java.io.IOException;
44 import java.io.PrintWriter;
45 import java.util.ArrayList;
46 import java.util.Collection;
47 import java.util.Collections;
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 class SnoozeHelper {
58     private static final String TAG = "SnoozeHelper";
59     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
60     private static final String INDENT = "    ";
61 
62     private static final String REPOST_ACTION = SnoozeHelper.class.getSimpleName() + ".EVALUATE";
63     private static final int REQUEST_CODE_REPOST = 1;
64     private static final String REPOST_SCHEME = "repost";
65     private static final String EXTRA_KEY = "key";
66     private static final String EXTRA_USER_ID = "userId";
67 
68     private final Context mContext;
69     private AlarmManager mAm;
70     private final ManagedServices.UserProfiles mUserProfiles;
71 
72     // User id : package name : notification key : record.
73     private ArrayMap<Integer, ArrayMap<String, ArrayMap<String, NotificationRecord>>>
74             mSnoozedNotifications = new ArrayMap<>();
75     // notification key : package.
76     private ArrayMap<String, String> mPackages = new ArrayMap<>();
77     // key : userId
78     private ArrayMap<String, Integer> mUsers = new ArrayMap<>();
79     private Callback mCallback;
80 
SnoozeHelper(Context context, Callback callback, ManagedServices.UserProfiles userProfiles)81     public SnoozeHelper(Context context, Callback callback,
82             ManagedServices.UserProfiles userProfiles) {
83         mContext = context;
84         IntentFilter filter = new IntentFilter(REPOST_ACTION);
85         filter.addDataScheme(REPOST_SCHEME);
86         mContext.registerReceiver(mBroadcastReceiver, filter);
87         mAm = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
88         mCallback = callback;
89         mUserProfiles = userProfiles;
90     }
91 
isSnoozed(int userId, String pkg, String key)92     protected boolean isSnoozed(int userId, String pkg, String key) {
93         return mSnoozedNotifications.containsKey(userId)
94                 && mSnoozedNotifications.get(userId).containsKey(pkg)
95                 && mSnoozedNotifications.get(userId).get(pkg).containsKey(key);
96     }
97 
getSnoozed(int userId, String pkg)98     protected Collection<NotificationRecord> getSnoozed(int userId, String pkg) {
99         if (mSnoozedNotifications.containsKey(userId)
100                 && mSnoozedNotifications.get(userId).containsKey(pkg)) {
101             return mSnoozedNotifications.get(userId).get(pkg).values();
102         }
103         return Collections.EMPTY_LIST;
104     }
105 
getSnoozed()106     protected @NonNull List<NotificationRecord> getSnoozed() {
107         List<NotificationRecord> snoozedForUser = new ArrayList<>();
108         int[] userIds = mUserProfiles.getCurrentProfileIds();
109         if (userIds != null) {
110             final int N = userIds.length;
111             for (int i = 0; i < N; i++) {
112                 final ArrayMap<String, ArrayMap<String, NotificationRecord>> snoozedPkgs =
113                         mSnoozedNotifications.get(userIds[i]);
114                 if (snoozedPkgs != null) {
115                     final int M = snoozedPkgs.size();
116                     for (int j = 0; j < M; j++) {
117                         final ArrayMap<String, NotificationRecord> records = snoozedPkgs.valueAt(j);
118                         if (records != null) {
119                             snoozedForUser.addAll(records.values());
120                         }
121                     }
122                 }
123             }
124         }
125         return snoozedForUser;
126     }
127 
128     /**
129      * Snoozes a notification and schedules an alarm to repost at that time.
130      */
snooze(NotificationRecord record, long duration)131     protected void snooze(NotificationRecord record, long duration) {
132         snooze(record);
133         scheduleRepost(record.sbn.getPackageName(), record.getKey(), record.getUserId(), duration);
134     }
135 
136     /**
137      * Records a snoozed notification.
138      */
snooze(NotificationRecord record)139     protected void snooze(NotificationRecord record) {
140         int userId = record.getUser().getIdentifier();
141         if (DEBUG) {
142             Slog.d(TAG, "Snoozing " + record.getKey());
143         }
144         ArrayMap<String, ArrayMap<String, NotificationRecord>> records =
145                 mSnoozedNotifications.get(userId);
146         if (records == null) {
147             records = new ArrayMap<>();
148         }
149         ArrayMap<String, NotificationRecord> pkgRecords = records.get(record.sbn.getPackageName());
150         if (pkgRecords == null) {
151             pkgRecords = new ArrayMap<>();
152         }
153         pkgRecords.put(record.getKey(), record);
154         records.put(record.sbn.getPackageName(), pkgRecords);
155         mSnoozedNotifications.put(userId, records);
156         mPackages.put(record.getKey(), record.sbn.getPackageName());
157         mUsers.put(record.getKey(), userId);
158     }
159 
cancel(int userId, String pkg, String tag, int id)160     protected boolean cancel(int userId, String pkg, String tag, int id) {
161         if (mSnoozedNotifications.containsKey(userId)) {
162             ArrayMap<String, NotificationRecord> recordsForPkg =
163                     mSnoozedNotifications.get(userId).get(pkg);
164             if (recordsForPkg != null) {
165                 final Set<Map.Entry<String, NotificationRecord>> records = recordsForPkg.entrySet();
166                 String key = null;
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();
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 
createPendingIntent(String pkg, String key, int userId)308     private PendingIntent createPendingIntent(String pkg, String key, int userId) {
309         return PendingIntent.getBroadcast(mContext,
310                 REQUEST_CODE_REPOST,
311                 new Intent(REPOST_ACTION)
312                         .setData(new Uri.Builder().scheme(REPOST_SCHEME).appendPath(key).build())
313                         .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
314                         .putExtra(EXTRA_KEY, key)
315                         .putExtra(EXTRA_USER_ID, userId),
316                 PendingIntent.FLAG_UPDATE_CURRENT);
317     }
318 
scheduleRepost(String pkg, String key, int userId, long duration)319     private void scheduleRepost(String pkg, String key, int userId, long duration) {
320         long identity = Binder.clearCallingIdentity();
321         try {
322             final PendingIntent pi = createPendingIntent(pkg, key, userId);
323             mAm.cancel(pi);
324             long time = SystemClock.elapsedRealtime() + duration;
325             if (DEBUG) Slog.d(TAG, "Scheduling evaluate for " + new Date(time));
326             mAm.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, time, pi);
327         } finally {
328             Binder.restoreCallingIdentity(identity);
329         }
330     }
331 
dump(PrintWriter pw, NotificationManagerService.DumpFilter filter)332     public void dump(PrintWriter pw, NotificationManagerService.DumpFilter filter) {
333         pw.println("\n  Snoozed notifications:");
334         for (int userId : mSnoozedNotifications.keySet()) {
335             pw.print(INDENT);
336             pw.println("user: " + userId);
337             ArrayMap<String, ArrayMap<String, NotificationRecord>> snoozedPkgs =
338                     mSnoozedNotifications.get(userId);
339             for (String pkg : snoozedPkgs.keySet()) {
340                 pw.print(INDENT);
341                 pw.print(INDENT);
342                 pw.println("package: " + pkg);
343                 Set<String> snoozedKeys = snoozedPkgs.get(pkg).keySet();
344                 for (String key : snoozedKeys) {
345                     pw.print(INDENT);
346                     pw.print(INDENT);
347                     pw.print(INDENT);
348                     pw.println(key);
349                 }
350             }
351         }
352     }
353 
writeXml(XmlSerializer out, boolean forBackup)354     protected void writeXml(XmlSerializer out, boolean forBackup) throws IOException {
355 
356     }
357 
readXml(XmlPullParser parser, boolean forRestore)358     public void readXml(XmlPullParser parser, boolean forRestore)
359             throws XmlPullParserException, IOException {
360 
361     }
362 
363     @VisibleForTesting
setAlarmManager(AlarmManager am)364     void setAlarmManager(AlarmManager am) {
365         mAm = am;
366     }
367 
368     protected interface Callback {
repost(int userId, NotificationRecord r)369         void repost(int userId, NotificationRecord r);
370     }
371 
372     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
373         @Override
374         public void onReceive(Context context, Intent intent) {
375             if (DEBUG) {
376                 Slog.d(TAG, "Reposting notification");
377             }
378             if (REPOST_ACTION.equals(intent.getAction())) {
379                 repost(intent.getStringExtra(EXTRA_KEY), intent.getIntExtra(EXTRA_USER_ID,
380                         UserHandle.USER_SYSTEM));
381             }
382         }
383     };
384 }
385