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