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