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.COLOR_DEFAULT; 19 import static android.app.Notification.FLAG_AUTOGROUP_SUMMARY; 20 import static android.app.Notification.FLAG_AUTO_CANCEL; 21 import static android.app.Notification.FLAG_GROUP_SUMMARY; 22 import static android.app.Notification.FLAG_LOCAL_ONLY; 23 import static android.app.Notification.FLAG_NO_CLEAR; 24 import static android.app.Notification.FLAG_ONGOING_EVENT; 25 import static android.app.Notification.VISIBILITY_PRIVATE; 26 import static android.app.Notification.VISIBILITY_PUBLIC; 27 28 import android.annotation.NonNull; 29 import android.app.Notification; 30 import android.content.Context; 31 import android.content.pm.PackageManager; 32 import android.content.pm.PackageManager.NameNotFoundException; 33 import android.graphics.drawable.AdaptiveIconDrawable; 34 import android.graphics.drawable.Drawable; 35 import android.graphics.drawable.Icon; 36 import android.service.notification.StatusBarNotification; 37 import android.util.ArrayMap; 38 import android.util.Slog; 39 40 import com.android.internal.R; 41 import com.android.internal.annotations.GuardedBy; 42 import com.android.internal.annotations.VisibleForTesting; 43 44 import java.util.ArrayList; 45 import java.util.List; 46 import java.util.Objects; 47 48 /** 49 * NotificationManagerService helper for auto-grouping notifications. 50 */ 51 public class GroupHelper { 52 private static final String TAG = "GroupHelper"; 53 54 protected static final String AUTOGROUP_KEY = "ranker_group"; 55 56 protected static final int FLAG_INVALID = -1; 57 58 // Flags that all autogroup summaries have 59 protected static final int BASE_FLAGS = 60 FLAG_AUTOGROUP_SUMMARY | FLAG_GROUP_SUMMARY | FLAG_LOCAL_ONLY; 61 // Flag that autogroup summaries inherits if all children have the flag 62 private static final int ALL_CHILDREN_FLAG = FLAG_AUTO_CANCEL; 63 // Flags that autogroup summaries inherits if any child has them 64 private static final int ANY_CHILDREN_FLAGS = FLAG_ONGOING_EVENT | FLAG_NO_CLEAR; 65 66 private final Callback mCallback; 67 private final int mAutoGroupAtCount; 68 private final Context mContext; 69 private final PackageManager mPackageManager; 70 71 // Only contains notifications that are not explicitly grouped by the app (aka no group or 72 // sort key). 73 // userId|packageName -> (keys of notifications that aren't in an explicit app group -> flags) 74 @GuardedBy("mUngroupedNotifications") 75 private final ArrayMap<String, ArrayMap<String, NotificationAttributes>> mUngroupedNotifications 76 = new ArrayMap<>(); 77 GroupHelper(Context context, PackageManager packageManager, int autoGroupAtCount, Callback callback)78 public GroupHelper(Context context, PackageManager packageManager, int autoGroupAtCount, 79 Callback callback) { 80 mAutoGroupAtCount = autoGroupAtCount; 81 mCallback = callback; 82 mContext = context; 83 mPackageManager = packageManager; 84 } 85 generatePackageKey(int userId, String pkg)86 private String generatePackageKey(int userId, String pkg) { 87 return userId + "|" + pkg; 88 } 89 90 @VisibleForTesting 91 @GuardedBy("mUngroupedNotifications") getAutogroupSummaryFlags( @onNull final ArrayMap<String, NotificationAttributes> children)92 protected int getAutogroupSummaryFlags( 93 @NonNull final ArrayMap<String, NotificationAttributes> children) { 94 boolean allChildrenHasFlag = children.size() > 0; 95 int anyChildFlagSet = 0; 96 for (int i = 0; i < children.size(); i++) { 97 if (!hasAnyFlag(children.valueAt(i).flags, ALL_CHILDREN_FLAG)) { 98 allChildrenHasFlag = false; 99 } 100 if (hasAnyFlag(children.valueAt(i).flags, ANY_CHILDREN_FLAGS)) { 101 anyChildFlagSet |= (children.valueAt(i).flags & ANY_CHILDREN_FLAGS); 102 } 103 } 104 return BASE_FLAGS | (allChildrenHasFlag ? ALL_CHILDREN_FLAG : 0) | anyChildFlagSet; 105 } 106 hasAnyFlag(int flags, int mask)107 private boolean hasAnyFlag(int flags, int mask) { 108 return (flags & mask) != 0; 109 } 110 111 /** 112 * Called when a notification is newly posted. Checks whether that notification, and all other 113 * active notifications should be grouped or ungrouped atuomatically, and returns whether. 114 * @param sbn The posted notification. 115 * @param autogroupSummaryExists Whether a summary for this notification already exists. 116 * @return Whether the provided notification should be autogrouped synchronously. 117 */ onNotificationPosted(StatusBarNotification sbn, boolean autogroupSummaryExists)118 public boolean onNotificationPosted(StatusBarNotification sbn, boolean autogroupSummaryExists) { 119 boolean sbnToBeAutogrouped = false; 120 try { 121 if (!sbn.isAppGroup()) { 122 sbnToBeAutogrouped = maybeGroup(sbn, autogroupSummaryExists); 123 } else { 124 maybeUngroup(sbn, false, sbn.getUserId()); 125 } 126 } catch (Exception e) { 127 Slog.e(TAG, "Failure processing new notification", e); 128 } 129 return sbnToBeAutogrouped; 130 } 131 onNotificationRemoved(StatusBarNotification sbn)132 public void onNotificationRemoved(StatusBarNotification sbn) { 133 try { 134 maybeUngroup(sbn, true, sbn.getUserId()); 135 } catch (Exception e) { 136 Slog.e(TAG, "Error processing canceled notification", e); 137 } 138 } 139 140 /** 141 * A non-app grouped notification has been added or updated 142 * Evaluate if: 143 * (a) an existing autogroup summary needs updated flags 144 * (b) a new autogroup summary needs to be added with correct flags 145 * (c) other non-app grouped children need to be moved to the autogroup 146 * 147 * And stores the list of upgrouped notifications & their flags 148 */ maybeGroup(StatusBarNotification sbn, boolean autogroupSummaryExists)149 private boolean maybeGroup(StatusBarNotification sbn, boolean autogroupSummaryExists) { 150 int flags = 0; 151 List<String> notificationsToGroup = new ArrayList<>(); 152 List<NotificationAttributes> childrenAttr = new ArrayList<>(); 153 // Indicates whether the provided sbn should be autogrouped by the caller. 154 boolean sbnToBeAutogrouped = false; 155 synchronized (mUngroupedNotifications) { 156 String packageKey = generatePackageKey(sbn.getUserId(), sbn.getPackageName()); 157 final ArrayMap<String, NotificationAttributes> children = 158 mUngroupedNotifications.getOrDefault(packageKey, new ArrayMap<>()); 159 160 NotificationAttributes attr = new NotificationAttributes(sbn.getNotification().flags, 161 sbn.getNotification().getSmallIcon(), sbn.getNotification().color, 162 sbn.getNotification().visibility); 163 children.put(sbn.getKey(), attr); 164 mUngroupedNotifications.put(packageKey, children); 165 166 if (children.size() >= mAutoGroupAtCount || autogroupSummaryExists) { 167 flags = getAutogroupSummaryFlags(children); 168 notificationsToGroup.addAll(children.keySet()); 169 childrenAttr.addAll(children.values()); 170 } 171 } 172 if (notificationsToGroup.size() > 0) { 173 if (autogroupSummaryExists) { 174 NotificationAttributes attr = new NotificationAttributes(flags, 175 sbn.getNotification().getSmallIcon(), sbn.getNotification().color, 176 VISIBILITY_PRIVATE); 177 if (Flags.autogroupSummaryIconUpdate()) { 178 attr = updateAutobundledSummaryAttributes(sbn.getPackageName(), childrenAttr, 179 attr); 180 } 181 182 mCallback.updateAutogroupSummary(sbn.getUserId(), sbn.getPackageName(), attr); 183 } else { 184 Icon summaryIcon = sbn.getNotification().getSmallIcon(); 185 int summaryIconColor = sbn.getNotification().color; 186 int summaryVisibility = VISIBILITY_PRIVATE; 187 if (Flags.autogroupSummaryIconUpdate()) { 188 // Calculate the initial summary icon, icon color and visibility 189 NotificationAttributes iconAttr = getAutobundledSummaryAttributes( 190 sbn.getPackageName(), childrenAttr); 191 summaryIcon = iconAttr.icon; 192 summaryIconColor = iconAttr.iconColor; 193 summaryVisibility = iconAttr.visibility; 194 } 195 196 NotificationAttributes attr = new NotificationAttributes(flags, summaryIcon, 197 summaryIconColor, summaryVisibility); 198 mCallback.addAutoGroupSummary(sbn.getUserId(), sbn.getPackageName(), sbn.getKey(), 199 attr); 200 } 201 for (String keyToGroup : notificationsToGroup) { 202 if (android.app.Flags.checkAutogroupBeforePost()) { 203 if (keyToGroup.equals(sbn.getKey())) { 204 // Autogrouping for the provided notification is to be done synchronously. 205 sbnToBeAutogrouped = true; 206 } else { 207 mCallback.addAutoGroup(keyToGroup, /*requestSort=*/true); 208 } 209 } else { 210 mCallback.addAutoGroup(keyToGroup, /*requestSort=*/true); 211 } 212 } 213 } 214 return sbnToBeAutogrouped; 215 } 216 217 /** 218 * A notification was added that's app grouped, or a notification was removed. 219 * Evaluate whether: 220 * (a) an existing autogroup summary needs updated flags 221 * (b) if we need to remove our autogroup overlay for this notification 222 * (c) we need to remove the autogroup summary 223 * 224 * And updates the internal state of un-app-grouped notifications and their flags. 225 */ maybeUngroup(StatusBarNotification sbn, boolean notificationGone, int userId)226 private void maybeUngroup(StatusBarNotification sbn, boolean notificationGone, int userId) { 227 boolean removeSummary = false; 228 int summaryFlags = FLAG_INVALID; 229 boolean updateSummaryFlags = false; 230 boolean removeAutogroupOverlay = false; 231 List<NotificationAttributes> childrenAttrs = new ArrayList<>(); 232 synchronized (mUngroupedNotifications) { 233 String key = generatePackageKey(sbn.getUserId(), sbn.getPackageName()); 234 final ArrayMap<String, NotificationAttributes> children = 235 mUngroupedNotifications.getOrDefault(key, new ArrayMap<>()); 236 if (children.size() == 0) { 237 return; 238 } 239 240 // if this notif was autogrouped and now isn't 241 if (children.containsKey(sbn.getKey())) { 242 // if this notification was contributing flags that aren't covered by other 243 // children to the summary, reevaluate flags for the summary 244 int flags = children.remove(sbn.getKey()).flags; 245 // this 246 if (hasAnyFlag(flags, ANY_CHILDREN_FLAGS)) { 247 updateSummaryFlags = true; 248 summaryFlags = getAutogroupSummaryFlags(children); 249 } 250 // if this notification still exists and has an autogroup overlay, but is now 251 // grouped by the app, clear the overlay 252 if (!notificationGone && sbn.getOverrideGroupKey() != null) { 253 removeAutogroupOverlay = true; 254 } 255 256 // If there are no more children left to autogroup, remove the summary 257 if (children.size() == 0) { 258 removeSummary = true; 259 } else { 260 childrenAttrs.addAll(children.values()); 261 } 262 } 263 } 264 265 if (removeSummary) { 266 mCallback.removeAutoGroupSummary(userId, sbn.getPackageName()); 267 } else { 268 NotificationAttributes attr = new NotificationAttributes(summaryFlags, 269 sbn.getNotification().getSmallIcon(), sbn.getNotification().color, 270 VISIBILITY_PRIVATE); 271 boolean attributesUpdated = false; 272 if (Flags.autogroupSummaryIconUpdate()) { 273 NotificationAttributes newAttr = updateAutobundledSummaryAttributes( 274 sbn.getPackageName(), childrenAttrs, attr); 275 if (!newAttr.equals(attr)) { 276 attributesUpdated = true; 277 attr = newAttr; 278 } 279 } 280 281 if (updateSummaryFlags || attributesUpdated) { 282 mCallback.updateAutogroupSummary(userId, sbn.getPackageName(), attr); 283 } 284 } 285 if (removeAutogroupOverlay) { 286 mCallback.removeAutoGroup(sbn.getKey()); 287 } 288 } 289 290 @VisibleForTesting getNotGroupedByAppCount(int userId, String pkg)291 int getNotGroupedByAppCount(int userId, String pkg) { 292 synchronized (mUngroupedNotifications) { 293 String key = generatePackageKey(userId, pkg); 294 final ArrayMap<String, NotificationAttributes> children = 295 mUngroupedNotifications.getOrDefault(key, new ArrayMap<>()); 296 return children.size(); 297 } 298 } 299 getAutobundledSummaryAttributes(@onNull String packageName, @NonNull List<NotificationAttributes> childrenAttr)300 NotificationAttributes getAutobundledSummaryAttributes(@NonNull String packageName, 301 @NonNull List<NotificationAttributes> childrenAttr) { 302 Icon newIcon = null; 303 boolean childrenHaveSameIcon = true; 304 int newColor = Notification.COLOR_INVALID; 305 boolean childrenHaveSameColor = true; 306 int newVisibility = VISIBILITY_PRIVATE; 307 308 // Both the icon drawable and the icon background color are updated according to this rule: 309 // - if all child icons are identical => use the common icon 310 // - if child icons are different: use the monochromatic app icon, if exists. 311 // Otherwise fall back to a generic icon representing a stack. 312 for (NotificationAttributes state: childrenAttr) { 313 // Check for icon 314 if (newIcon == null) { 315 newIcon = state.icon; 316 } else { 317 if (!newIcon.sameAs(state.icon)) { 318 childrenHaveSameIcon = false; 319 } 320 } 321 // Check for color 322 if (newColor == Notification.COLOR_INVALID) { 323 newColor = state.iconColor; 324 } else { 325 if (newColor != state.iconColor) { 326 childrenHaveSameColor = false; 327 } 328 } 329 // Check for visibility. If at least one child is public, then set to public 330 if (state.visibility == VISIBILITY_PUBLIC) { 331 newVisibility = VISIBILITY_PUBLIC; 332 } 333 } 334 if (!childrenHaveSameIcon) { 335 newIcon = getMonochromeAppIcon(packageName); 336 } 337 if (!childrenHaveSameColor) { 338 newColor = COLOR_DEFAULT; 339 } 340 341 return new NotificationAttributes(0, newIcon, newColor, newVisibility); 342 } 343 updateAutobundledSummaryAttributes(@onNull String packageName, @NonNull List<NotificationAttributes> childrenAttr, @NonNull NotificationAttributes oldAttr)344 NotificationAttributes updateAutobundledSummaryAttributes(@NonNull String packageName, 345 @NonNull List<NotificationAttributes> childrenAttr, 346 @NonNull NotificationAttributes oldAttr) { 347 NotificationAttributes newAttr = getAutobundledSummaryAttributes(packageName, 348 childrenAttr); 349 Icon newIcon = newAttr.icon; 350 int newColor = newAttr.iconColor; 351 if (newAttr.icon == null) { 352 newIcon = oldAttr.icon; 353 } 354 if (newAttr.iconColor == Notification.COLOR_INVALID) { 355 newColor = oldAttr.iconColor; 356 } 357 358 return new NotificationAttributes(oldAttr.flags, newIcon, newColor, newAttr.visibility); 359 } 360 361 /** 362 * Get the monochrome app icon for an app from the adaptive launcher icon 363 * or a fallback generic icon for autogroup summaries. 364 * 365 * @param pkg packageName of the app 366 * @return a monochrome app icon or a fallback generic icon 367 */ 368 @NonNull getMonochromeAppIcon(@onNull final String pkg)369 Icon getMonochromeAppIcon(@NonNull final String pkg) { 370 Icon monochromeIcon = null; 371 final int fallbackIconResId = R.drawable.ic_notification_summary_auto; 372 try { 373 final Drawable appIcon = mPackageManager.getApplicationIcon(pkg); 374 if (appIcon instanceof AdaptiveIconDrawable) { 375 if (((AdaptiveIconDrawable) appIcon).getMonochrome() != null) { 376 monochromeIcon = Icon.createWithResourceAdaptiveDrawable(pkg, 377 ((AdaptiveIconDrawable) appIcon).getSourceDrawableResId(), true, 378 -2.0f * AdaptiveIconDrawable.getExtraInsetFraction()); 379 } 380 } 381 } catch (NameNotFoundException e) { 382 Slog.e(TAG, "Failed to getApplicationIcon() in getMonochromeAppIcon()", e); 383 } 384 if (monochromeIcon != null) { 385 return monochromeIcon; 386 } else { 387 return Icon.createWithResource(mContext, fallbackIconResId); 388 } 389 } 390 391 protected static class NotificationAttributes { 392 public final int flags; 393 public final int iconColor; 394 public final Icon icon; 395 public final int visibility; 396 NotificationAttributes(int flags, Icon icon, int iconColor, int visibility)397 public NotificationAttributes(int flags, Icon icon, int iconColor, int visibility) { 398 this.flags = flags; 399 this.icon = icon; 400 this.iconColor = iconColor; 401 this.visibility = visibility; 402 } 403 NotificationAttributes(@onNull NotificationAttributes attr)404 public NotificationAttributes(@NonNull NotificationAttributes attr) { 405 this.flags = attr.flags; 406 this.icon = attr.icon; 407 this.iconColor = attr.iconColor; 408 this.visibility = attr.visibility; 409 } 410 411 @Override equals(Object o)412 public boolean equals(Object o) { 413 if (this == o) { 414 return true; 415 } 416 if (!(o instanceof NotificationAttributes that)) { 417 return false; 418 } 419 return flags == that.flags && iconColor == that.iconColor && icon.sameAs(that.icon) 420 && visibility == that.visibility; 421 } 422 423 @Override hashCode()424 public int hashCode() { 425 return Objects.hash(flags, iconColor, icon, visibility); 426 } 427 } 428 429 protected interface Callback { addAutoGroup(String key, boolean requestSort)430 void addAutoGroup(String key, boolean requestSort); removeAutoGroup(String key)431 void removeAutoGroup(String key); 432 addAutoGroupSummary(int userId, String pkg, String triggeringKey, NotificationAttributes summaryAttr)433 void addAutoGroupSummary(int userId, String pkg, String triggeringKey, 434 NotificationAttributes summaryAttr); removeAutoGroupSummary(int user, String pkg)435 void removeAutoGroupSummary(int user, String pkg); updateAutogroupSummary(int userId, String pkg, NotificationAttributes summaryAttr)436 void updateAutogroupSummary(int userId, String pkg, NotificationAttributes summaryAttr); 437 } 438 } 439