blob: 91f497cf9607a4e9aed4d7b204f37b383cd0ac0f [file] [log] [blame]
Julia Reynolds72f1cbb2016-09-19 14:57:31 -04001/*
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 */
16package com.android.server.notification;
17
Julia Reynolds520df6e2017-02-13 09:05:10 -050018import android.annotation.NonNull;
Julia Reynolds72f1cbb2016-09-19 14:57:31 -040019import android.app.AlarmManager;
20import android.app.PendingIntent;
21import android.content.BroadcastReceiver;
22import android.content.Context;
23import android.content.Intent;
24import android.content.IntentFilter;
25import android.net.Uri;
Julia Reynolds79672302017-01-12 08:30:16 -050026import android.os.Binder;
Julia Reynolds50989772017-02-23 14:32:16 -050027import android.os.SystemClock;
Julia Reynoldsb6c1f992016-11-22 09:26:46 -050028import android.os.UserHandle;
Julia Reynolds72f1cbb2016-09-19 14:57:31 -040029import android.service.notification.StatusBarNotification;
30import android.util.ArrayMap;
Julia Reynoldsca8e5352018-09-18 13:39:26 -040031import android.util.IntArray;
Julia Reynolds72f1cbb2016-09-19 14:57:31 -040032import android.util.Log;
33import android.util.Slog;
34
Julia Reynoldsca8e5352018-09-18 13:39:26 -040035import com.android.internal.annotations.VisibleForTesting;
36import com.android.internal.logging.MetricsLogger;
37import com.android.internal.logging.nano.MetricsProto;
38
39import org.xmlpull.v1.XmlPullParser;
40import org.xmlpull.v1.XmlPullParserException;
41import org.xmlpull.v1.XmlSerializer;
42
Julia Reynolds72f1cbb2016-09-19 14:57:31 -040043import java.io.IOException;
44import java.io.PrintWriter;
Chris Wren6676dab2016-12-21 18:26:27 -050045import java.util.ArrayList;
46import java.util.Collection;
47import java.util.Collections;
Julia Reynolds72f1cbb2016-09-19 14:57:31 -040048import java.util.Date;
Julia Reynoldscf63ff12017-01-24 13:55:48 -050049import java.util.List;
Julia Reynolds72f1cbb2016-09-19 14:57:31 -040050import java.util.Map;
51import java.util.Objects;
52import java.util.Set;
53
54/**
55 * NotificationManagerService helper for handling snoozed notifications.
56 */
57public 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";
Julia Reynolds72f1cbb2016-09-19 14:57:31 -040065 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<>();
Julia Reynoldsb6c1f992016-11-22 09:26:46 -050075 // notification key : package.
76 private ArrayMap<String, String> mPackages = new ArrayMap<>();
Julia Reynolds79672302017-01-12 08:30:16 -050077 // key : userId
78 private ArrayMap<String, Integer> mUsers = new ArrayMap<>();
Julia Reynolds72f1cbb2016-09-19 14:57:31 -040079 private Callback mCallback;
80
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
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
Chris Wren6676dab2016-12-21 18:26:27 -050098 protected Collection<NotificationRecord> getSnoozed(int userId, String pkg) {
99 if (mSnoozedNotifications.containsKey(userId)
100 && mSnoozedNotifications.get(userId).containsKey(pkg)) {
Julia Reynoldsa78cdff2017-04-26 10:19:25 -0400101 return mSnoozedNotifications.get(userId).get(pkg).values();
Chris Wren6676dab2016-12-21 18:26:27 -0500102 }
103 return Collections.EMPTY_LIST;
104 }
105
Jay Aliomer24642da2019-07-30 09:57:41 -0400106 @NonNull
107 ArrayList<NotificationRecord> getNotifications(String pkg,
108 String groupKey, Integer userId) {
109 ArrayList<NotificationRecord> records = new ArrayList<>();
110 if (mSnoozedNotifications.containsKey(userId)
111 && mSnoozedNotifications.get(userId).containsKey(pkg)) {
112 ArrayMap<String, NotificationRecord> packages =
113 mSnoozedNotifications.get(userId).get(pkg);
114 for (int i = 0; i < packages.size(); i++) {
115 String currentGroupKey = packages.valueAt(i).sbn.getGroup();
116 if (currentGroupKey.equals(groupKey)) {
117 records.add(packages.valueAt(i));
118 }
119 }
120 }
121 return records;
122 }
123
124 protected NotificationRecord getNotification(String key) {
125 List<NotificationRecord> snoozedForUser = new ArrayList<>();
126 IntArray userIds = mUserProfiles.getCurrentProfileIds();
127 if (userIds != null) {
128 final int userIdsSize = userIds.size();
129 for (int i = 0; i < userIdsSize; i++) {
130 final ArrayMap<String, ArrayMap<String, NotificationRecord>> snoozedPkgs =
131 mSnoozedNotifications.get(userIds.get(i));
132 if (snoozedPkgs != null) {
133 final int snoozedPkgsSize = snoozedPkgs.size();
134 for (int j = 0; j < snoozedPkgsSize; j++) {
135 final ArrayMap<String, NotificationRecord> records = snoozedPkgs.valueAt(j);
136 if (records != null) {
137 return records.get(key);
138 }
139 }
140 }
141 }
142 }
143 return null;
144 }
145
Julia Reynolds520df6e2017-02-13 09:05:10 -0500146 protected @NonNull List<NotificationRecord> getSnoozed() {
Julia Reynoldscf63ff12017-01-24 13:55:48 -0500147 List<NotificationRecord> snoozedForUser = new ArrayList<>();
Julia Reynoldsca8e5352018-09-18 13:39:26 -0400148 IntArray userIds = mUserProfiles.getCurrentProfileIds();
Julia Reynoldsa78cdff2017-04-26 10:19:25 -0400149 if (userIds != null) {
Julia Reynoldsca8e5352018-09-18 13:39:26 -0400150 final int N = userIds.size();
Julia Reynoldsa78cdff2017-04-26 10:19:25 -0400151 for (int i = 0; i < N; i++) {
152 final ArrayMap<String, ArrayMap<String, NotificationRecord>> snoozedPkgs =
Julia Reynoldsca8e5352018-09-18 13:39:26 -0400153 mSnoozedNotifications.get(userIds.get(i));
Julia Reynoldsa78cdff2017-04-26 10:19:25 -0400154 if (snoozedPkgs != null) {
155 final int M = snoozedPkgs.size();
156 for (int j = 0; j < M; j++) {
157 final ArrayMap<String, NotificationRecord> records = snoozedPkgs.valueAt(j);
158 if (records != null) {
159 snoozedForUser.addAll(records.values());
160 }
Julia Reynoldscf63ff12017-01-24 13:55:48 -0500161 }
162 }
163 }
164 }
165 return snoozedForUser;
166 }
167
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400168 /**
Julia Reynoldsb6c1f992016-11-22 09:26:46 -0500169 * Snoozes a notification and schedules an alarm to repost at that time.
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400170 */
Julia Reynolds50989772017-02-23 14:32:16 -0500171 protected void snooze(NotificationRecord record, long duration) {
Julia Reynolds79672302017-01-12 08:30:16 -0500172 snooze(record);
Julia Reynolds50989772017-02-23 14:32:16 -0500173 scheduleRepost(record.sbn.getPackageName(), record.getKey(), record.getUserId(), duration);
Julia Reynoldsb6c1f992016-11-22 09:26:46 -0500174 }
175
176 /**
177 * Records a snoozed notification.
178 */
Julia Reynolds79672302017-01-12 08:30:16 -0500179 protected void snooze(NotificationRecord record) {
180 int userId = record.getUser().getIdentifier();
Julia Reynoldsb6c1f992016-11-22 09:26:46 -0500181 if (DEBUG) {
182 Slog.d(TAG, "Snoozing " + record.getKey());
183 }
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400184 ArrayMap<String, ArrayMap<String, NotificationRecord>> records =
185 mSnoozedNotifications.get(userId);
186 if (records == null) {
187 records = new ArrayMap<>();
188 }
189 ArrayMap<String, NotificationRecord> pkgRecords = records.get(record.sbn.getPackageName());
190 if (pkgRecords == null) {
191 pkgRecords = new ArrayMap<>();
192 }
193 pkgRecords.put(record.getKey(), record);
194 records.put(record.sbn.getPackageName(), pkgRecords);
195 mSnoozedNotifications.put(userId, records);
Julia Reynoldsb6c1f992016-11-22 09:26:46 -0500196 mPackages.put(record.getKey(), record.sbn.getPackageName());
Julia Reynolds79672302017-01-12 08:30:16 -0500197 mUsers.put(record.getKey(), userId);
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400198 }
199
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400200 protected boolean cancel(int userId, String pkg, String tag, int id) {
201 if (mSnoozedNotifications.containsKey(userId)) {
202 ArrayMap<String, NotificationRecord> recordsForPkg =
203 mSnoozedNotifications.get(userId).get(pkg);
204 if (recordsForPkg != null) {
205 final Set<Map.Entry<String, NotificationRecord>> records = recordsForPkg.entrySet();
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400206 for (Map.Entry<String, NotificationRecord> record : records) {
207 final StatusBarNotification sbn = record.getValue().sbn;
208 if (Objects.equals(sbn.getTag(), tag) && sbn.getId() == id) {
Julia Reynoldsa8b766f2017-03-07 16:30:21 -0500209 record.getValue().isCanceled = true;
210 return true;
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400211 }
212 }
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400213 }
214 }
215 return false;
216 }
217
218 protected boolean cancel(int userId, boolean includeCurrentProfiles) {
219 int[] userIds = {userId};
220 if (includeCurrentProfiles) {
Julia Reynoldsca8e5352018-09-18 13:39:26 -0400221 userIds = mUserProfiles.getCurrentProfileIds().toArray();
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400222 }
223 final int N = userIds.length;
224 for (int i = 0; i < N; i++) {
225 final ArrayMap<String, ArrayMap<String, NotificationRecord>> snoozedPkgs =
Julia Reynoldsa8b766f2017-03-07 16:30:21 -0500226 mSnoozedNotifications.get(userIds[i]);
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400227 if (snoozedPkgs != null) {
228 final int M = snoozedPkgs.size();
229 for (int j = 0; j < M; j++) {
230 final ArrayMap<String, NotificationRecord> records = snoozedPkgs.valueAt(j);
231 if (records != null) {
232 int P = records.size();
233 for (int k = 0; k < P; k++) {
Julia Reynoldsa8b766f2017-03-07 16:30:21 -0500234 records.valueAt(k).isCanceled = true;
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400235 }
236 }
237 }
238 return true;
239 }
240 }
241 return false;
242 }
243
244 protected boolean cancel(int userId, String pkg) {
245 if (mSnoozedNotifications.containsKey(userId)) {
246 if (mSnoozedNotifications.get(userId).containsKey(pkg)) {
247 ArrayMap<String, NotificationRecord> records =
Julia Reynoldsa8b766f2017-03-07 16:30:21 -0500248 mSnoozedNotifications.get(userId).get(pkg);
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400249 int N = records.size();
250 for (int i = 0; i < N; i++) {
Julia Reynoldsa8b766f2017-03-07 16:30:21 -0500251 records.valueAt(i).isCanceled = true;
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400252 }
253 return true;
254 }
255 }
256 return false;
257 }
258
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400259 /**
260 * Updates the notification record so the most up to date information is shown on re-post.
261 */
262 protected void update(int userId, NotificationRecord record) {
263 ArrayMap<String, ArrayMap<String, NotificationRecord>> records =
264 mSnoozedNotifications.get(userId);
265 if (records == null) {
266 return;
267 }
268 ArrayMap<String, NotificationRecord> pkgRecords = records.get(record.sbn.getPackageName());
269 if (pkgRecords == null) {
270 return;
271 }
Julia Reynoldsa8b766f2017-03-07 16:30:21 -0500272 NotificationRecord existing = pkgRecords.get(record.getKey());
273 if (existing != null && existing.isCanceled) {
274 return;
275 }
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400276 pkgRecords.put(record.getKey(), record);
277 }
278
Julia Reynolds79672302017-01-12 08:30:16 -0500279 protected void repost(String key) {
280 Integer userId = mUsers.get(key);
281 if (userId != null) {
282 repost(key, userId);
283 }
284 }
285
Julia Reynoldsb6c1f992016-11-22 09:26:46 -0500286 protected void repost(String key, int userId) {
287 final String pkg = mPackages.remove(key);
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400288 ArrayMap<String, ArrayMap<String, NotificationRecord>> records =
289 mSnoozedNotifications.get(userId);
290 if (records == null) {
291 return;
292 }
293 ArrayMap<String, NotificationRecord> pkgRecords = records.get(pkg);
294 if (pkgRecords == null) {
295 return;
296 }
297 final NotificationRecord record = pkgRecords.remove(key);
Julia Reynoldsa8b766f2017-03-07 16:30:21 -0500298 mPackages.remove(key);
299 mUsers.remove(key);
Julia Reynoldsb6c1f992016-11-22 09:26:46 -0500300
Julia Reynoldsa8b766f2017-03-07 16:30:21 -0500301 if (record != null && !record.isCanceled) {
Julia Reynolds520df6e2017-02-13 09:05:10 -0500302 MetricsLogger.action(record.getLogMaker()
303 .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED)
304 .setType(MetricsProto.MetricsEvent.TYPE_OPEN));
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400305 mCallback.repost(userId, record);
306 }
307 }
308
Julia Reynoldsa78cdff2017-04-26 10:19:25 -0400309 protected void repostGroupSummary(String pkg, int userId, String groupKey) {
310 if (mSnoozedNotifications.containsKey(userId)) {
311 ArrayMap<String, ArrayMap<String, NotificationRecord>> keysByPackage
312 = mSnoozedNotifications.get(userId);
313
314 if (keysByPackage != null && keysByPackage.containsKey(pkg)) {
315 ArrayMap<String, NotificationRecord> recordsByKey = keysByPackage.get(pkg);
316
317 if (recordsByKey != null) {
318 String groupSummaryKey = null;
319 int N = recordsByKey.size();
320 for (int i = 0; i < N; i++) {
321 final NotificationRecord potentialGroupSummary = recordsByKey.valueAt(i);
322 if (potentialGroupSummary.sbn.isGroup()
323 && potentialGroupSummary.getNotification().isGroupSummary()
324 && groupKey.equals(potentialGroupSummary.getGroupKey())) {
325 groupSummaryKey = potentialGroupSummary.getKey();
326 break;
327 }
328 }
329
330 if (groupSummaryKey != null) {
331 NotificationRecord record = recordsByKey.remove(groupSummaryKey);
332 mPackages.remove(groupSummaryKey);
333 mUsers.remove(groupSummaryKey);
334
Julia Reynoldsa8b766f2017-03-07 16:30:21 -0500335 if (record != null && !record.isCanceled) {
336 MetricsLogger.action(record.getLogMaker()
337 .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED)
338 .setType(MetricsProto.MetricsEvent.TYPE_OPEN));
339 mCallback.repost(userId, record);
340 }
Julia Reynoldsa78cdff2017-04-26 10:19:25 -0400341 }
342 }
343 }
344 }
345 }
346
Julia Reynolds67c1e962019-01-04 14:01:10 -0500347 protected void clearData(int userId, String pkg) {
348 ArrayMap<String, ArrayMap<String, NotificationRecord>> records =
349 mSnoozedNotifications.get(userId);
350 if (records == null) {
351 return;
352 }
353 ArrayMap<String, NotificationRecord> pkgRecords = records.get(pkg);
354 if (pkgRecords == null) {
355 return;
356 }
357 for (int i = pkgRecords.size() - 1; i >= 0; i--) {
358 final NotificationRecord r = pkgRecords.removeAt(i);
359 if (r != null) {
360 mPackages.remove(r.getKey());
361 mUsers.remove(r.getKey());
362 final PendingIntent pi = createPendingIntent(pkg, r.getKey(), userId);
363 mAm.cancel(pi);
364 MetricsLogger.action(r.getLogMaker()
365 .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED)
366 .setType(MetricsProto.MetricsEvent.TYPE_DISMISS));
367 }
368 }
369 }
370
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400371 private PendingIntent createPendingIntent(String pkg, String key, int userId) {
372 return PendingIntent.getBroadcast(mContext,
373 REQUEST_CODE_REPOST,
374 new Intent(REPOST_ACTION)
375 .setData(new Uri.Builder().scheme(REPOST_SCHEME).appendPath(key).build())
376 .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400377 .putExtra(EXTRA_KEY, key)
378 .putExtra(EXTRA_USER_ID, userId),
379 PendingIntent.FLAG_UPDATE_CURRENT);
380 }
381
Julia Reynolds50989772017-02-23 14:32:16 -0500382 private void scheduleRepost(String pkg, String key, int userId, long duration) {
Julia Reynolds79672302017-01-12 08:30:16 -0500383 long identity = Binder.clearCallingIdentity();
384 try {
385 final PendingIntent pi = createPendingIntent(pkg, key, userId);
386 mAm.cancel(pi);
Julia Reynolds50989772017-02-23 14:32:16 -0500387 long time = SystemClock.elapsedRealtime() + duration;
Julia Reynolds79672302017-01-12 08:30:16 -0500388 if (DEBUG) Slog.d(TAG, "Scheduling evaluate for " + new Date(time));
Julia Reynolds50989772017-02-23 14:32:16 -0500389 mAm.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, time, pi);
Julia Reynolds79672302017-01-12 08:30:16 -0500390 } finally {
391 Binder.restoreCallingIdentity(identity);
392 }
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400393 }
394
395 public void dump(PrintWriter pw, NotificationManagerService.DumpFilter filter) {
396 pw.println("\n Snoozed notifications:");
397 for (int userId : mSnoozedNotifications.keySet()) {
398 pw.print(INDENT);
399 pw.println("user: " + userId);
400 ArrayMap<String, ArrayMap<String, NotificationRecord>> snoozedPkgs =
401 mSnoozedNotifications.get(userId);
402 for (String pkg : snoozedPkgs.keySet()) {
403 pw.print(INDENT);
404 pw.print(INDENT);
405 pw.println("package: " + pkg);
406 Set<String> snoozedKeys = snoozedPkgs.get(pkg).keySet();
407 for (String key : snoozedKeys) {
408 pw.print(INDENT);
409 pw.print(INDENT);
410 pw.print(INDENT);
411 pw.println(key);
412 }
413 }
414 }
415 }
416
417 protected void writeXml(XmlSerializer out, boolean forBackup) throws IOException {
418
419 }
420
421 public void readXml(XmlPullParser parser, boolean forRestore)
422 throws XmlPullParserException, IOException {
423
424 }
425
426 @VisibleForTesting
427 void setAlarmManager(AlarmManager am) {
428 mAm = am;
429 }
430
431 protected interface Callback {
432 void repost(int userId, NotificationRecord r);
433 }
434
435 private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
436 @Override
437 public void onReceive(Context context, Intent intent) {
438 if (DEBUG) {
439 Slog.d(TAG, "Reposting notification");
440 }
441 if (REPOST_ACTION.equals(intent.getAction())) {
Julia Reynoldsb6c1f992016-11-22 09:26:46 -0500442 repost(intent.getStringExtra(EXTRA_KEY), intent.getIntExtra(EXTRA_USER_ID,
443 UserHandle.USER_SYSTEM));
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400444 }
445 }
446 };
447}