1 /*
2  * Copyright (C) 2017 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 
17 package com.android.dialer.notification;
18 
19 import android.app.Notification;
20 import android.app.NotificationManager;
21 import android.content.Context;
22 import android.service.notification.StatusBarNotification;
23 import android.support.annotation.NonNull;
24 import android.text.TextUtils;
25 import com.android.dialer.common.Assert;
26 import com.android.dialer.common.LogUtil;
27 import com.android.dialer.logging.DialerImpression;
28 import com.android.dialer.logging.Logger;
29 import java.util.ArrayList;
30 import java.util.Collections;
31 import java.util.Comparator;
32 import java.util.HashSet;
33 import java.util.List;
34 import java.util.Set;
35 
36 /**
37  * Utility to ensure that only a certain number of notifications are shown for a particular
38  * notification type. Once the limit is reached, older notifications are cancelled.
39  */
40 class NotificationThrottler {
41   /**
42    * For gropued bundled notifications, the system UI will only display the last 8. For grouped
43    * unbundled notifications, the system displays all notifications until a global maximum of 50 is
44    * reached.
45    */
46   private static final int MAX_NOTIFICATIONS_PER_TAG = 8;
47 
48   private static final int HIGH_GLOBAL_NOTIFICATION_COUNT = 45;
49 
50   private static boolean didLogHighGlobalNotificationCountReached;
51 
52   /**
53    * For all the active notifications in the same group as the provided notification, cancel the
54    * earliest ones until the left ones is under limit.
55    *
56    * @param notification the provided notification to determine group
57    * @return a set of cancelled notification
58    */
throttle( @onNull Context context, @NonNull Notification notification)59   static Set<StatusBarNotification> throttle(
60       @NonNull Context context, @NonNull Notification notification) {
61     Assert.isNotNull(context);
62     Assert.isNotNull(notification);
63     Set<StatusBarNotification> throttledNotificationSet = new HashSet<>();
64 
65     // No limiting for non-grouped notifications.
66     String groupKey = notification.getGroup();
67     if (TextUtils.isEmpty(groupKey)) {
68       return throttledNotificationSet;
69     }
70 
71     NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
72     StatusBarNotification[] activeNotifications = notificationManager.getActiveNotifications();
73     if (activeNotifications.length > HIGH_GLOBAL_NOTIFICATION_COUNT
74         && !didLogHighGlobalNotificationCountReached) {
75       LogUtil.i(
76           "NotificationThrottler.throttle",
77           "app has %d notifications, system may suppress future notifications",
78           activeNotifications.length);
79       didLogHighGlobalNotificationCountReached = true;
80       Logger.get(context)
81           .logImpression(DialerImpression.Type.HIGH_GLOBAL_NOTIFICATION_COUNT_REACHED);
82     }
83 
84     // Count the number of notificatons for this group (excluding the summary).
85     int count = 0;
86     for (StatusBarNotification currentNotification : activeNotifications) {
87       if (isNotificationInGroup(currentNotification, groupKey)) {
88         count++;
89       }
90     }
91 
92     if (count > MAX_NOTIFICATIONS_PER_TAG) {
93       LogUtil.i(
94           "NotificationThrottler.throttle",
95           "groupKey: %s is over limit, count: %d, limit: %d",
96           groupKey,
97           count,
98           MAX_NOTIFICATIONS_PER_TAG);
99       List<StatusBarNotification> notifications = getSortedMatchingNotifications(context, groupKey);
100       for (int i = 0; i < (count - MAX_NOTIFICATIONS_PER_TAG); i++) {
101         notificationManager.cancel(notifications.get(i).getTag(), notifications.get(i).getId());
102         throttledNotificationSet.add(notifications.get(i));
103       }
104     }
105     return throttledNotificationSet;
106   }
107 
getSortedMatchingNotifications( @onNull Context context, @NonNull String groupKey)108   private static List<StatusBarNotification> getSortedMatchingNotifications(
109       @NonNull Context context, @NonNull String groupKey) {
110     List<StatusBarNotification> notifications = new ArrayList<>();
111     NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
112     for (StatusBarNotification notification : notificationManager.getActiveNotifications()) {
113       if (isNotificationInGroup(notification, groupKey)) {
114         notifications.add(notification);
115       }
116     }
117     Collections.sort(
118         notifications,
119         new Comparator<StatusBarNotification>() {
120           @Override
121           public int compare(StatusBarNotification left, StatusBarNotification right) {
122             return Long.compare(left.getPostTime(), right.getPostTime());
123           }
124         });
125     return notifications;
126   }
127 
isNotificationInGroup( @onNull StatusBarNotification notification, @NonNull String groupKey)128   private static boolean isNotificationInGroup(
129       @NonNull StatusBarNotification notification, @NonNull String groupKey) {
130     // Don't include group summaries.
131     if ((notification.getNotification().flags & Notification.FLAG_GROUP_SUMMARY) != 0) {
132       return false;
133     }
134 
135     return TextUtils.equals(groupKey, notification.getNotification().getGroup());
136   }
137 
NotificationThrottler()138   private NotificationThrottler() {}
139 }
140