| /* |
| * 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 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.IntArray; |
| import android.util.Log; |
| import android.util.Slog; |
| |
| 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 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<>(); |
| IntArray userIds = mUserProfiles.getCurrentProfileIds(); |
| if (userIds != null) { |
| final int N = userIds.size(); |
| for (int i = 0; i < N; i++) { |
| final ArrayMap<String, ArrayMap<String, NotificationRecord>> snoozedPkgs = |
| mSnoozedNotifications.get(userIds.get(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(); |
| 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().toArray(); |
| } |
| 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); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| protected void clearData(int userId, String pkg) { |
| ArrayMap<String, ArrayMap<String, NotificationRecord>> records = |
| mSnoozedNotifications.get(userId); |
| if (records == null) { |
| return; |
| } |
| ArrayMap<String, NotificationRecord> pkgRecords = records.get(pkg); |
| if (pkgRecords == null) { |
| return; |
| } |
| for (int i = pkgRecords.size() - 1; i >= 0; i--) { |
| final NotificationRecord r = pkgRecords.removeAt(i); |
| if (r != null) { |
| mPackages.remove(r.getKey()); |
| mUsers.remove(r.getKey()); |
| final PendingIntent pi = createPendingIntent(pkg, r.getKey(), userId); |
| mAm.cancel(pi); |
| MetricsLogger.action(r.getLogMaker() |
| .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED) |
| .setType(MetricsProto.MetricsEvent.TYPE_DISMISS)); |
| } |
| } |
| } |
| |
| 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)); |
| } |
| } |
| }; |
| } |