blob: fbb6d6a064337675a6140cb2d47813964d165d62 [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;
Julia Reynolds67c1e962019-01-04 14:01:10 -050020import android.app.Notification;
Julia Reynolds72f1cbb2016-09-19 14:57:31 -040021import android.app.PendingIntent;
22import android.content.BroadcastReceiver;
23import android.content.Context;
24import android.content.Intent;
25import android.content.IntentFilter;
26import android.net.Uri;
Julia Reynolds79672302017-01-12 08:30:16 -050027import android.os.Binder;
Julia Reynolds50989772017-02-23 14:32:16 -050028import android.os.SystemClock;
Julia Reynoldsb6c1f992016-11-22 09:26:46 -050029import android.os.UserHandle;
Julia Reynolds72f1cbb2016-09-19 14:57:31 -040030import android.service.notification.StatusBarNotification;
31import android.util.ArrayMap;
Julia Reynoldsca8e5352018-09-18 13:39:26 -040032import android.util.IntArray;
Julia Reynolds72f1cbb2016-09-19 14:57:31 -040033import android.util.Log;
34import android.util.Slog;
35
Julia Reynoldsca8e5352018-09-18 13:39:26 -040036import com.android.internal.annotations.VisibleForTesting;
37import com.android.internal.logging.MetricsLogger;
38import com.android.internal.logging.nano.MetricsProto;
39
40import org.xmlpull.v1.XmlPullParser;
41import org.xmlpull.v1.XmlPullParserException;
42import org.xmlpull.v1.XmlSerializer;
43
Julia Reynolds72f1cbb2016-09-19 14:57:31 -040044import java.io.IOException;
45import java.io.PrintWriter;
Chris Wren6676dab2016-12-21 18:26:27 -050046import java.util.ArrayList;
47import java.util.Collection;
48import java.util.Collections;
Julia Reynolds72f1cbb2016-09-19 14:57:31 -040049import java.util.Date;
Julia Reynoldscf63ff12017-01-24 13:55:48 -050050import java.util.List;
Julia Reynolds72f1cbb2016-09-19 14:57:31 -040051import java.util.Map;
52import java.util.Objects;
53import java.util.Set;
54
55/**
56 * NotificationManagerService helper for handling snoozed notifications.
57 */
58public class SnoozeHelper {
59 private static final String TAG = "SnoozeHelper";
60 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
61 private static final String INDENT = " ";
62
63 private static final String REPOST_ACTION = SnoozeHelper.class.getSimpleName() + ".EVALUATE";
64 private static final int REQUEST_CODE_REPOST = 1;
65 private static final String REPOST_SCHEME = "repost";
Julia Reynolds72f1cbb2016-09-19 14:57:31 -040066 private static final String EXTRA_KEY = "key";
67 private static final String EXTRA_USER_ID = "userId";
68
69 private final Context mContext;
70 private AlarmManager mAm;
71 private final ManagedServices.UserProfiles mUserProfiles;
72
73 // User id : package name : notification key : record.
74 private ArrayMap<Integer, ArrayMap<String, ArrayMap<String, NotificationRecord>>>
75 mSnoozedNotifications = new ArrayMap<>();
Julia Reynoldsb6c1f992016-11-22 09:26:46 -050076 // notification key : package.
77 private ArrayMap<String, String> mPackages = new ArrayMap<>();
Julia Reynolds79672302017-01-12 08:30:16 -050078 // key : userId
79 private ArrayMap<String, Integer> mUsers = new ArrayMap<>();
Julia Reynolds72f1cbb2016-09-19 14:57:31 -040080 private Callback mCallback;
81
82 public SnoozeHelper(Context context, Callback callback,
83 ManagedServices.UserProfiles userProfiles) {
84 mContext = context;
85 IntentFilter filter = new IntentFilter(REPOST_ACTION);
86 filter.addDataScheme(REPOST_SCHEME);
87 mContext.registerReceiver(mBroadcastReceiver, filter);
88 mAm = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
89 mCallback = callback;
90 mUserProfiles = userProfiles;
91 }
92
93 protected boolean isSnoozed(int userId, String pkg, String key) {
94 return mSnoozedNotifications.containsKey(userId)
95 && mSnoozedNotifications.get(userId).containsKey(pkg)
96 && mSnoozedNotifications.get(userId).get(pkg).containsKey(key);
97 }
98
Chris Wren6676dab2016-12-21 18:26:27 -050099 protected Collection<NotificationRecord> getSnoozed(int userId, String pkg) {
100 if (mSnoozedNotifications.containsKey(userId)
101 && mSnoozedNotifications.get(userId).containsKey(pkg)) {
Julia Reynoldsa78cdff2017-04-26 10:19:25 -0400102 return mSnoozedNotifications.get(userId).get(pkg).values();
Chris Wren6676dab2016-12-21 18:26:27 -0500103 }
104 return Collections.EMPTY_LIST;
105 }
106
Julia Reynolds520df6e2017-02-13 09:05:10 -0500107 protected @NonNull List<NotificationRecord> getSnoozed() {
Julia Reynoldscf63ff12017-01-24 13:55:48 -0500108 List<NotificationRecord> snoozedForUser = new ArrayList<>();
Julia Reynoldsca8e5352018-09-18 13:39:26 -0400109 IntArray userIds = mUserProfiles.getCurrentProfileIds();
Julia Reynoldsa78cdff2017-04-26 10:19:25 -0400110 if (userIds != null) {
Julia Reynoldsca8e5352018-09-18 13:39:26 -0400111 final int N = userIds.size();
Julia Reynoldsa78cdff2017-04-26 10:19:25 -0400112 for (int i = 0; i < N; i++) {
113 final ArrayMap<String, ArrayMap<String, NotificationRecord>> snoozedPkgs =
Julia Reynoldsca8e5352018-09-18 13:39:26 -0400114 mSnoozedNotifications.get(userIds.get(i));
Julia Reynoldsa78cdff2017-04-26 10:19:25 -0400115 if (snoozedPkgs != null) {
116 final int M = snoozedPkgs.size();
117 for (int j = 0; j < M; j++) {
118 final ArrayMap<String, NotificationRecord> records = snoozedPkgs.valueAt(j);
119 if (records != null) {
120 snoozedForUser.addAll(records.values());
121 }
Julia Reynoldscf63ff12017-01-24 13:55:48 -0500122 }
123 }
124 }
125 }
126 return snoozedForUser;
127 }
128
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400129 /**
Julia Reynoldsb6c1f992016-11-22 09:26:46 -0500130 * Snoozes a notification and schedules an alarm to repost at that time.
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400131 */
Julia Reynolds50989772017-02-23 14:32:16 -0500132 protected void snooze(NotificationRecord record, long duration) {
Julia Reynolds79672302017-01-12 08:30:16 -0500133 snooze(record);
Julia Reynolds50989772017-02-23 14:32:16 -0500134 scheduleRepost(record.sbn.getPackageName(), record.getKey(), record.getUserId(), duration);
Julia Reynoldsb6c1f992016-11-22 09:26:46 -0500135 }
136
137 /**
138 * Records a snoozed notification.
139 */
Julia Reynolds79672302017-01-12 08:30:16 -0500140 protected void snooze(NotificationRecord record) {
141 int userId = record.getUser().getIdentifier();
Julia Reynoldsb6c1f992016-11-22 09:26:46 -0500142 if (DEBUG) {
143 Slog.d(TAG, "Snoozing " + record.getKey());
144 }
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400145 ArrayMap<String, ArrayMap<String, NotificationRecord>> records =
146 mSnoozedNotifications.get(userId);
147 if (records == null) {
148 records = new ArrayMap<>();
149 }
150 ArrayMap<String, NotificationRecord> pkgRecords = records.get(record.sbn.getPackageName());
151 if (pkgRecords == null) {
152 pkgRecords = new ArrayMap<>();
153 }
154 pkgRecords.put(record.getKey(), record);
155 records.put(record.sbn.getPackageName(), pkgRecords);
156 mSnoozedNotifications.put(userId, records);
Julia Reynoldsb6c1f992016-11-22 09:26:46 -0500157 mPackages.put(record.getKey(), record.sbn.getPackageName());
Julia Reynolds79672302017-01-12 08:30:16 -0500158 mUsers.put(record.getKey(), userId);
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400159 }
160
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400161 protected boolean cancel(int userId, String pkg, String tag, int id) {
162 if (mSnoozedNotifications.containsKey(userId)) {
163 ArrayMap<String, NotificationRecord> recordsForPkg =
164 mSnoozedNotifications.get(userId).get(pkg);
165 if (recordsForPkg != null) {
166 final Set<Map.Entry<String, NotificationRecord>> records = recordsForPkg.entrySet();
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400167 for (Map.Entry<String, NotificationRecord> record : records) {
168 final StatusBarNotification sbn = record.getValue().sbn;
169 if (Objects.equals(sbn.getTag(), tag) && sbn.getId() == id) {
Julia Reynoldsa8b766f2017-03-07 16:30:21 -0500170 record.getValue().isCanceled = true;
171 return true;
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400172 }
173 }
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400174 }
175 }
176 return false;
177 }
178
179 protected boolean cancel(int userId, boolean includeCurrentProfiles) {
180 int[] userIds = {userId};
181 if (includeCurrentProfiles) {
Julia Reynoldsca8e5352018-09-18 13:39:26 -0400182 userIds = mUserProfiles.getCurrentProfileIds().toArray();
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400183 }
184 final int N = userIds.length;
185 for (int i = 0; i < N; i++) {
186 final ArrayMap<String, ArrayMap<String, NotificationRecord>> snoozedPkgs =
Julia Reynoldsa8b766f2017-03-07 16:30:21 -0500187 mSnoozedNotifications.get(userIds[i]);
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400188 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++) {
Julia Reynoldsa8b766f2017-03-07 16:30:21 -0500195 records.valueAt(k).isCanceled = true;
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400196 }
197 }
198 }
199 return true;
200 }
201 }
202 return false;
203 }
204
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 =
Julia Reynoldsa8b766f2017-03-07 16:30:21 -0500209 mSnoozedNotifications.get(userId).get(pkg);
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400210 int N = records.size();
211 for (int i = 0; i < N; i++) {
Julia Reynoldsa8b766f2017-03-07 16:30:21 -0500212 records.valueAt(i).isCanceled = true;
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400213 }
214 return true;
215 }
216 }
217 return false;
218 }
219
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400220 /**
221 * Updates the notification record so the most up to date information is shown on re-post.
222 */
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 }
Julia Reynoldsa8b766f2017-03-07 16:30:21 -0500233 NotificationRecord existing = pkgRecords.get(record.getKey());
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400234 pkgRecords.put(record.getKey(), record);
235 }
236
Julia Reynolds79672302017-01-12 08:30:16 -0500237 protected void repost(String key) {
238 Integer userId = mUsers.get(key);
239 if (userId != null) {
240 repost(key, userId);
241 }
242 }
243
Julia Reynoldsb6c1f992016-11-22 09:26:46 -0500244 protected void repost(String key, int userId) {
245 final String pkg = mPackages.remove(key);
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400246 ArrayMap<String, ArrayMap<String, NotificationRecord>> records =
247 mSnoozedNotifications.get(userId);
248 if (records == null) {
249 return;
250 }
251 ArrayMap<String, NotificationRecord> pkgRecords = records.get(pkg);
252 if (pkgRecords == null) {
253 return;
254 }
255 final NotificationRecord record = pkgRecords.remove(key);
Julia Reynoldsa8b766f2017-03-07 16:30:21 -0500256 mPackages.remove(key);
257 mUsers.remove(key);
Julia Reynoldsb6c1f992016-11-22 09:26:46 -0500258
Julia Reynoldsa8b766f2017-03-07 16:30:21 -0500259 if (record != null && !record.isCanceled) {
Julia Reynolds520df6e2017-02-13 09:05:10 -0500260 MetricsLogger.action(record.getLogMaker()
261 .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED)
262 .setType(MetricsProto.MetricsEvent.TYPE_OPEN));
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400263 mCallback.repost(userId, record);
264 }
265 }
266
Julia Reynoldsa78cdff2017-04-26 10:19:25 -0400267 protected void repostGroupSummary(String pkg, int userId, String groupKey) {
268 if (mSnoozedNotifications.containsKey(userId)) {
269 ArrayMap<String, ArrayMap<String, NotificationRecord>> keysByPackage
270 = mSnoozedNotifications.get(userId);
271
272 if (keysByPackage != null && keysByPackage.containsKey(pkg)) {
273 ArrayMap<String, NotificationRecord> recordsByKey = keysByPackage.get(pkg);
274
275 if (recordsByKey != null) {
276 String groupSummaryKey = null;
277 int N = recordsByKey.size();
278 for (int i = 0; i < N; i++) {
279 final NotificationRecord potentialGroupSummary = recordsByKey.valueAt(i);
280 if (potentialGroupSummary.sbn.isGroup()
281 && potentialGroupSummary.getNotification().isGroupSummary()
282 && groupKey.equals(potentialGroupSummary.getGroupKey())) {
283 groupSummaryKey = potentialGroupSummary.getKey();
284 break;
285 }
286 }
287
288 if (groupSummaryKey != null) {
289 NotificationRecord record = recordsByKey.remove(groupSummaryKey);
290 mPackages.remove(groupSummaryKey);
291 mUsers.remove(groupSummaryKey);
292
Julia Reynoldsa8b766f2017-03-07 16:30:21 -0500293 if (record != null && !record.isCanceled) {
294 MetricsLogger.action(record.getLogMaker()
295 .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED)
296 .setType(MetricsProto.MetricsEvent.TYPE_OPEN));
297 mCallback.repost(userId, record);
298 }
Julia Reynoldsa78cdff2017-04-26 10:19:25 -0400299 }
300 }
301 }
302 }
303 }
304
Julia Reynolds67c1e962019-01-04 14:01:10 -0500305 protected void clearData(int userId, String pkg) {
306 ArrayMap<String, ArrayMap<String, NotificationRecord>> records =
307 mSnoozedNotifications.get(userId);
308 if (records == null) {
309 return;
310 }
311 ArrayMap<String, NotificationRecord> pkgRecords = records.get(pkg);
312 if (pkgRecords == null) {
313 return;
314 }
315 for (int i = pkgRecords.size() - 1; i >= 0; i--) {
316 final NotificationRecord r = pkgRecords.removeAt(i);
317 if (r != null) {
318 mPackages.remove(r.getKey());
319 mUsers.remove(r.getKey());
320 final PendingIntent pi = createPendingIntent(pkg, r.getKey(), userId);
321 mAm.cancel(pi);
322 MetricsLogger.action(r.getLogMaker()
323 .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED)
324 .setType(MetricsProto.MetricsEvent.TYPE_DISMISS));
325 }
326 }
327 }
328
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400329 private PendingIntent createPendingIntent(String pkg, String key, int userId) {
330 return PendingIntent.getBroadcast(mContext,
331 REQUEST_CODE_REPOST,
332 new Intent(REPOST_ACTION)
333 .setData(new Uri.Builder().scheme(REPOST_SCHEME).appendPath(key).build())
334 .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400335 .putExtra(EXTRA_KEY, key)
336 .putExtra(EXTRA_USER_ID, userId),
337 PendingIntent.FLAG_UPDATE_CURRENT);
338 }
339
Julia Reynolds50989772017-02-23 14:32:16 -0500340 private void scheduleRepost(String pkg, String key, int userId, long duration) {
Julia Reynolds79672302017-01-12 08:30:16 -0500341 long identity = Binder.clearCallingIdentity();
342 try {
343 final PendingIntent pi = createPendingIntent(pkg, key, userId);
344 mAm.cancel(pi);
Julia Reynolds50989772017-02-23 14:32:16 -0500345 long time = SystemClock.elapsedRealtime() + duration;
Julia Reynolds79672302017-01-12 08:30:16 -0500346 if (DEBUG) Slog.d(TAG, "Scheduling evaluate for " + new Date(time));
Julia Reynolds50989772017-02-23 14:32:16 -0500347 mAm.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, time, pi);
Julia Reynolds79672302017-01-12 08:30:16 -0500348 } finally {
349 Binder.restoreCallingIdentity(identity);
350 }
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400351 }
352
353 public void dump(PrintWriter pw, NotificationManagerService.DumpFilter filter) {
354 pw.println("\n Snoozed notifications:");
355 for (int userId : mSnoozedNotifications.keySet()) {
356 pw.print(INDENT);
357 pw.println("user: " + userId);
358 ArrayMap<String, ArrayMap<String, NotificationRecord>> snoozedPkgs =
359 mSnoozedNotifications.get(userId);
360 for (String pkg : snoozedPkgs.keySet()) {
361 pw.print(INDENT);
362 pw.print(INDENT);
363 pw.println("package: " + pkg);
364 Set<String> snoozedKeys = snoozedPkgs.get(pkg).keySet();
365 for (String key : snoozedKeys) {
366 pw.print(INDENT);
367 pw.print(INDENT);
368 pw.print(INDENT);
369 pw.println(key);
370 }
371 }
372 }
373 }
374
375 protected void writeXml(XmlSerializer out, boolean forBackup) throws IOException {
376
377 }
378
379 public void readXml(XmlPullParser parser, boolean forRestore)
380 throws XmlPullParserException, IOException {
381
382 }
383
384 @VisibleForTesting
385 void setAlarmManager(AlarmManager am) {
386 mAm = am;
387 }
388
389 protected interface Callback {
390 void repost(int userId, NotificationRecord r);
391 }
392
393 private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
394 @Override
395 public void onReceive(Context context, Intent intent) {
396 if (DEBUG) {
397 Slog.d(TAG, "Reposting notification");
398 }
399 if (REPOST_ACTION.equals(intent.getAction())) {
Julia Reynoldsb6c1f992016-11-22 09:26:46 -0500400 repost(intent.getStringExtra(EXTRA_KEY), intent.getIntExtra(EXTRA_USER_ID,
401 UserHandle.USER_SYSTEM));
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400402 }
403 }
404 };
405}