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