/*
|
* 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));
|
}
|
}
|
};
|
}
|