| /* | |
| * Copyright (C) 2016 The Android Open Source Project | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| package com.android.server.notification; | |
| import com.android.internal.annotations.VisibleForTesting; | |
| import com.android.internal.logging.MetricsLogger; | |
| import com.android.internal.logging.nano.MetricsProto; | |
| import org.xmlpull.v1.XmlPullParser; | |
| import org.xmlpull.v1.XmlPullParserException; | |
| import org.xmlpull.v1.XmlSerializer; | |
| import android.annotation.NonNull; | |
| import android.app.AlarmManager; | |
| import android.app.Notification; | |
| import android.app.PendingIntent; | |
| import android.content.BroadcastReceiver; | |
| import android.content.Context; | |
| import android.content.Intent; | |
| import android.content.IntentFilter; | |
| import android.net.Uri; | |
| import android.os.Binder; | |
| import android.os.SystemClock; | |
| import android.os.UserHandle; | |
| import android.service.notification.StatusBarNotification; | |
| import android.util.ArrayMap; | |
| import android.util.Log; | |
| import android.util.Slog; | |
| import java.io.IOException; | |
| import java.io.PrintWriter; | |
| import java.util.ArrayList; | |
| import java.util.Collection; | |
| import java.util.Collections; | |
| import java.util.Date; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Objects; | |
| import java.util.Set; | |
| /** | |
| * NotificationManagerService helper for handling snoozed notifications. | |
| */ | |
| public class SnoozeHelper { | |
| private static final String TAG = "SnoozeHelper"; | |
| private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); | |
| private static final String INDENT = " "; | |
| private static final String REPOST_ACTION = SnoozeHelper.class.getSimpleName() + ".EVALUATE"; | |
| private static final int REQUEST_CODE_REPOST = 1; | |
| private static final String REPOST_SCHEME = "repost"; | |
| private static final String EXTRA_KEY = "key"; | |
| private static final String EXTRA_USER_ID = "userId"; | |
| private final Context mContext; | |
| private AlarmManager mAm; | |
| private final ManagedServices.UserProfiles mUserProfiles; | |
| // User id : package name : notification key : record. | |
| private ArrayMap<Integer, ArrayMap<String, ArrayMap<String, NotificationRecord>>> | |
| mSnoozedNotifications = new ArrayMap<>(); | |
| // notification key : package. | |
| private ArrayMap<String, String> mPackages = new ArrayMap<>(); | |
| // key : userId | |
| private ArrayMap<String, Integer> mUsers = new ArrayMap<>(); | |
| private Callback mCallback; | |
| public SnoozeHelper(Context context, Callback callback, | |
| ManagedServices.UserProfiles userProfiles) { | |
| mContext = context; | |
| IntentFilter filter = new IntentFilter(REPOST_ACTION); | |
| filter.addDataScheme(REPOST_SCHEME); | |
| mContext.registerReceiver(mBroadcastReceiver, filter); | |
| mAm = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); | |
| mCallback = callback; | |
| mUserProfiles = userProfiles; | |
| } | |
| protected boolean isSnoozed(int userId, String pkg, String key) { | |
| return mSnoozedNotifications.containsKey(userId) | |
| && mSnoozedNotifications.get(userId).containsKey(pkg) | |
| && mSnoozedNotifications.get(userId).get(pkg).containsKey(key); | |
| } | |
| protected Collection<NotificationRecord> getSnoozed(int userId, String pkg) { | |
| if (mSnoozedNotifications.containsKey(userId) | |
| && mSnoozedNotifications.get(userId).containsKey(pkg)) { | |
| return mSnoozedNotifications.get(userId).get(pkg).values(); | |
| } | |
| return Collections.EMPTY_LIST; | |
| } | |
| protected @NonNull List<NotificationRecord> getSnoozed() { | |
| List<NotificationRecord> snoozedForUser = new ArrayList<>(); | |
| int[] userIds = mUserProfiles.getCurrentProfileIds(); | |
| if (userIds != null) { | |
| final int N = userIds.length; | |
| for (int i = 0; i < N; i++) { | |
| final ArrayMap<String, ArrayMap<String, NotificationRecord>> snoozedPkgs = | |
| mSnoozedNotifications.get(userIds[i]); | |
| if (snoozedPkgs != null) { | |
| final int M = snoozedPkgs.size(); | |
| for (int j = 0; j < M; j++) { | |
| final ArrayMap<String, NotificationRecord> records = snoozedPkgs.valueAt(j); | |
| if (records != null) { | |
| snoozedForUser.addAll(records.values()); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| return snoozedForUser; | |
| } | |
| /** | |
| * Snoozes a notification and schedules an alarm to repost at that time. | |
| */ | |
| protected void snooze(NotificationRecord record, long duration) { | |
| snooze(record); | |
| scheduleRepost(record.sbn.getPackageName(), record.getKey(), record.getUserId(), duration); | |
| } | |
| /** | |
| * Records a snoozed notification. | |
| */ | |
| protected void snooze(NotificationRecord record) { | |
| int userId = record.getUser().getIdentifier(); | |
| if (DEBUG) { | |
| Slog.d(TAG, "Snoozing " + record.getKey()); | |
| } | |
| ArrayMap<String, ArrayMap<String, NotificationRecord>> records = | |
| mSnoozedNotifications.get(userId); | |
| if (records == null) { | |
| records = new ArrayMap<>(); | |
| } | |
| ArrayMap<String, NotificationRecord> pkgRecords = records.get(record.sbn.getPackageName()); | |
| if (pkgRecords == null) { | |
| pkgRecords = new ArrayMap<>(); | |
| } | |
| pkgRecords.put(record.getKey(), record); | |
| records.put(record.sbn.getPackageName(), pkgRecords); | |
| mSnoozedNotifications.put(userId, records); | |
| mPackages.put(record.getKey(), record.sbn.getPackageName()); | |
| mUsers.put(record.getKey(), userId); | |
| } | |
| protected boolean cancel(int userId, String pkg, String tag, int id) { | |
| if (mSnoozedNotifications.containsKey(userId)) { | |
| ArrayMap<String, NotificationRecord> recordsForPkg = | |
| mSnoozedNotifications.get(userId).get(pkg); | |
| if (recordsForPkg != null) { | |
| final Set<Map.Entry<String, NotificationRecord>> records = recordsForPkg.entrySet(); | |
| String key = null; | |
| for (Map.Entry<String, NotificationRecord> record : records) { | |
| final StatusBarNotification sbn = record.getValue().sbn; | |
| if (Objects.equals(sbn.getTag(), tag) && sbn.getId() == id) { | |
| record.getValue().isCanceled = true; | |
| return true; | |
| } | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| protected boolean cancel(int userId, boolean includeCurrentProfiles) { | |
| int[] userIds = {userId}; | |
| if (includeCurrentProfiles) { | |
| userIds = mUserProfiles.getCurrentProfileIds(); | |
| } | |
| final int N = userIds.length; | |
| for (int i = 0; i < N; i++) { | |
| final ArrayMap<String, ArrayMap<String, NotificationRecord>> snoozedPkgs = | |
| mSnoozedNotifications.get(userIds[i]); | |
| if (snoozedPkgs != null) { | |
| final int M = snoozedPkgs.size(); | |
| for (int j = 0; j < M; j++) { | |
| final ArrayMap<String, NotificationRecord> records = snoozedPkgs.valueAt(j); | |
| if (records != null) { | |
| int P = records.size(); | |
| for (int k = 0; k < P; k++) { | |
| records.valueAt(k).isCanceled = true; | |
| } | |
| } | |
| } | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| protected boolean cancel(int userId, String pkg) { | |
| if (mSnoozedNotifications.containsKey(userId)) { | |
| if (mSnoozedNotifications.get(userId).containsKey(pkg)) { | |
| ArrayMap<String, NotificationRecord> records = | |
| mSnoozedNotifications.get(userId).get(pkg); | |
| int N = records.size(); | |
| for (int i = 0; i < N; i++) { | |
| records.valueAt(i).isCanceled = true; | |
| } | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| /** | |
| * Updates the notification record so the most up to date information is shown on re-post. | |
| */ | |
| protected void update(int userId, NotificationRecord record) { | |
| ArrayMap<String, ArrayMap<String, NotificationRecord>> records = | |
| mSnoozedNotifications.get(userId); | |
| if (records == null) { | |
| return; | |
| } | |
| ArrayMap<String, NotificationRecord> pkgRecords = records.get(record.sbn.getPackageName()); | |
| if (pkgRecords == null) { | |
| return; | |
| } | |
| NotificationRecord existing = pkgRecords.get(record.getKey()); | |
| if (existing != null && existing.isCanceled) { | |
| return; | |
| } | |
| pkgRecords.put(record.getKey(), record); | |
| } | |
| protected void repost(String key) { | |
| Integer userId = mUsers.get(key); | |
| if (userId != null) { | |
| repost(key, userId); | |
| } | |
| } | |
| protected void repost(String key, int userId) { | |
| final String pkg = mPackages.remove(key); | |
| ArrayMap<String, ArrayMap<String, NotificationRecord>> records = | |
| mSnoozedNotifications.get(userId); | |
| if (records == null) { | |
| return; | |
| } | |
| ArrayMap<String, NotificationRecord> pkgRecords = records.get(pkg); | |
| if (pkgRecords == null) { | |
| return; | |
| } | |
| final NotificationRecord record = pkgRecords.remove(key); | |
| mPackages.remove(key); | |
| mUsers.remove(key); | |
| if (record != null && !record.isCanceled) { | |
| MetricsLogger.action(record.getLogMaker() | |
| .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED) | |
| .setType(MetricsProto.MetricsEvent.TYPE_OPEN)); | |
| mCallback.repost(userId, record); | |
| } | |
| } | |
| protected void repostGroupSummary(String pkg, int userId, String groupKey) { | |
| if (mSnoozedNotifications.containsKey(userId)) { | |
| ArrayMap<String, ArrayMap<String, NotificationRecord>> keysByPackage | |
| = mSnoozedNotifications.get(userId); | |
| if (keysByPackage != null && keysByPackage.containsKey(pkg)) { | |
| ArrayMap<String, NotificationRecord> recordsByKey = keysByPackage.get(pkg); | |
| if (recordsByKey != null) { | |
| String groupSummaryKey = null; | |
| int N = recordsByKey.size(); | |
| for (int i = 0; i < N; i++) { | |
| final NotificationRecord potentialGroupSummary = recordsByKey.valueAt(i); | |
| if (potentialGroupSummary.sbn.isGroup() | |
| && potentialGroupSummary.getNotification().isGroupSummary() | |
| && groupKey.equals(potentialGroupSummary.getGroupKey())) { | |
| groupSummaryKey = potentialGroupSummary.getKey(); | |
| break; | |
| } | |
| } | |
| if (groupSummaryKey != null) { | |
| NotificationRecord record = recordsByKey.remove(groupSummaryKey); | |
| mPackages.remove(groupSummaryKey); | |
| mUsers.remove(groupSummaryKey); | |
| if (record != null && !record.isCanceled) { | |
| MetricsLogger.action(record.getLogMaker() | |
| .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED) | |
| .setType(MetricsProto.MetricsEvent.TYPE_OPEN)); | |
| mCallback.repost(userId, record); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| private PendingIntent createPendingIntent(String pkg, String key, int userId) { | |
| return PendingIntent.getBroadcast(mContext, | |
| REQUEST_CODE_REPOST, | |
| new Intent(REPOST_ACTION) | |
| .setData(new Uri.Builder().scheme(REPOST_SCHEME).appendPath(key).build()) | |
| .addFlags(Intent.FLAG_RECEIVER_FOREGROUND) | |
| .putExtra(EXTRA_KEY, key) | |
| .putExtra(EXTRA_USER_ID, userId), | |
| PendingIntent.FLAG_UPDATE_CURRENT); | |
| } | |
| private void scheduleRepost(String pkg, String key, int userId, long duration) { | |
| long identity = Binder.clearCallingIdentity(); | |
| try { | |
| final PendingIntent pi = createPendingIntent(pkg, key, userId); | |
| mAm.cancel(pi); | |
| long time = SystemClock.elapsedRealtime() + duration; | |
| if (DEBUG) Slog.d(TAG, "Scheduling evaluate for " + new Date(time)); | |
| mAm.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, time, pi); | |
| } finally { | |
| Binder.restoreCallingIdentity(identity); | |
| } | |
| } | |
| public void dump(PrintWriter pw, NotificationManagerService.DumpFilter filter) { | |
| pw.println("\n Snoozed notifications:"); | |
| for (int userId : mSnoozedNotifications.keySet()) { | |
| pw.print(INDENT); | |
| pw.println("user: " + userId); | |
| ArrayMap<String, ArrayMap<String, NotificationRecord>> snoozedPkgs = | |
| mSnoozedNotifications.get(userId); | |
| for (String pkg : snoozedPkgs.keySet()) { | |
| pw.print(INDENT); | |
| pw.print(INDENT); | |
| pw.println("package: " + pkg); | |
| Set<String> snoozedKeys = snoozedPkgs.get(pkg).keySet(); | |
| for (String key : snoozedKeys) { | |
| pw.print(INDENT); | |
| pw.print(INDENT); | |
| pw.print(INDENT); | |
| pw.println(key); | |
| } | |
| } | |
| } | |
| } | |
| protected void writeXml(XmlSerializer out, boolean forBackup) throws IOException { | |
| } | |
| public void readXml(XmlPullParser parser, boolean forRestore) | |
| throws XmlPullParserException, IOException { | |
| } | |
| @VisibleForTesting | |
| void setAlarmManager(AlarmManager am) { | |
| mAm = am; | |
| } | |
| protected interface Callback { | |
| void repost(int userId, NotificationRecord r); | |
| } | |
| private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { | |
| @Override | |
| public void onReceive(Context context, Intent intent) { | |
| if (DEBUG) { | |
| Slog.d(TAG, "Reposting notification"); | |
| } | |
| if (REPOST_ACTION.equals(intent.getAction())) { | |
| repost(intent.getStringExtra(EXTRA_KEY), intent.getIntExtra(EXTRA_USER_ID, | |
| UserHandle.USER_SYSTEM)); | |
| } | |
| } | |
| }; | |
| } |