blob: dff02bf711cd8f77c536df421dad4d8e3a186764 [file] [log] [blame]
/*
* 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 static android.app.Notification.FLAG_AUTOGROUP_SUMMARY;
import static android.app.Notification.FLAG_AUTO_CANCEL;
import static android.app.Notification.FLAG_GROUP_SUMMARY;
import static android.app.Notification.FLAG_LOCAL_ONLY;
import static android.app.Notification.FLAG_NO_CLEAR;
import static android.app.Notification.FLAG_ONGOING_EVENT;
import android.annotation.NonNull;
import android.service.notification.StatusBarNotification;
import android.util.ArrayMap;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
/**
* NotificationManagerService helper for auto-grouping notifications.
*/
public class GroupHelper {
private static final String TAG = "GroupHelper";
protected static final String AUTOGROUP_KEY = "ranker_group";
// Flags that all autogroup summaries have
protected static final int BASE_FLAGS =
FLAG_AUTOGROUP_SUMMARY | FLAG_GROUP_SUMMARY | FLAG_LOCAL_ONLY;
// Flag that autogroup summaries inherits if all children have the flag
private static final int ALL_CHILDREN_FLAG = FLAG_AUTO_CANCEL;
// Flags that autogroup summaries inherits if any child has them
private static final int ANY_CHILDREN_FLAGS = FLAG_ONGOING_EVENT | FLAG_NO_CLEAR;
private final Callback mCallback;
private final int mAutoGroupAtCount;
// Only contains notifications that are not explicitly grouped by the app (aka no group or
// sort key).
// userId|packageName -> (keys of notifications that aren't in an explicit app group -> flags)
@GuardedBy("mUngroupedNotifications")
private final ArrayMap<String, ArrayMap<String, Integer>> mUngroupedNotifications
= new ArrayMap<>();
public GroupHelper(int autoGroupAtCount, Callback callback) {
mAutoGroupAtCount = autoGroupAtCount;
mCallback = callback;
}
private String generatePackageKey(int userId, String pkg) {
return userId + "|" + pkg;
}
@VisibleForTesting
@GuardedBy("mUngroupedNotifications")
protected int getAutogroupSummaryFlags(@NonNull final ArrayMap<String, Integer> children) {
boolean allChildrenHasFlag = children.size() > 0;
int anyChildFlagSet = 0;
for (int i = 0; i < children.size(); i++) {
if (!hasAnyFlag(children.valueAt(i), ALL_CHILDREN_FLAG)) {
allChildrenHasFlag = false;
}
if (hasAnyFlag(children.valueAt(i), ANY_CHILDREN_FLAGS)) {
anyChildFlagSet |= (children.valueAt(i) & ANY_CHILDREN_FLAGS);
}
}
return BASE_FLAGS | (allChildrenHasFlag ? ALL_CHILDREN_FLAG : 0) | anyChildFlagSet;
}
private boolean hasAnyFlag(int flags, int mask) {
return (flags & mask) != 0;
}
public void onNotificationPosted(StatusBarNotification sbn, boolean autogroupSummaryExists) {
try {
if (!sbn.isAppGroup()) {
maybeGroup(sbn, autogroupSummaryExists);
} else {
maybeUngroup(sbn, false, sbn.getUserId());
}
} catch (Exception e) {
Slog.e(TAG, "Failure processing new notification", e);
}
}
public void onNotificationRemoved(StatusBarNotification sbn) {
try {
maybeUngroup(sbn, true, sbn.getUserId());
} catch (Exception e) {
Slog.e(TAG, "Error processing canceled notification", e);
}
}
/**
* A non-app grouped notification has been added or updated
* Evaluate if:
* (a) an existing autogroup summary needs updated flags
* (b) a new autogroup summary needs to be added with correct flags
* (c) other non-app grouped children need to be moved to the autogroup
*
* And stores the list of upgrouped notifications & their flags
*/
private void maybeGroup(StatusBarNotification sbn, boolean autogroupSummaryExists) {
int flags = 0;
List<String> notificationsToGroup = new ArrayList<>();
synchronized (mUngroupedNotifications) {
String key = generatePackageKey(sbn.getUserId(), sbn.getPackageName());
final ArrayMap<String, Integer> children =
mUngroupedNotifications.getOrDefault(key, new ArrayMap<>());
children.put(sbn.getKey(), sbn.getNotification().flags);
mUngroupedNotifications.put(key, children);
if (children.size() >= mAutoGroupAtCount || autogroupSummaryExists) {
flags = getAutogroupSummaryFlags(children);
notificationsToGroup.addAll(children.keySet());
}
}
if (notificationsToGroup.size() > 0) {
if (autogroupSummaryExists) {
mCallback.updateAutogroupSummary(sbn.getUserId(), sbn.getPackageName(), flags);
} else {
mCallback.addAutoGroupSummary(
sbn.getUserId(), sbn.getPackageName(), sbn.getKey(), flags);
}
for (String key : notificationsToGroup) {
mCallback.addAutoGroup(key);
}
}
}
/**
* A notification was added that's app grouped, or a notification was removed.
* Evaluate whether:
* (a) an existing autogroup summary needs updated flags
* (b) if we need to remove our autogroup overlay for this notification
* (c) we need to remove the autogroup summary
*
* And updates the internal state of un-app-grouped notifications and their flags
*/
private void maybeUngroup(StatusBarNotification sbn, boolean notificationGone, int userId) {
boolean removeSummary = false;
int summaryFlags = 0;
boolean updateSummaryFlags = false;
boolean removeAutogroupOverlay = false;
synchronized (mUngroupedNotifications) {
String key = generatePackageKey(sbn.getUserId(), sbn.getPackageName());
final ArrayMap<String, Integer> children =
mUngroupedNotifications.getOrDefault(key, new ArrayMap<>());
if (children.size() == 0) {
return;
}
// if this notif was autogrouped and now isn't
if (children.containsKey(sbn.getKey())) {
// if this notification was contributing flags that aren't covered by other
// children to the summary, reevaluate flags for the summary
int flags = children.remove(sbn.getKey());
// this
if (hasAnyFlag(flags, ANY_CHILDREN_FLAGS)) {
updateSummaryFlags = true;
summaryFlags = getAutogroupSummaryFlags(children);
}
// if this notification still exists and has an autogroup overlay, but is now
// grouped by the app, clear the overlay
if (!notificationGone && sbn.getOverrideGroupKey() != null) {
removeAutogroupOverlay = true;
}
// If there are no more children left to autogroup, remove the summary
if (children.size() == 0) {
removeSummary = true;
}
}
}
if (removeSummary) {
mCallback.removeAutoGroupSummary(userId, sbn.getPackageName());
} else {
if (updateSummaryFlags) {
mCallback.updateAutogroupSummary(userId, sbn.getPackageName(), summaryFlags);
}
}
if (removeAutogroupOverlay) {
mCallback.removeAutoGroup(sbn.getKey());
}
}
@VisibleForTesting
int getNotGroupedByAppCount(int userId, String pkg) {
synchronized (mUngroupedNotifications) {
String key = generatePackageKey(userId, pkg);
final ArrayMap<String, Integer> children =
mUngroupedNotifications.getOrDefault(key, new ArrayMap<>());
return children.size();
}
}
protected interface Callback {
void addAutoGroup(String key);
void removeAutoGroup(String key);
void addAutoGroupSummary(int userId, String pkg, String triggeringKey, int flags);
void removeAutoGroupSummary(int user, String pkg);
void updateAutogroupSummary(int userId, String pkg, int flags);
}
}