1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.server.notification; 17 18 import static android.app.Notification.FLAG_AUTOGROUP_SUMMARY; 19 import static android.app.Notification.FLAG_AUTO_CANCEL; 20 import static android.app.Notification.FLAG_GROUP_SUMMARY; 21 import static android.app.Notification.FLAG_LOCAL_ONLY; 22 import static android.app.Notification.FLAG_NO_CLEAR; 23 import static android.app.Notification.FLAG_ONGOING_EVENT; 24 25 import android.annotation.NonNull; 26 import android.service.notification.StatusBarNotification; 27 import android.util.ArrayMap; 28 import android.util.Slog; 29 30 import com.android.internal.annotations.GuardedBy; 31 import com.android.internal.annotations.VisibleForTesting; 32 33 import java.util.ArrayList; 34 import java.util.List; 35 36 /** 37 * NotificationManagerService helper for auto-grouping notifications. 38 */ 39 public class GroupHelper { 40 private static final String TAG = "GroupHelper"; 41 42 protected static final String AUTOGROUP_KEY = "ranker_group"; 43 44 // Flags that all autogroup summaries have 45 protected static final int BASE_FLAGS = 46 FLAG_AUTOGROUP_SUMMARY | FLAG_GROUP_SUMMARY | FLAG_LOCAL_ONLY; 47 // Flag that autogroup summaries inherits if all children have the flag 48 private static final int ALL_CHILDREN_FLAG = FLAG_AUTO_CANCEL; 49 // Flags that autogroup summaries inherits if any child has them 50 private static final int ANY_CHILDREN_FLAGS = FLAG_ONGOING_EVENT | FLAG_NO_CLEAR; 51 52 private final Callback mCallback; 53 private final int mAutoGroupAtCount; 54 55 // Only contains notifications that are not explicitly grouped by the app (aka no group or 56 // sort key). 57 // userId|packageName -> (keys of notifications that aren't in an explicit app group -> flags) 58 @GuardedBy("mUngroupedNotifications") 59 private final ArrayMap<String, ArrayMap<String, Integer>> mUngroupedNotifications 60 = new ArrayMap<>(); 61 GroupHelper(int autoGroupAtCount, Callback callback)62 public GroupHelper(int autoGroupAtCount, Callback callback) { 63 mAutoGroupAtCount = autoGroupAtCount; 64 mCallback = callback; 65 } 66 generatePackageKey(int userId, String pkg)67 private String generatePackageKey(int userId, String pkg) { 68 return userId + "|" + pkg; 69 } 70 71 @VisibleForTesting 72 @GuardedBy("mUngroupedNotifications") getAutogroupSummaryFlags(@onNull final ArrayMap<String, Integer> children)73 protected int getAutogroupSummaryFlags(@NonNull final ArrayMap<String, Integer> children) { 74 boolean allChildrenHasFlag = children.size() > 0; 75 int anyChildFlagSet = 0; 76 for (int i = 0; i < children.size(); i++) { 77 if (!hasAnyFlag(children.valueAt(i), ALL_CHILDREN_FLAG)) { 78 allChildrenHasFlag = false; 79 } 80 if (hasAnyFlag(children.valueAt(i), ANY_CHILDREN_FLAGS)) { 81 anyChildFlagSet |= (children.valueAt(i) & ANY_CHILDREN_FLAGS); 82 } 83 } 84 return BASE_FLAGS | (allChildrenHasFlag ? ALL_CHILDREN_FLAG : 0) | anyChildFlagSet; 85 } 86 hasAnyFlag(int flags, int mask)87 private boolean hasAnyFlag(int flags, int mask) { 88 return (flags & mask) != 0; 89 } 90 onNotificationPosted(StatusBarNotification sbn, boolean autogroupSummaryExists)91 public void onNotificationPosted(StatusBarNotification sbn, boolean autogroupSummaryExists) { 92 try { 93 if (!sbn.isAppGroup()) { 94 maybeGroup(sbn, autogroupSummaryExists); 95 } else { 96 maybeUngroup(sbn, false, sbn.getUserId()); 97 } 98 99 } catch (Exception e) { 100 Slog.e(TAG, "Failure processing new notification", e); 101 } 102 } 103 onNotificationRemoved(StatusBarNotification sbn)104 public void onNotificationRemoved(StatusBarNotification sbn) { 105 try { 106 maybeUngroup(sbn, true, sbn.getUserId()); 107 } catch (Exception e) { 108 Slog.e(TAG, "Error processing canceled notification", e); 109 } 110 } 111 112 /** 113 * A non-app grouped notification has been added or updated 114 * Evaluate if: 115 * (a) an existing autogroup summary needs updated flags 116 * (b) a new autogroup summary needs to be added with correct flags 117 * (c) other non-app grouped children need to be moved to the autogroup 118 * 119 * And stores the list of upgrouped notifications & their flags 120 */ maybeGroup(StatusBarNotification sbn, boolean autogroupSummaryExists)121 private void maybeGroup(StatusBarNotification sbn, boolean autogroupSummaryExists) { 122 int flags = 0; 123 List<String> notificationsToGroup = new ArrayList<>(); 124 synchronized (mUngroupedNotifications) { 125 String key = generatePackageKey(sbn.getUserId(), sbn.getPackageName()); 126 final ArrayMap<String, Integer> children = 127 mUngroupedNotifications.getOrDefault(key, new ArrayMap<>()); 128 129 children.put(sbn.getKey(), sbn.getNotification().flags); 130 mUngroupedNotifications.put(key, children); 131 132 if (children.size() >= mAutoGroupAtCount || autogroupSummaryExists) { 133 flags = getAutogroupSummaryFlags(children); 134 notificationsToGroup.addAll(children.keySet()); 135 } 136 } 137 if (notificationsToGroup.size() > 0) { 138 if (autogroupSummaryExists) { 139 mCallback.updateAutogroupSummary(sbn.getUserId(), sbn.getPackageName(), flags); 140 } else { 141 mCallback.addAutoGroupSummary( 142 sbn.getUserId(), sbn.getPackageName(), sbn.getKey(), flags); 143 } 144 for (String key : notificationsToGroup) { 145 mCallback.addAutoGroup(key); 146 } 147 } 148 } 149 150 /** 151 * A notification was added that's app grouped, or a notification was removed. 152 * Evaluate whether: 153 * (a) an existing autogroup summary needs updated flags 154 * (b) if we need to remove our autogroup overlay for this notification 155 * (c) we need to remove the autogroup summary 156 * 157 * And updates the internal state of un-app-grouped notifications and their flags 158 */ maybeUngroup(StatusBarNotification sbn, boolean notificationGone, int userId)159 private void maybeUngroup(StatusBarNotification sbn, boolean notificationGone, int userId) { 160 boolean removeSummary = false; 161 int summaryFlags = 0; 162 boolean updateSummaryFlags = false; 163 boolean removeAutogroupOverlay = false; 164 synchronized (mUngroupedNotifications) { 165 String key = generatePackageKey(sbn.getUserId(), sbn.getPackageName()); 166 final ArrayMap<String, Integer> children = 167 mUngroupedNotifications.getOrDefault(key, new ArrayMap<>()); 168 if (children.size() == 0) { 169 return; 170 } 171 172 // if this notif was autogrouped and now isn't 173 if (children.containsKey(sbn.getKey())) { 174 // if this notification was contributing flags that aren't covered by other 175 // children to the summary, reevaluate flags for the summary 176 int flags = children.remove(sbn.getKey()); 177 // this 178 if (hasAnyFlag(flags, ANY_CHILDREN_FLAGS)) { 179 updateSummaryFlags = true; 180 summaryFlags = getAutogroupSummaryFlags(children); 181 } 182 // if this notification still exists and has an autogroup overlay, but is now 183 // grouped by the app, clear the overlay 184 if (!notificationGone && sbn.getOverrideGroupKey() != null) { 185 removeAutogroupOverlay = true; 186 } 187 188 // If there are no more children left to autogroup, remove the summary 189 if (children.size() == 0) { 190 removeSummary = true; 191 } 192 } 193 } 194 if (removeSummary) { 195 mCallback.removeAutoGroupSummary(userId, sbn.getPackageName()); 196 } else { 197 if (updateSummaryFlags) { 198 mCallback.updateAutogroupSummary(userId, sbn.getPackageName(), summaryFlags); 199 } 200 } 201 if (removeAutogroupOverlay) { 202 mCallback.removeAutoGroup(sbn.getKey()); 203 } 204 } 205 206 @VisibleForTesting getNotGroupedByAppCount(int userId, String pkg)207 int getNotGroupedByAppCount(int userId, String pkg) { 208 synchronized (mUngroupedNotifications) { 209 String key = generatePackageKey(userId, pkg); 210 final ArrayMap<String, Integer> children = 211 mUngroupedNotifications.getOrDefault(key, new ArrayMap<>()); 212 return children.size(); 213 } 214 } 215 216 protected interface Callback { addAutoGroup(String key)217 void addAutoGroup(String key); removeAutoGroup(String key)218 void removeAutoGroup(String key); addAutoGroupSummary(int userId, String pkg, String triggeringKey, int flags)219 void addAutoGroupSummary(int userId, String pkg, String triggeringKey, int flags); removeAutoGroupSummary(int user, String pkg)220 void removeAutoGroupSummary(int user, String pkg); updateAutogroupSummary(int userId, String pkg, int flags)221 void updateAutogroupSummary(int userId, String pkg, int flags); 222 } 223 } 224