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 com.android.internal.annotations.VisibleForTesting; 19 import com.android.internal.logging.MetricsLogger; 20 import com.android.internal.logging.nano.MetricsProto; 21 22 import org.xmlpull.v1.XmlPullParser; 23 import org.xmlpull.v1.XmlPullParserException; 24 import org.xmlpull.v1.XmlSerializer; 25 26 import android.annotation.NonNull; 27 import android.app.AlarmManager; 28 import android.app.Notification; 29 import android.app.PendingIntent; 30 import android.content.BroadcastReceiver; 31 import android.content.Context; 32 import android.content.Intent; 33 import android.content.IntentFilter; 34 import android.net.Uri; 35 import android.os.Binder; 36 import android.os.SystemClock; 37 import android.os.UserHandle; 38 import android.service.notification.StatusBarNotification; 39 import android.util.ArrayMap; 40 import android.util.Log; 41 import android.util.Slog; 42 43 import java.io.IOException; 44 import java.io.PrintWriter; 45 import java.util.ArrayList; 46 import java.util.Collection; 47 import java.util.Collections; 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 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"; 65 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<>(); 75 // notification key : package. 76 private ArrayMap<String, String> mPackages = new ArrayMap<>(); 77 // key : userId 78 private ArrayMap<String, Integer> mUsers = new ArrayMap<>(); 79 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 98 protected Collection<NotificationRecord> getSnoozed(int userId, String pkg) { 99 if (mSnoozedNotifications.containsKey(userId) 100 && mSnoozedNotifications.get(userId).containsKey(pkg)) { 101 return mSnoozedNotifications.get(userId).get(pkg).values(); 102 } 103 return Collections.EMPTY_LIST; 104 } 105 106 protected @NonNull List<NotificationRecord> getSnoozed() { 107 List<NotificationRecord> snoozedForUser = new ArrayList<>(); 108 int[] userIds = mUserProfiles.getCurrentProfileIds(); 109 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 } 121 } 122 } 123 } 124 } 125 return snoozedForUser; 126 } 127 128 /** 129 * Snoozes a notification and schedules an alarm to repost at that time. 130 */ 131 protected void snooze(NotificationRecord record, long duration) { 132 snooze(record); 133 scheduleRepost(record.sbn.getPackageName(), record.getKey(), record.getUserId(), duration); 134 } 135 136 /** 137 * Records a snoozed notification. 138 */ 139 protected void snooze(NotificationRecord record) { 140 int userId = record.getUser().getIdentifier(); 141 if (DEBUG) { 142 Slog.d(TAG, "Snoozing " + record.getKey()); 143 } 144 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); 156 mPackages.put(record.getKey(), record.sbn.getPackageName()); 157 mUsers.put(record.getKey(), userId); 158 } 159 160 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) { 170 record.getValue().isCanceled = true; 171 return true; 172 } 173 } 174 } 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 = 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 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 */ 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 if (existing != null && existing.isCanceled) { 235 return; 236 } 237 pkgRecords.put(record.getKey(), record); 238 } 239 240 protected void repost(String key) { 241 Integer userId = mUsers.get(key); 242 if (userId != null) { 243 repost(key, userId); 244 } 245 } 246 247 protected void repost(String key, int userId) { 248 final String pkg = mPackages.remove(key); 249 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); 259 mPackages.remove(key); 260 mUsers.remove(key); 261 262 if (record != null && !record.isCanceled) { 263 MetricsLogger.action(record.getLogMaker() 264 .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED) 265 .setType(MetricsProto.MetricsEvent.TYPE_OPEN)); 266 mCallback.repost(userId, record); 267 } 268 } 269 270 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 296 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 } 302 } 303 } 304 } 305 } 306 } 307 308 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) 314 .putExtra(EXTRA_KEY, key) 315 .putExtra(EXTRA_USER_ID, userId), 316 PendingIntent.FLAG_UPDATE_CURRENT); 317 } 318 319 private void scheduleRepost(String pkg, String key, int userId, long duration) { 320 long identity = Binder.clearCallingIdentity(); 321 try { 322 final PendingIntent pi = createPendingIntent(pkg, key, userId); 323 mAm.cancel(pi); 324 long time = SystemClock.elapsedRealtime() + duration; 325 if (DEBUG) Slog.d(TAG, "Scheduling evaluate for " + new Date(time)); 326 mAm.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, time, pi); 327 } finally { 328 Binder.restoreCallingIdentity(identity); 329 } 330 } 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())) { 379 repost(intent.getStringExtra(EXTRA_KEY), intent.getIntExtra(EXTRA_USER_ID, 380 UserHandle.USER_SYSTEM)); 381 } 382 } 383 }; 384 } 385