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 17 package android.ext.services.notification; 18 19 import static android.service.notification.NotificationListenerService.Ranking.IMPORTANCE_UNSPECIFIED; 20 21 import android.os.Bundle; 22 import android.os.UserHandle; 23 import android.service.notification.Adjustment; 24 import android.service.notification.NotificationRankerService; 25 import android.service.notification.StatusBarNotification; 26 import android.util.Log; 27 import android.util.Slog; 28 29 import java.util.ArrayList; 30 import java.util.HashMap; 31 import java.util.LinkedHashSet; 32 import java.util.List; 33 import java.util.Map; 34 35 import android.ext.services.R; 36 37 /** 38 * Class that provides an updatable ranker module for the notification manager.. 39 */ 40 public final class Ranker extends NotificationRankerService { 41 private static final String TAG = "RocketRanker"; 42 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 43 44 private static final int AUTOBUNDLE_AT_COUNT = 4; 45 private static final String AUTOBUNDLE_KEY = "ranker_bundle"; 46 47 // Map of user : <Map of package : notification keys>. Only contains notifications that are not 48 // bundled by the app (aka no group or sort key). 49 Map<Integer, Map<String, LinkedHashSet<String>>> mUnbundledNotifications; 50 51 @Override 52 public Adjustment onNotificationEnqueued(StatusBarNotification sbn, int importance, 53 boolean user) { 54 if (DEBUG) Log.i(TAG, "ENQUEUED " + sbn.getKey()); 55 return null; 56 } 57 58 @Override 59 public void onNotificationPosted(StatusBarNotification sbn) { 60 if (DEBUG) Log.i(TAG, "POSTED " + sbn.getKey()); 61 try { 62 List<String> notificationsToBundle = new ArrayList<>(); 63 if (!sbn.isAppGroup()) { 64 // Not grouped by the app, add to the list of notifications for the app; 65 // send bundling update if app exceeds the autobundling limit. 66 synchronized (mUnbundledNotifications) { 67 Map<String, LinkedHashSet<String>> unbundledNotificationsByUser 68 = mUnbundledNotifications.get(sbn.getUserId()); 69 if (unbundledNotificationsByUser == null) { 70 unbundledNotificationsByUser = new HashMap<>(); 71 } 72 mUnbundledNotifications.put(sbn.getUserId(), unbundledNotificationsByUser); 73 LinkedHashSet<String> notificationsForPackage 74 = unbundledNotificationsByUser.get(sbn.getPackageName()); 75 if (notificationsForPackage == null) { 76 notificationsForPackage = new LinkedHashSet<>(); 77 } 78 79 notificationsForPackage.add(sbn.getKey()); 80 unbundledNotificationsByUser.put(sbn.getPackageName(), notificationsForPackage); 81 82 if (notificationsForPackage.size() >= AUTOBUNDLE_AT_COUNT) { 83 for (String key : notificationsForPackage) { 84 notificationsToBundle.add(key); 85 } 86 } 87 } 88 if (notificationsToBundle.size() > 0) { 89 adjustAutobundlingSummary(sbn.getPackageName(), notificationsToBundle.get(0), 90 true, sbn.getUserId()); 91 adjustNotificationBundling(sbn.getPackageName(), notificationsToBundle, true, 92 sbn.getUserId()); 93 } 94 } else { 95 // Grouped, but not by us. Send updates to unautobundle, if we bundled it. 96 maybeUnbundle(sbn, false, sbn.getUserId()); 97 } 98 } catch (Exception e) { 99 Slog.e(TAG, "Failure processing new notification", e); 100 } 101 } 102 103 @Override 104 public void onNotificationRemoved(StatusBarNotification sbn) { 105 try { 106 maybeUnbundle(sbn, true, sbn.getUserId()); 107 } catch (Exception e) { 108 Slog.e(TAG, "Error processing canceled notification", e); 109 } 110 } 111 112 /** 113 * Un-autobundles notifications that are now grouped by the app. Additionally cancels 114 * autobundling if the status change of this notification resulted in the loose notification 115 * count being under the limit. 116 */ 117 private void maybeUnbundle(StatusBarNotification sbn, boolean notificationGone, int user) { 118 List<String> notificationsToUnAutobundle = new ArrayList<>(); 119 boolean removeSummary = false; 120 synchronized (mUnbundledNotifications) { 121 Map<String, LinkedHashSet<String>> unbundledNotificationsByUser 122 = mUnbundledNotifications.get(sbn.getUserId()); 123 if (unbundledNotificationsByUser == null || unbundledNotificationsByUser.size() == 0) { 124 return; 125 } 126 LinkedHashSet<String> notificationsForPackage 127 = unbundledNotificationsByUser.get(sbn.getPackageName()); 128 if (notificationsForPackage == null || notificationsForPackage.size() == 0) { 129 return; 130 } 131 if (notificationsForPackage.remove(sbn.getKey())) { 132 if (!notificationGone) { 133 // Add the current notification to the unbundling list if it still exists. 134 notificationsToUnAutobundle.add(sbn.getKey()); 135 } 136 // If the status change of this notification has brought the number of loose 137 // notifications back below the limit, remove the summary and un-autobundle. 138 if (notificationsForPackage.size() == AUTOBUNDLE_AT_COUNT - 1) { 139 removeSummary = true; 140 for (String key : notificationsForPackage) { 141 notificationsToUnAutobundle.add(key); 142 } 143 } 144 } 145 } 146 if (notificationsToUnAutobundle.size() > 0) { 147 if (removeSummary) { 148 adjustAutobundlingSummary(sbn.getPackageName(), null, false, user); 149 } 150 adjustNotificationBundling(sbn.getPackageName(), notificationsToUnAutobundle, false, 151 user); 152 } 153 } 154 155 @Override 156 public void onListenerConnected() { 157 if (DEBUG) Log.i(TAG, "CONNECTED"); 158 mUnbundledNotifications = new HashMap<>(); 159 for (StatusBarNotification sbn : getActiveNotifications()) { 160 onNotificationPosted(sbn); 161 } 162 } 163 164 private void adjustAutobundlingSummary(String packageName, String key, boolean summaryNeeded, 165 int user) { 166 Bundle signals = new Bundle(); 167 if (summaryNeeded) { 168 signals.putBoolean(Adjustment.NEEDS_AUTOGROUPING_KEY, true); 169 signals.putString(Adjustment.GROUP_KEY_OVERRIDE_KEY, AUTOBUNDLE_KEY); 170 } else { 171 signals.putBoolean(Adjustment.NEEDS_AUTOGROUPING_KEY, false); 172 } 173 Adjustment adjustment = new Adjustment(packageName, key, IMPORTANCE_UNSPECIFIED, signals, 174 getContext().getString(R.string.notification_ranker_autobundle_explanation), null, 175 user); 176 if (DEBUG) { 177 Log.i(TAG, "Summary update for: " + packageName + " " 178 + (summaryNeeded ? "adding" : "removing")); 179 } 180 try { 181 adjustNotification(adjustment); 182 } catch (Exception e) { 183 Slog.e(TAG, "Adjustment failed", e); 184 } 185 186 } 187 private void adjustNotificationBundling(String packageName, List<String> keys, boolean bundle, 188 int user) { 189 List<Adjustment> adjustments = new ArrayList<>(); 190 for (String key : keys) { 191 adjustments.add(createBundlingAdjustment(packageName, key, bundle, user)); 192 if (DEBUG) Log.i(TAG, "Sending bundling adjustment for: " + key); 193 } 194 try { 195 adjustNotifications(adjustments); 196 } catch (Exception e) { 197 Slog.e(TAG, "Adjustments failed", e); 198 } 199 } 200 201 private Adjustment createBundlingAdjustment(String packageName, String key, boolean bundle, 202 int user) { 203 Bundle signals = new Bundle(); 204 if (bundle) { 205 signals.putString(Adjustment.GROUP_KEY_OVERRIDE_KEY, AUTOBUNDLE_KEY); 206 } else { 207 signals.putString(Adjustment.GROUP_KEY_OVERRIDE_KEY, null); 208 } 209 return new Adjustment(packageName, key, IMPORTANCE_UNSPECIFIED, signals, 210 getContext().getString(R.string.notification_ranker_autobundle_explanation), 211 null, user); 212 } 213 214 }