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