blob: a178a525cedeacef9fae42720c597425d1ee547b [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
18import com.android.internal.annotations.VisibleForTesting;
Julia Reynolds520df6e2017-02-13 09:05:10 -050019import com.android.internal.logging.MetricsLogger;
20import com.android.internal.logging.nano.MetricsProto;
Julia Reynolds72f1cbb2016-09-19 14:57:31 -040021
22import org.xmlpull.v1.XmlPullParser;
23import org.xmlpull.v1.XmlPullParserException;
24import org.xmlpull.v1.XmlSerializer;
25
Julia Reynolds520df6e2017-02-13 09:05:10 -050026import android.annotation.NonNull;
Julia Reynolds72f1cbb2016-09-19 14:57:31 -040027import android.app.AlarmManager;
Julia Reynoldsa78cdff2017-04-26 10:19:25 -040028import android.app.Notification;
Julia Reynolds72f1cbb2016-09-19 14:57:31 -040029import android.app.PendingIntent;
30import android.content.BroadcastReceiver;
31import android.content.Context;
32import android.content.Intent;
33import android.content.IntentFilter;
34import android.net.Uri;
Julia Reynolds79672302017-01-12 08:30:16 -050035import android.os.Binder;
Julia Reynolds50989772017-02-23 14:32:16 -050036import android.os.SystemClock;
Julia Reynoldsb6c1f992016-11-22 09:26:46 -050037import android.os.UserHandle;
Julia Reynolds72f1cbb2016-09-19 14:57:31 -040038import android.service.notification.StatusBarNotification;
39import android.util.ArrayMap;
40import android.util.Log;
41import android.util.Slog;
42
43import 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
Julia Reynolds520df6e2017-02-13 09:05:10 -0500106 protected @NonNull List<NotificationRecord> getSnoozed() {
Julia Reynoldscf63ff12017-01-24 13:55:48 -0500107 List<NotificationRecord> snoozedForUser = new ArrayList<>();
108 int[] userIds = mUserProfiles.getCurrentProfileIds();
Julia Reynoldsa78cdff2017-04-26 10:19:25 -0400109 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 }
Julia Reynoldscf63ff12017-01-24 13:55:48 -0500121 }
122 }
123 }
124 }
125 return snoozedForUser;
126 }
127
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400128 /**
Julia Reynoldsb6c1f992016-11-22 09:26:46 -0500129 * Snoozes a notification and schedules an alarm to repost at that time.
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400130 */
Julia Reynolds50989772017-02-23 14:32:16 -0500131 protected void snooze(NotificationRecord record, long duration) {
Julia Reynolds79672302017-01-12 08:30:16 -0500132 snooze(record);
Julia Reynolds50989772017-02-23 14:32:16 -0500133 scheduleRepost(record.sbn.getPackageName(), record.getKey(), record.getUserId(), duration);
Julia Reynoldsb6c1f992016-11-22 09:26:46 -0500134 }
135
136 /**
137 * Records a snoozed notification.
138 */
Julia Reynolds79672302017-01-12 08:30:16 -0500139 protected void snooze(NotificationRecord record) {
140 int userId = record.getUser().getIdentifier();
Julia Reynoldsb6c1f992016-11-22 09:26:46 -0500141 if (DEBUG) {
142 Slog.d(TAG, "Snoozing " + record.getKey());
143 }
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400144 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);
Julia Reynoldsb6c1f992016-11-22 09:26:46 -0500156 mPackages.put(record.getKey(), record.sbn.getPackageName());
Julia Reynolds79672302017-01-12 08:30:16 -0500157 mUsers.put(record.getKey(), userId);
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400158 }
159
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400160 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) {
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) {
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 =
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());
234 if (existing != null && existing.isCanceled) {
235 return;
236 }
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400237 pkgRecords.put(record.getKey(), record);
238 }
239
Julia Reynolds79672302017-01-12 08:30:16 -0500240 protected void repost(String key) {
241 Integer userId = mUsers.get(key);
242 if (userId != null) {
243 repost(key, userId);
244 }
245 }
246
Julia Reynoldsb6c1f992016-11-22 09:26:46 -0500247 protected void repost(String key, int userId) {
248 final String pkg = mPackages.remove(key);
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400249 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);
Julia Reynoldsa8b766f2017-03-07 16:30:21 -0500259 mPackages.remove(key);
260 mUsers.remove(key);
Julia Reynoldsb6c1f992016-11-22 09:26:46 -0500261
Julia Reynoldsa8b766f2017-03-07 16:30:21 -0500262 if (record != null && !record.isCanceled) {
Julia Reynolds520df6e2017-02-13 09:05:10 -0500263 MetricsLogger.action(record.getLogMaker()
264 .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED)
265 .setType(MetricsProto.MetricsEvent.TYPE_OPEN));
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400266 mCallback.repost(userId, record);
267 }
268 }
269
Julia Reynoldsa78cdff2017-04-26 10:19:25 -0400270 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
Julia Reynoldsa8b766f2017-03-07 16:30:21 -0500296 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 }
Julia Reynoldsa78cdff2017-04-26 10:19:25 -0400302 }
303 }
304 }
305 }
306 }
307
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400308 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)
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400314 .putExtra(EXTRA_KEY, key)
315 .putExtra(EXTRA_USER_ID, userId),
316 PendingIntent.FLAG_UPDATE_CURRENT);
317 }
318
Julia Reynolds50989772017-02-23 14:32:16 -0500319 private void scheduleRepost(String pkg, String key, int userId, long duration) {
Julia Reynolds79672302017-01-12 08:30:16 -0500320 long identity = Binder.clearCallingIdentity();
321 try {
322 final PendingIntent pi = createPendingIntent(pkg, key, userId);
323 mAm.cancel(pi);
Julia Reynolds50989772017-02-23 14:32:16 -0500324 long time = SystemClock.elapsedRealtime() + duration;
Julia Reynolds79672302017-01-12 08:30:16 -0500325 if (DEBUG) Slog.d(TAG, "Scheduling evaluate for " + new Date(time));
Julia Reynolds50989772017-02-23 14:32:16 -0500326 mAm.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, time, pi);
Julia Reynolds79672302017-01-12 08:30:16 -0500327 } finally {
328 Binder.restoreCallingIdentity(identity);
329 }
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400330 }
331
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
354 protected void writeXml(XmlSerializer out, boolean forBackup) throws IOException {
355
356 }
357
358 public void readXml(XmlPullParser parser, boolean forRestore)
359 throws XmlPullParserException, IOException {
360
361 }
362
363 @VisibleForTesting
364 void setAlarmManager(AlarmManager am) {
365 mAm = am;
366 }
367
368 protected interface Callback {
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())) {
Julia Reynoldsb6c1f992016-11-22 09:26:46 -0500379 repost(intent.getStringExtra(EXTRA_KEY), intent.getIntExtra(EXTRA_USER_ID,
380 UserHandle.USER_SYSTEM));
Julia Reynolds72f1cbb2016-09-19 14:57:31 -0400381 }
382 }
383 };
384}