/* * 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>> mSnoozedNotifications = new ArrayMap<>(); // notification key : package. private ArrayMap mPackages = new ArrayMap<>(); // key : userId private ArrayMap 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 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 getSnoozed() { List snoozedForUser = new ArrayList<>(); int[] userIds = mUserProfiles.getCurrentProfileIds(); if (userIds != null) { final int N = userIds.length; for (int i = 0; i < N; i++) { final ArrayMap> snoozedPkgs = mSnoozedNotifications.get(userIds[i]); if (snoozedPkgs != null) { final int M = snoozedPkgs.size(); for (int j = 0; j < M; j++) { final ArrayMap 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> records = mSnoozedNotifications.get(userId); if (records == null) { records = new ArrayMap<>(); } ArrayMap 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 recordsForPkg = mSnoozedNotifications.get(userId).get(pkg); if (recordsForPkg != null) { final Set> records = recordsForPkg.entrySet(); String key = null; for (Map.Entry 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> snoozedPkgs = mSnoozedNotifications.get(userIds[i]); if (snoozedPkgs != null) { final int M = snoozedPkgs.size(); for (int j = 0; j < M; j++) { final ArrayMap 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 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> records = mSnoozedNotifications.get(userId); if (records == null) { return; } ArrayMap 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> records = mSnoozedNotifications.get(userId); if (records == null) { return; } ArrayMap 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> keysByPackage = mSnoozedNotifications.get(userId); if (keysByPackage != null && keysByPackage.containsKey(pkg)) { ArrayMap 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> snoozedPkgs = mSnoozedNotifications.get(userId); for (String pkg : snoozedPkgs.keySet()) { pw.print(INDENT); pw.print(INDENT); pw.println("package: " + pkg); Set 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)); } } }; }