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 android.service.notification.StatusBarNotification; 19 import android.util.ArrayMap; 20 import android.util.ArraySet; 21 import android.util.Log; 22 import android.util.Slog; 23 24 import com.android.internal.annotations.VisibleForTesting; 25 26 import java.util.ArrayList; 27 import java.util.HashMap; 28 import java.util.LinkedHashSet; 29 import java.util.List; 30 import java.util.Map; 31 32 /** 33 * NotificationManagerService helper for auto-grouping notifications. 34 */ 35 public class GroupHelper { 36 private static final String TAG = "GroupHelper"; 37 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 38 39 protected static final String AUTOGROUP_KEY = "ranker_group"; 40 41 private final Callback mCallback; 42 private final int mAutoGroupAtCount; 43 44 // count the number of ongoing notifications per group 45 // userId -> (package name -> (group Id -> (set of notification keys))) 46 final ArrayMap<String, ArraySet<String>> 47 mOngoingGroupCount = new ArrayMap<>(); 48 49 // Map of user : <Map of package : notification keys>. Only contains notifications that are not 50 // grouped by the app (aka no group or sort key). 51 Map<Integer, Map<String, LinkedHashSet<String>>> mUngroupedNotifications = new HashMap<>(); 52 GroupHelper(int autoGroupAtCount, Callback callback)53 public GroupHelper(int autoGroupAtCount, Callback callback) { 54 mAutoGroupAtCount = autoGroupAtCount; 55 mCallback = callback; 56 } 57 generatePackageGroupKey(int userId, String pkg, String group)58 private String generatePackageGroupKey(int userId, String pkg, String group) { 59 return userId + "|" + pkg + "|" + group; 60 } 61 62 @VisibleForTesting getOngoingGroupCount(int userId, String pkg, String group)63 protected int getOngoingGroupCount(int userId, String pkg, String group) { 64 String key = generatePackageGroupKey(userId, pkg, group); 65 return mOngoingGroupCount.getOrDefault(key, new ArraySet<>(0)).size(); 66 } 67 addToOngoingGroupCount(StatusBarNotification sbn, boolean add)68 private void addToOngoingGroupCount(StatusBarNotification sbn, boolean add) { 69 if (sbn.getNotification().isGroupSummary()) return; 70 if (!sbn.isOngoing() && add) return; 71 String group = sbn.getGroup(); 72 if (group == null) return; 73 int userId = sbn.getUser().getIdentifier(); 74 String key = generatePackageGroupKey(userId, sbn.getPackageName(), group); 75 ArraySet<String> notifications = mOngoingGroupCount.getOrDefault(key, new ArraySet<>(0)); 76 if (add) { 77 notifications.add(sbn.getKey()); 78 mOngoingGroupCount.put(key, notifications); 79 } else { 80 notifications.remove(sbn.getKey()); 81 // we dont need to put it back if it is default 82 } 83 String combinedKey = generatePackageGroupKey(userId, sbn.getPackageName(), group); 84 boolean needsOngoingFlag = notifications.size() > 0; 85 mCallback.updateAutogroupSummary(sbn.getKey(), needsOngoingFlag); 86 } 87 onNotificationUpdated(StatusBarNotification childSbn, boolean autogroupSummaryExists)88 public void onNotificationUpdated(StatusBarNotification childSbn, 89 boolean autogroupSummaryExists) { 90 if (childSbn.getGroup() != AUTOGROUP_KEY 91 || childSbn.getNotification().isGroupSummary()) return; 92 if (childSbn.isOngoing()) { 93 addToOngoingGroupCount(childSbn, true); 94 } else { 95 addToOngoingGroupCount(childSbn, false); 96 } 97 } 98 onNotificationPosted(StatusBarNotification sbn, boolean autogroupSummaryExists)99 public void onNotificationPosted(StatusBarNotification sbn, boolean autogroupSummaryExists) { 100 if (DEBUG) Log.i(TAG, "POSTED " + sbn.getKey()); 101 try { 102 List<String> notificationsToGroup = new ArrayList<>(); 103 if (autogroupSummaryExists) addToOngoingGroupCount(sbn, true); 104 if (!sbn.isAppGroup()) { 105 // Not grouped by the app, add to the list of notifications for the app; 106 // send grouping update if app exceeds the autogrouping limit. 107 synchronized (mUngroupedNotifications) { 108 Map<String, LinkedHashSet<String>> ungroupedNotificationsByUser 109 = mUngroupedNotifications.get(sbn.getUserId()); 110 if (ungroupedNotificationsByUser == null) { 111 ungroupedNotificationsByUser = new HashMap<>(); 112 } 113 mUngroupedNotifications.put(sbn.getUserId(), ungroupedNotificationsByUser); 114 LinkedHashSet<String> notificationsForPackage 115 = ungroupedNotificationsByUser.get(sbn.getPackageName()); 116 if (notificationsForPackage == null) { 117 notificationsForPackage = new LinkedHashSet<>(); 118 } 119 120 notificationsForPackage.add(sbn.getKey()); 121 ungroupedNotificationsByUser.put(sbn.getPackageName(), notificationsForPackage); 122 123 if (notificationsForPackage.size() >= mAutoGroupAtCount 124 || autogroupSummaryExists) { 125 notificationsToGroup.addAll(notificationsForPackage); 126 } 127 } 128 if (notificationsToGroup.size() > 0) { 129 adjustAutogroupingSummary(sbn.getUserId(), sbn.getPackageName(), 130 notificationsToGroup.get(0), true); 131 adjustNotificationBundling(notificationsToGroup, true); 132 } 133 } else { 134 // Grouped, but not by us. Send updates to un-autogroup, if we grouped it. 135 maybeUngroup(sbn, false, sbn.getUserId()); 136 } 137 } catch (Exception e) { 138 Slog.e(TAG, "Failure processing new notification", e); 139 } 140 } 141 onNotificationRemoved(StatusBarNotification sbn)142 public void onNotificationRemoved(StatusBarNotification sbn) { 143 try { 144 addToOngoingGroupCount(sbn, false); 145 maybeUngroup(sbn, true, sbn.getUserId()); 146 } catch (Exception e) { 147 Slog.e(TAG, "Error processing canceled notification", e); 148 } 149 } 150 151 /** 152 * Un-autogroups notifications that are now grouped by the app. 153 */ maybeUngroup(StatusBarNotification sbn, boolean notificationGone, int userId)154 private void maybeUngroup(StatusBarNotification sbn, boolean notificationGone, int userId) { 155 List<String> notificationsToUnAutogroup = new ArrayList<>(); 156 boolean removeSummary = false; 157 synchronized (mUngroupedNotifications) { 158 Map<String, LinkedHashSet<String>> ungroupedNotificationsByUser 159 = mUngroupedNotifications.get(sbn.getUserId()); 160 if (ungroupedNotificationsByUser == null || ungroupedNotificationsByUser.size() == 0) { 161 return; 162 } 163 LinkedHashSet<String> notificationsForPackage 164 = ungroupedNotificationsByUser.get(sbn.getPackageName()); 165 if (notificationsForPackage == null || notificationsForPackage.size() == 0) { 166 return; 167 } 168 if (notificationsForPackage.remove(sbn.getKey())) { 169 if (!notificationGone) { 170 // Add the current notification to the ungrouping list if it still exists. 171 notificationsToUnAutogroup.add(sbn.getKey()); 172 } 173 } 174 // If the status change of this notification has brought the number of loose 175 // notifications to zero, remove the summary and un-autogroup. 176 if (notificationsForPackage.size() == 0) { 177 ungroupedNotificationsByUser.remove(sbn.getPackageName()); 178 removeSummary = true; 179 } 180 } 181 if (removeSummary) { 182 adjustAutogroupingSummary(userId, sbn.getPackageName(), null, false); 183 } 184 if (notificationsToUnAutogroup.size() > 0) { 185 adjustNotificationBundling(notificationsToUnAutogroup, false); 186 } 187 } 188 adjustAutogroupingSummary(int userId, String packageName, String triggeringKey, boolean summaryNeeded)189 private void adjustAutogroupingSummary(int userId, String packageName, String triggeringKey, 190 boolean summaryNeeded) { 191 if (summaryNeeded) { 192 mCallback.addAutoGroupSummary(userId, packageName, triggeringKey); 193 } else { 194 mCallback.removeAutoGroupSummary(userId, packageName); 195 } 196 } 197 adjustNotificationBundling(List<String> keys, boolean group)198 private void adjustNotificationBundling(List<String> keys, boolean group) { 199 for (String key : keys) { 200 if (DEBUG) Log.i(TAG, "Sending grouping adjustment for: " + key + " group? " + group); 201 if (group) { 202 mCallback.addAutoGroup(key); 203 } else { 204 mCallback.removeAutoGroup(key); 205 } 206 } 207 } 208 209 protected interface Callback { addAutoGroup(String key)210 void addAutoGroup(String key); removeAutoGroup(String key)211 void removeAutoGroup(String key); addAutoGroupSummary(int userId, String pkg, String triggeringKey)212 void addAutoGroupSummary(int userId, String pkg, String triggeringKey); removeAutoGroupSummary(int user, String pkg)213 void removeAutoGroupSummary(int user, String pkg); updateAutogroupSummary(String key, boolean needsOngoingFlag)214 void updateAutogroupSummary(String key, boolean needsOngoingFlag); 215 } 216 } 217