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.Log;
20 import android.util.Slog;
21 
22 import java.util.ArrayList;
23 import java.util.HashMap;
24 import java.util.LinkedHashSet;
25 import java.util.List;
26 import java.util.Map;
27 
28 /**
29  * NotificationManagerService helper for auto-grouping notifications.
30  */
31 public class GroupHelper {
32     private static final String TAG = "GroupHelper";
33     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
34 
35     protected static final String AUTOGROUP_KEY = "ranker_group";
36 
37     private final Callback mCallback;
38     private final int mAutoGroupAtCount;
39 
40     // Map of user : <Map of package : notification keys>. Only contains notifications that are not
41     // grouped by the app (aka no group or sort key).
42     Map<Integer, Map<String, LinkedHashSet<String>>> mUngroupedNotifications = new HashMap<>();
43 
GroupHelper(int autoGroupAtCount, Callback callback)44     public GroupHelper(int autoGroupAtCount, Callback callback) {
45         mAutoGroupAtCount = autoGroupAtCount;
46         mCallback = callback;
47     }
48 
onNotificationPosted(StatusBarNotification sbn, boolean autogroupSummaryExists)49     public void onNotificationPosted(StatusBarNotification sbn, boolean autogroupSummaryExists) {
50         if (DEBUG) Log.i(TAG, "POSTED " + sbn.getKey());
51         try {
52             List<String> notificationsToGroup = new ArrayList<>();
53             if (!sbn.isAppGroup()) {
54                 // Not grouped by the app, add to the list of notifications for the app;
55                 // send grouping update if app exceeds the autogrouping limit.
56                 synchronized (mUngroupedNotifications) {
57                     Map<String, LinkedHashSet<String>> ungroupedNotificationsByUser
58                             = mUngroupedNotifications.get(sbn.getUserId());
59                     if (ungroupedNotificationsByUser == null) {
60                         ungroupedNotificationsByUser = new HashMap<>();
61                     }
62                     mUngroupedNotifications.put(sbn.getUserId(), ungroupedNotificationsByUser);
63                     LinkedHashSet<String> notificationsForPackage
64                             = ungroupedNotificationsByUser.get(sbn.getPackageName());
65                     if (notificationsForPackage == null) {
66                         notificationsForPackage = new LinkedHashSet<>();
67                     }
68 
69                     notificationsForPackage.add(sbn.getKey());
70                     ungroupedNotificationsByUser.put(sbn.getPackageName(), notificationsForPackage);
71 
72                     if (notificationsForPackage.size() >= mAutoGroupAtCount
73                             || autogroupSummaryExists) {
74                         notificationsToGroup.addAll(notificationsForPackage);
75                     }
76                 }
77                 if (notificationsToGroup.size() > 0) {
78                     adjustAutogroupingSummary(sbn.getUserId(), sbn.getPackageName(),
79                             notificationsToGroup.get(0), true);
80                     adjustNotificationBundling(notificationsToGroup, true);
81                 }
82             } else {
83                 // Grouped, but not by us. Send updates to un-autogroup, if we grouped it.
84                 maybeUngroup(sbn, false, sbn.getUserId());
85             }
86         } catch (Exception e) {
87             Slog.e(TAG, "Failure processing new notification", e);
88         }
89     }
90 
onNotificationRemoved(StatusBarNotification sbn)91     public void onNotificationRemoved(StatusBarNotification sbn) {
92         try {
93             maybeUngroup(sbn, true, sbn.getUserId());
94         } catch (Exception e) {
95             Slog.e(TAG, "Error processing canceled notification", e);
96         }
97     }
98 
99     /**
100      * Un-autogroups notifications that are now grouped by the app.
101      */
maybeUngroup(StatusBarNotification sbn, boolean notificationGone, int userId)102     private void maybeUngroup(StatusBarNotification sbn, boolean notificationGone, int userId) {
103         List<String> notificationsToUnAutogroup = new ArrayList<>();
104         boolean removeSummary = false;
105         synchronized (mUngroupedNotifications) {
106             Map<String, LinkedHashSet<String>> ungroupedNotificationsByUser
107                     = mUngroupedNotifications.get(sbn.getUserId());
108             if (ungroupedNotificationsByUser == null || ungroupedNotificationsByUser.size() == 0) {
109                 return;
110             }
111             LinkedHashSet<String> notificationsForPackage
112                     = ungroupedNotificationsByUser.get(sbn.getPackageName());
113             if (notificationsForPackage == null || notificationsForPackage.size() == 0) {
114                 return;
115             }
116             if (notificationsForPackage.remove(sbn.getKey())) {
117                 if (!notificationGone) {
118                     // Add the current notification to the ungrouping list if it still exists.
119                     notificationsToUnAutogroup.add(sbn.getKey());
120                 }
121             }
122             // If the status change of this notification has brought the number of loose
123             // notifications to zero, remove the summary and un-autogroup.
124             if (notificationsForPackage.size() == 0) {
125                 ungroupedNotificationsByUser.remove(sbn.getPackageName());
126                 removeSummary = true;
127             }
128         }
129         if (removeSummary) {
130             adjustAutogroupingSummary(userId, sbn.getPackageName(), null, false);
131         }
132         if (notificationsToUnAutogroup.size() > 0) {
133             adjustNotificationBundling(notificationsToUnAutogroup, false);
134         }
135     }
136 
adjustAutogroupingSummary(int userId, String packageName, String triggeringKey, boolean summaryNeeded)137     private void adjustAutogroupingSummary(int userId, String packageName, String triggeringKey,
138             boolean summaryNeeded) {
139         if (summaryNeeded) {
140             mCallback.addAutoGroupSummary(userId, packageName, triggeringKey);
141         } else {
142             mCallback.removeAutoGroupSummary(userId, packageName);
143         }
144     }
145 
adjustNotificationBundling(List<String> keys, boolean group)146     private void adjustNotificationBundling(List<String> keys, boolean group) {
147         for (String key : keys) {
148             if (DEBUG) Log.i(TAG, "Sending grouping adjustment for: " + key + " group? " + group);
149             if (group) {
150                 mCallback.addAutoGroup(key);
151             } else {
152                 mCallback.removeAutoGroup(key);
153             }
154         }
155     }
156 
157     protected interface Callback {
addAutoGroup(String key)158         void addAutoGroup(String key);
removeAutoGroup(String key)159         void removeAutoGroup(String key);
addAutoGroupSummary(int userId, String pkg, String triggeringKey)160         void addAutoGroupSummary(int userId, String pkg, String triggeringKey);
removeAutoGroupSummary(int user, String pkg)161         void removeAutoGroupSummary(int user, String pkg);
162     }
163 }
164