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.Notification; 21 import android.app.PendingIntent; 22 import android.content.BroadcastReceiver; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.IntentFilter; 26 import android.net.Uri; 27 import android.os.Binder; 28 import android.os.SystemClock; 29 import android.os.UserHandle; 30 import android.service.notification.StatusBarNotification; 31 import android.util.ArrayMap; 32 import android.util.IntArray; 33 import android.util.Log; 34 import android.util.Slog; 35 36 import com.android.internal.annotations.VisibleForTesting; 37 import com.android.internal.logging.MetricsLogger; 38 import com.android.internal.logging.nano.MetricsProto; 39 40 import org.xmlpull.v1.XmlPullParser; 41 import org.xmlpull.v1.XmlPullParserException; 42 import org.xmlpull.v1.XmlSerializer; 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.Collections; 49 import java.util.Date; 50 import java.util.List; 51 import java.util.Map; 52 import java.util.Objects; 53 import java.util.Set; 54 55 /** 56 * NotificationManagerService helper for handling snoozed notifications. 57 */ 58 public 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"; 66 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<>(); 76 // notification key : package. 77 private ArrayMap<String, String> mPackages = new ArrayMap<>(); 78 // key : userId 79 private ArrayMap<String, Integer> mUsers = new ArrayMap<>(); 80 private Callback mCallback; 81 SnoozeHelper(Context context, Callback callback, ManagedServices.UserProfiles userProfiles)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 isSnoozed(int userId, String pkg, String key)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 getSnoozed(int userId, String pkg)99 protected Collection<NotificationRecord> getSnoozed(int userId, String pkg) { 100 if (mSnoozedNotifications.containsKey(userId) 101 && mSnoozedNotifications.get(userId).containsKey(pkg)) { 102 return mSnoozedNotifications.get(userId).get(pkg).values(); 103 } 104 return Collections.EMPTY_LIST; 105 } 106 getSnoozed()107 protected @NonNull List<NotificationRecord> getSnoozed() { 108 List<NotificationRecord> snoozedForUser = new ArrayList<>(); 109 IntArray userIds = mUserProfiles.getCurrentProfileIds(); 110 if (userIds != null) { 111 final int N = userIds.size(); 112 for (int i = 0; i < N; i++) { 113 final ArrayMap<String, ArrayMap<String, NotificationRecord>> snoozedPkgs = 114 mSnoozedNotifications.get(userIds.get(i)); 115 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 } 122 } 123 } 124 } 125 } 126 return snoozedForUser; 127 } 128 129 /** 130 * Snoozes a notification and schedules an alarm to repost at that time. 131 */ snooze(NotificationRecord record, long duration)132 protected void snooze(NotificationRecord record, long duration) { 133 snooze(record); 134 scheduleRepost(record.sbn.getPackageName(), record.getKey(), record.getUserId(), duration); 135 } 136 137 /** 138 * Records a snoozed notification. 139 */ snooze(NotificationRecord record)140 protected void snooze(NotificationRecord record) { 141 int userId = record.getUser().getIdentifier(); 142 if (DEBUG) { 143 Slog.d(TAG, "Snoozing " + record.getKey()); 144 } 145 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); 157 mPackages.put(record.getKey(), record.sbn.getPackageName()); 158 mUsers.put(record.getKey(), userId); 159 } 160 cancel(int userId, String pkg, String tag, int id)161 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(); 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) { 170 record.getValue().isCanceled = true; 171 return true; 172 } 173 } 174 } 175 } 176 return false; 177 } 178 cancel(int userId, boolean includeCurrentProfiles)179 protected boolean cancel(int userId, boolean includeCurrentProfiles) { 180 int[] userIds = {userId}; 181 if (includeCurrentProfiles) { 182 userIds = mUserProfiles.getCurrentProfileIds().toArray(); 183 } 184 final int N = userIds.length; 185 for (int i = 0; i < N; i++) { 186 final ArrayMap<String, ArrayMap<String, NotificationRecord>> snoozedPkgs = 187 mSnoozedNotifications.get(userIds[i]); 188 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++) { 195 records.valueAt(k).isCanceled = true; 196 } 197 } 198 } 199 return true; 200 } 201 } 202 return false; 203 } 204 cancel(int userId, String pkg)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 = 209 mSnoozedNotifications.get(userId).get(pkg); 210 int N = records.size(); 211 for (int i = 0; i < N; i++) { 212 records.valueAt(i).isCanceled = true; 213 } 214 return true; 215 } 216 } 217 return false; 218 } 219 220 /** 221 * Updates the notification record so the most up to date information is shown on re-post. 222 */ update(int userId, NotificationRecord record)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 } 233 NotificationRecord existing = pkgRecords.get(record.getKey()); 234 pkgRecords.put(record.getKey(), record); 235 } 236 repost(String key)237 protected void repost(String key) { 238 Integer userId = mUsers.get(key); 239 if (userId != null) { 240 repost(key, userId); 241 } 242 } 243 repost(String key, int userId)244 protected void repost(String key, int userId) { 245 final String pkg = mPackages.remove(key); 246 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); 256 mPackages.remove(key); 257 mUsers.remove(key); 258 259 if (record != null && !record.isCanceled) { 260 MetricsLogger.action(record.getLogMaker() 261 .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED) 262 .setType(MetricsProto.MetricsEvent.TYPE_OPEN)); 263 mCallback.repost(userId, record); 264 } 265 } 266 repostGroupSummary(String pkg, int userId, String groupKey)267 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 293 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 } 299 } 300 } 301 } 302 } 303 } 304 clearData(int userId, String pkg)305 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 createPendingIntent(String pkg, String key, int userId)329 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) 335 .putExtra(EXTRA_KEY, key) 336 .putExtra(EXTRA_USER_ID, userId), 337 PendingIntent.FLAG_UPDATE_CURRENT); 338 } 339 scheduleRepost(String pkg, String key, int userId, long duration)340 private void scheduleRepost(String pkg, String key, int userId, long duration) { 341 long identity = Binder.clearCallingIdentity(); 342 try { 343 final PendingIntent pi = createPendingIntent(pkg, key, userId); 344 mAm.cancel(pi); 345 long time = SystemClock.elapsedRealtime() + duration; 346 if (DEBUG) Slog.d(TAG, "Scheduling evaluate for " + new Date(time)); 347 mAm.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, time, pi); 348 } finally { 349 Binder.restoreCallingIdentity(identity); 350 } 351 } 352 dump(PrintWriter pw, NotificationManagerService.DumpFilter filter)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 writeXml(XmlSerializer out, boolean forBackup)375 protected void writeXml(XmlSerializer out, boolean forBackup) throws IOException { 376 377 } 378 readXml(XmlPullParser parser, boolean forRestore)379 public void readXml(XmlPullParser parser, boolean forRestore) 380 throws XmlPullParserException, IOException { 381 382 } 383 384 @VisibleForTesting setAlarmManager(AlarmManager am)385 void setAlarmManager(AlarmManager am) { 386 mAm = am; 387 } 388 389 protected interface Callback { repost(int userId, NotificationRecord r)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())) { 400 repost(intent.getStringExtra(EXTRA_KEY), intent.getIntExtra(EXTRA_USER_ID, 401 UserHandle.USER_SYSTEM)); 402 } 403 } 404 }; 405 } 406