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