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|packageName -> (set of ongoing notifications that aren't in an app group) 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 generatePackageKey(int userId, String pkg)58 private String generatePackageKey(int userId, String pkg) { 59 return userId + "|" + pkg; 60 } 61 62 @VisibleForTesting getOngoingGroupCount(int userId, String pkg)63 protected int getOngoingGroupCount(int userId, String pkg) { 64 String key = generatePackageKey(userId, pkg); 65 return mOngoingGroupCount.getOrDefault(key, new ArraySet<>(0)).size(); 66 } 67 updateOngoingGroupCount(StatusBarNotification sbn, boolean add)68 private void updateOngoingGroupCount(StatusBarNotification sbn, boolean add) { 69 if (sbn.getNotification().isGroupSummary()) { 70 return; 71 } 72 String key = generatePackageKey(sbn.getUserId(), sbn.getPackageName()); 73 ArraySet<String> notifications = mOngoingGroupCount.getOrDefault(key, new ArraySet<>(0)); 74 if (add) { 75 notifications.add(sbn.getKey()); 76 mOngoingGroupCount.put(key, notifications); 77 } else { 78 notifications.remove(sbn.getKey()); 79 // we don't need to put it back if it is default 80 } 81 82 boolean needsOngoingFlag = notifications.size() > 0; 83 mCallback.updateAutogroupSummary(sbn.getUserId(), sbn.getPackageName(), needsOngoingFlag); 84 } 85 onNotificationUpdated(StatusBarNotification childSbn)86 public void onNotificationUpdated(StatusBarNotification childSbn) { 87 updateOngoingGroupCount(childSbn, childSbn.isOngoing() && !childSbn.isAppGroup()); 88 } 89 onNotificationPosted(StatusBarNotification sbn, boolean autogroupSummaryExists)90 public void onNotificationPosted(StatusBarNotification sbn, boolean autogroupSummaryExists) { 91 try { 92 updateOngoingGroupCount(sbn, sbn.isOngoing() && !sbn.isAppGroup()); 93 94 List<String> notificationsToGroup = new ArrayList<>(); 95 if (!sbn.isAppGroup()) { 96 // Not grouped by the app, add to the list of notifications for the app; 97 // send grouping update if app exceeds the autogrouping limit. 98 synchronized (mUngroupedNotifications) { 99 Map<String, LinkedHashSet<String>> ungroupedNotificationsByUser 100 = mUngroupedNotifications.get(sbn.getUserId()); 101 if (ungroupedNotificationsByUser == null) { 102 ungroupedNotificationsByUser = new HashMap<>(); 103 } 104 mUngroupedNotifications.put(sbn.getUserId(), ungroupedNotificationsByUser); 105 LinkedHashSet<String> notificationsForPackage 106 = ungroupedNotificationsByUser.get(sbn.getPackageName()); 107 if (notificationsForPackage == null) { 108 notificationsForPackage = new LinkedHashSet<>(); 109 } 110 111 notificationsForPackage.add(sbn.getKey()); 112 ungroupedNotificationsByUser.put(sbn.getPackageName(), notificationsForPackage); 113 114 if (notificationsForPackage.size() >= mAutoGroupAtCount 115 || autogroupSummaryExists) { 116 notificationsToGroup.addAll(notificationsForPackage); 117 } 118 } 119 if (notificationsToGroup.size() > 0) { 120 adjustAutogroupingSummary(sbn.getUserId(), sbn.getPackageName(), 121 notificationsToGroup.get(0), true); 122 adjustNotificationBundling(notificationsToGroup, true); 123 } 124 } else { 125 // Grouped, but not by us. Send updates to un-autogroup, if we grouped it. 126 maybeUngroup(sbn, false, sbn.getUserId()); 127 } 128 129 } catch (Exception e) { 130 Slog.e(TAG, "Failure processing new notification", e); 131 } 132 } 133 onNotificationRemoved(StatusBarNotification sbn)134 public void onNotificationRemoved(StatusBarNotification sbn) { 135 try { 136 updateOngoingGroupCount(sbn, false); 137 maybeUngroup(sbn, true, sbn.getUserId()); 138 } catch (Exception e) { 139 Slog.e(TAG, "Error processing canceled notification", e); 140 } 141 } 142 143 /** 144 * Un-autogroups notifications that are now grouped by the app. 145 */ maybeUngroup(StatusBarNotification sbn, boolean notificationGone, int userId)146 private void maybeUngroup(StatusBarNotification sbn, boolean notificationGone, int userId) { 147 List<String> notificationsToUnAutogroup = new ArrayList<>(); 148 boolean removeSummary = false; 149 synchronized (mUngroupedNotifications) { 150 Map<String, LinkedHashSet<String>> ungroupedNotificationsByUser 151 = mUngroupedNotifications.get(sbn.getUserId()); 152 if (ungroupedNotificationsByUser == null || ungroupedNotificationsByUser.size() == 0) { 153 return; 154 } 155 LinkedHashSet<String> notificationsForPackage 156 = ungroupedNotificationsByUser.get(sbn.getPackageName()); 157 if (notificationsForPackage == null || notificationsForPackage.size() == 0) { 158 return; 159 } 160 if (notificationsForPackage.remove(sbn.getKey())) { 161 if (!notificationGone) { 162 // Add the current notification to the ungrouping list if it still exists. 163 notificationsToUnAutogroup.add(sbn.getKey()); 164 } 165 } 166 // If the status change of this notification has brought the number of loose 167 // notifications to zero, remove the summary and un-autogroup. 168 if (notificationsForPackage.size() == 0) { 169 ungroupedNotificationsByUser.remove(sbn.getPackageName()); 170 removeSummary = true; 171 } 172 } 173 if (removeSummary) { 174 adjustAutogroupingSummary(userId, sbn.getPackageName(), null, false); 175 } 176 if (notificationsToUnAutogroup.size() > 0) { 177 adjustNotificationBundling(notificationsToUnAutogroup, false); 178 } 179 } 180 adjustAutogroupingSummary(int userId, String packageName, String triggeringKey, boolean summaryNeeded)181 private void adjustAutogroupingSummary(int userId, String packageName, String triggeringKey, 182 boolean summaryNeeded) { 183 if (summaryNeeded) { 184 mCallback.addAutoGroupSummary(userId, packageName, triggeringKey, 185 getOngoingGroupCount(userId, packageName) > 0); 186 } else { 187 mCallback.removeAutoGroupSummary(userId, packageName); 188 } 189 } 190 adjustNotificationBundling(List<String> keys, boolean group)191 private void adjustNotificationBundling(List<String> keys, boolean group) { 192 for (String key : keys) { 193 if (DEBUG) Log.i(TAG, "Sending grouping adjustment for: " + key + " group? " + group); 194 if (group) { 195 mCallback.addAutoGroup(key); 196 } else { 197 mCallback.removeAutoGroup(key); 198 } 199 } 200 } 201 202 protected interface Callback { addAutoGroup(String key)203 void addAutoGroup(String key); removeAutoGroup(String key)204 void removeAutoGroup(String key); addAutoGroupSummary(int userId, String pkg, String triggeringKey, boolean needsOngoingFlag)205 void addAutoGroupSummary(int userId, String pkg, String triggeringKey, 206 boolean needsOngoingFlag); removeAutoGroupSummary(int user, String pkg)207 void removeAutoGroupSummary(int user, String pkg); updateAutogroupSummary(int userId, String pkg, boolean needsOngoingFlag)208 void updateAutogroupSummary(int userId, String pkg, boolean needsOngoingFlag); 209 } 210 } 211