1 /* 2 * Copyright (C) 2019 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.systemui.statusbar.notification.collection; 18 19 import android.app.Notification; 20 import android.app.NotificationChannel; 21 import android.app.NotificationManager; 22 import android.app.Person; 23 import android.service.notification.NotificationListenerService.Ranking; 24 import android.service.notification.NotificationListenerService.RankingMap; 25 import android.service.notification.SnoozeCriterion; 26 import android.service.notification.StatusBarNotification; 27 import android.util.ArrayMap; 28 29 import com.android.internal.annotations.VisibleForTesting; 30 import com.android.systemui.Dependency; 31 import com.android.systemui.statusbar.NotificationMediaManager; 32 import com.android.systemui.statusbar.notification.NotificationFilter; 33 import com.android.systemui.statusbar.phone.NotificationGroupManager; 34 import com.android.systemui.statusbar.policy.HeadsUpManager; 35 36 import java.io.PrintWriter; 37 import java.util.ArrayList; 38 import java.util.Collections; 39 import java.util.Comparator; 40 import java.util.List; 41 import java.util.Objects; 42 43 /** 44 * The list of currently displaying notifications. 45 */ 46 public class NotificationData { 47 48 private final NotificationFilter mNotificationFilter = Dependency.get(NotificationFilter.class); 49 50 /** 51 * These dependencies are late init-ed 52 */ 53 private KeyguardEnvironment mEnvironment; 54 private NotificationMediaManager mMediaManager; 55 56 private HeadsUpManager mHeadsUpManager; 57 58 private final ArrayMap<String, NotificationEntry> mEntries = new ArrayMap<>(); 59 private final ArrayList<NotificationEntry> mSortedAndFiltered = new ArrayList<>(); 60 61 private final NotificationGroupManager mGroupManager = 62 Dependency.get(NotificationGroupManager.class); 63 64 private RankingMap mRankingMap; 65 private final Ranking mTmpRanking = new Ranking(); 66 setHeadsUpManager(HeadsUpManager headsUpManager)67 public void setHeadsUpManager(HeadsUpManager headsUpManager) { 68 mHeadsUpManager = headsUpManager; 69 } 70 71 @VisibleForTesting 72 protected final Comparator<NotificationEntry> mRankingComparator = 73 new Comparator<NotificationEntry>() { 74 private final Ranking mRankingA = new Ranking(); 75 private final Ranking mRankingB = new Ranking(); 76 77 @Override 78 public int compare(NotificationEntry a, NotificationEntry b) { 79 final StatusBarNotification na = a.notification; 80 final StatusBarNotification nb = b.notification; 81 int aImportance = NotificationManager.IMPORTANCE_DEFAULT; 82 int bImportance = NotificationManager.IMPORTANCE_DEFAULT; 83 int aRank = 0; 84 int bRank = 0; 85 86 if (mRankingMap != null) { 87 // RankingMap as received from NoMan 88 getRanking(a.key, mRankingA); 89 getRanking(b.key, mRankingB); 90 aImportance = mRankingA.getImportance(); 91 bImportance = mRankingB.getImportance(); 92 aRank = mRankingA.getRank(); 93 bRank = mRankingB.getRank(); 94 } 95 96 String mediaNotification = getMediaManager().getMediaNotificationKey(); 97 98 // IMPORTANCE_MIN media streams are allowed to drift to the bottom 99 final boolean aMedia = a.key.equals(mediaNotification) 100 && aImportance > NotificationManager.IMPORTANCE_MIN; 101 final boolean bMedia = b.key.equals(mediaNotification) 102 && bImportance > NotificationManager.IMPORTANCE_MIN; 103 104 boolean aSystemMax = aImportance >= NotificationManager.IMPORTANCE_HIGH 105 && isSystemNotification(na); 106 boolean bSystemMax = bImportance >= NotificationManager.IMPORTANCE_HIGH 107 && isSystemNotification(nb); 108 109 110 boolean aHeadsUp = a.getRow().isHeadsUp(); 111 boolean bHeadsUp = b.getRow().isHeadsUp(); 112 113 // HACK: This should really go elsewhere, but it's currently not straightforward to 114 // extract the comparison code and we're guaranteed to touch every element, so this is 115 // the best place to set the buckets for the moment. 116 a.setIsTopBucket(aHeadsUp || aMedia || aSystemMax || a.isHighPriority()); 117 b.setIsTopBucket(bHeadsUp || bMedia || bSystemMax || b.isHighPriority()); 118 119 if (aHeadsUp != bHeadsUp) { 120 return aHeadsUp ? -1 : 1; 121 } else if (aHeadsUp) { 122 // Provide consistent ranking with headsUpManager 123 return mHeadsUpManager.compare(a, b); 124 } else if (aMedia != bMedia) { 125 // Upsort current media notification. 126 return aMedia ? -1 : 1; 127 } else if (aSystemMax != bSystemMax) { 128 // Upsort PRIORITY_MAX system notifications 129 return aSystemMax ? -1 : 1; 130 } else if (a.isHighPriority() != b.isHighPriority()) { 131 return -1 * Boolean.compare(a.isHighPriority(), b.isHighPriority()); 132 } else if (aRank != bRank) { 133 return aRank - bRank; 134 } else { 135 return Long.compare(nb.getNotification().when, na.getNotification().when); 136 } 137 } 138 }; 139 getEnvironment()140 private KeyguardEnvironment getEnvironment() { 141 if (mEnvironment == null) { 142 mEnvironment = Dependency.get(KeyguardEnvironment.class); 143 } 144 return mEnvironment; 145 } 146 getMediaManager()147 private NotificationMediaManager getMediaManager() { 148 if (mMediaManager == null) { 149 mMediaManager = Dependency.get(NotificationMediaManager.class); 150 } 151 return mMediaManager; 152 } 153 154 /** 155 * Returns the sorted list of active notifications (depending on {@link KeyguardEnvironment} 156 * 157 * <p> 158 * This call doesn't update the list of active notifications. Call {@link #filterAndSort()} 159 * when the environment changes. 160 * <p> 161 * Don't hold on to or modify the returned list. 162 */ getActiveNotifications()163 public ArrayList<NotificationEntry> getActiveNotifications() { 164 return mSortedAndFiltered; 165 } 166 getNotificationsForCurrentUser()167 public ArrayList<NotificationEntry> getNotificationsForCurrentUser() { 168 synchronized (mEntries) { 169 final int len = mEntries.size(); 170 ArrayList<NotificationEntry> filteredForUser = new ArrayList<>(len); 171 172 for (int i = 0; i < len; i++) { 173 NotificationEntry entry = mEntries.valueAt(i); 174 final StatusBarNotification sbn = entry.notification; 175 if (!getEnvironment().isNotificationForCurrentProfiles(sbn)) { 176 continue; 177 } 178 filteredForUser.add(entry); 179 } 180 return filteredForUser; 181 } 182 } 183 get(String key)184 public NotificationEntry get(String key) { 185 return mEntries.get(key); 186 } 187 add(NotificationEntry entry)188 public void add(NotificationEntry entry) { 189 synchronized (mEntries) { 190 mEntries.put(entry.notification.getKey(), entry); 191 } 192 mGroupManager.onEntryAdded(entry); 193 194 updateRankingAndSort(mRankingMap); 195 } 196 remove(String key, RankingMap ranking)197 public NotificationEntry remove(String key, RankingMap ranking) { 198 NotificationEntry removed; 199 synchronized (mEntries) { 200 removed = mEntries.remove(key); 201 } 202 if (removed == null) return null; 203 // NEM may pass us a null ranking map if removing a lifetime-extended notification, 204 // so use the most recent ranking 205 if (ranking == null) ranking = mRankingMap; 206 mGroupManager.onEntryRemoved(removed); 207 updateRankingAndSort(ranking); 208 return removed; 209 } 210 211 /** Updates the given notification entry with the provided ranking. */ update( NotificationEntry entry, RankingMap ranking, StatusBarNotification notification)212 public void update( 213 NotificationEntry entry, 214 RankingMap ranking, 215 StatusBarNotification notification) { 216 updateRanking(ranking); 217 final StatusBarNotification oldNotification = entry.notification; 218 entry.notification = notification; 219 mGroupManager.onEntryUpdated(entry, oldNotification); 220 } 221 updateRanking(RankingMap ranking)222 public void updateRanking(RankingMap ranking) { 223 updateRankingAndSort(ranking); 224 } 225 updateAppOp(int appOp, int uid, String pkg, String key, boolean showIcon)226 public void updateAppOp(int appOp, int uid, String pkg, String key, boolean showIcon) { 227 synchronized (mEntries) { 228 final int len = mEntries.size(); 229 for (int i = 0; i < len; i++) { 230 NotificationEntry entry = mEntries.valueAt(i); 231 if (uid == entry.notification.getUid() 232 && pkg.equals(entry.notification.getPackageName()) 233 && key.equals(entry.key)) { 234 if (showIcon) { 235 entry.mActiveAppOps.add(appOp); 236 } else { 237 entry.mActiveAppOps.remove(appOp); 238 } 239 } 240 } 241 } 242 } 243 244 /** 245 * Returns true if this notification should be displayed in the high-priority notifications 246 * section 247 */ isHighPriority(StatusBarNotification statusBarNotification)248 public boolean isHighPriority(StatusBarNotification statusBarNotification) { 249 if (mRankingMap != null) { 250 getRanking(statusBarNotification.getKey(), mTmpRanking); 251 if (mTmpRanking.getImportance() >= NotificationManager.IMPORTANCE_DEFAULT 252 || hasHighPriorityCharacteristics( 253 mTmpRanking.getChannel(), statusBarNotification)) { 254 return true; 255 } 256 if (mGroupManager.isSummaryOfGroup(statusBarNotification)) { 257 final ArrayList<NotificationEntry> logicalChildren = 258 mGroupManager.getLogicalChildren(statusBarNotification); 259 for (NotificationEntry child : logicalChildren) { 260 if (isHighPriority(child.notification)) { 261 return true; 262 } 263 } 264 } 265 } 266 return false; 267 } 268 hasHighPriorityCharacteristics(NotificationChannel channel, StatusBarNotification statusBarNotification)269 private boolean hasHighPriorityCharacteristics(NotificationChannel channel, 270 StatusBarNotification statusBarNotification) { 271 272 if (isImportantOngoing(statusBarNotification.getNotification()) 273 || statusBarNotification.getNotification().hasMediaSession() 274 || hasPerson(statusBarNotification.getNotification()) 275 || hasStyle(statusBarNotification.getNotification(), 276 Notification.MessagingStyle.class)) { 277 // Users who have long pressed and demoted to silent should not see the notification 278 // in the top section 279 if (channel != null && channel.hasUserSetImportance()) { 280 return false; 281 } 282 return true; 283 } 284 285 return false; 286 } 287 isImportantOngoing(Notification notification)288 private boolean isImportantOngoing(Notification notification) { 289 return notification.isForegroundService() 290 && mTmpRanking.getImportance() >= NotificationManager.IMPORTANCE_LOW; 291 } 292 hasStyle(Notification notification, Class targetStyle)293 private boolean hasStyle(Notification notification, Class targetStyle) { 294 Class<? extends Notification.Style> style = notification.getNotificationStyle(); 295 return targetStyle.equals(style); 296 } 297 hasPerson(Notification notification)298 private boolean hasPerson(Notification notification) { 299 // TODO: cache favorite and recent contacts to check contact affinity 300 ArrayList<Person> people = notification.extras != null 301 ? notification.extras.getParcelableArrayList(Notification.EXTRA_PEOPLE_LIST) 302 : new ArrayList<>(); 303 return people != null && !people.isEmpty(); 304 } 305 isAmbient(String key)306 public boolean isAmbient(String key) { 307 if (mRankingMap != null) { 308 getRanking(key, mTmpRanking); 309 return mTmpRanking.isAmbient(); 310 } 311 return false; 312 } 313 getVisibilityOverride(String key)314 public int getVisibilityOverride(String key) { 315 if (mRankingMap != null) { 316 getRanking(key, mTmpRanking); 317 return mTmpRanking.getVisibilityOverride(); 318 } 319 return Ranking.VISIBILITY_NO_OVERRIDE; 320 } 321 getImportance(String key)322 public int getImportance(String key) { 323 if (mRankingMap != null) { 324 getRanking(key, mTmpRanking); 325 return mTmpRanking.getImportance(); 326 } 327 return NotificationManager.IMPORTANCE_UNSPECIFIED; 328 } 329 getOverrideGroupKey(String key)330 public String getOverrideGroupKey(String key) { 331 if (mRankingMap != null) { 332 getRanking(key, mTmpRanking); 333 return mTmpRanking.getOverrideGroupKey(); 334 } 335 return null; 336 } 337 getSnoozeCriteria(String key)338 public List<SnoozeCriterion> getSnoozeCriteria(String key) { 339 if (mRankingMap != null) { 340 getRanking(key, mTmpRanking); 341 return mTmpRanking.getSnoozeCriteria(); 342 } 343 return null; 344 } 345 getChannel(String key)346 public NotificationChannel getChannel(String key) { 347 if (mRankingMap != null) { 348 getRanking(key, mTmpRanking); 349 return mTmpRanking.getChannel(); 350 } 351 return null; 352 } 353 getRank(String key)354 public int getRank(String key) { 355 if (mRankingMap != null) { 356 getRanking(key, mTmpRanking); 357 return mTmpRanking.getRank(); 358 } 359 return 0; 360 } 361 shouldHide(String key)362 public boolean shouldHide(String key) { 363 if (mRankingMap != null) { 364 getRanking(key, mTmpRanking); 365 return mTmpRanking.isSuspended(); 366 } 367 return false; 368 } 369 updateRankingAndSort(RankingMap ranking)370 private void updateRankingAndSort(RankingMap ranking) { 371 if (ranking != null) { 372 mRankingMap = ranking; 373 synchronized (mEntries) { 374 final int len = mEntries.size(); 375 for (int i = 0; i < len; i++) { 376 NotificationEntry entry = mEntries.valueAt(i); 377 if (!getRanking(entry.key, mTmpRanking)) { 378 continue; 379 } 380 final StatusBarNotification oldSbn = entry.notification.cloneLight(); 381 final String overrideGroupKey = getOverrideGroupKey(entry.key); 382 if (!Objects.equals(oldSbn.getOverrideGroupKey(), overrideGroupKey)) { 383 entry.notification.setOverrideGroupKey(overrideGroupKey); 384 mGroupManager.onEntryUpdated(entry, oldSbn); 385 } 386 entry.populateFromRanking(mTmpRanking); 387 entry.setIsHighPriority(isHighPriority(entry.notification)); 388 } 389 } 390 } 391 filterAndSort(); 392 } 393 394 /** 395 * Get the ranking from the current ranking map. 396 * 397 * @param key the key to look up 398 * @param outRanking the ranking to populate 399 * 400 * @return {@code true} if the ranking was properly obtained. 401 */ 402 @VisibleForTesting getRanking(String key, Ranking outRanking)403 protected boolean getRanking(String key, Ranking outRanking) { 404 return mRankingMap.getRanking(key, outRanking); 405 } 406 407 // TODO: This should not be public. Instead the Environment should notify this class when 408 // anything changed, and this class should call back the UI so it updates itself. filterAndSort()409 public void filterAndSort() { 410 mSortedAndFiltered.clear(); 411 412 synchronized (mEntries) { 413 final int len = mEntries.size(); 414 for (int i = 0; i < len; i++) { 415 NotificationEntry entry = mEntries.valueAt(i); 416 417 if (mNotificationFilter.shouldFilterOut(entry)) { 418 continue; 419 } 420 421 mSortedAndFiltered.add(entry); 422 } 423 } 424 425 if (mSortedAndFiltered.size() == 1) { 426 // HACK: We need the comparator to run on all children in order to set the 427 // isHighPriority field. If there is only one child, then the comparison won't be run, 428 // so we have to trigger it manually. Get rid of this code as soon as possible. 429 mRankingComparator.compare(mSortedAndFiltered.get(0), mSortedAndFiltered.get(0)); 430 } else { 431 Collections.sort(mSortedAndFiltered, mRankingComparator); 432 } 433 } 434 dump(PrintWriter pw, String indent)435 public void dump(PrintWriter pw, String indent) { 436 int filteredLen = mSortedAndFiltered.size(); 437 pw.print(indent); 438 pw.println("active notifications: " + filteredLen); 439 int active; 440 for (active = 0; active < filteredLen; active++) { 441 NotificationEntry e = mSortedAndFiltered.get(active); 442 dumpEntry(pw, indent, active, e); 443 } 444 synchronized (mEntries) { 445 int totalLen = mEntries.size(); 446 pw.print(indent); 447 pw.println("inactive notifications: " + (totalLen - active)); 448 int inactiveCount = 0; 449 for (int i = 0; i < totalLen; i++) { 450 NotificationEntry entry = mEntries.valueAt(i); 451 if (!mSortedAndFiltered.contains(entry)) { 452 dumpEntry(pw, indent, inactiveCount, entry); 453 inactiveCount++; 454 } 455 } 456 } 457 } 458 dumpEntry(PrintWriter pw, String indent, int i, NotificationEntry e)459 private void dumpEntry(PrintWriter pw, String indent, int i, NotificationEntry e) { 460 getRanking(e.key, mTmpRanking); 461 pw.print(indent); 462 pw.println(" [" + i + "] key=" + e.key + " icon=" + e.icon); 463 StatusBarNotification n = e.notification; 464 pw.print(indent); 465 pw.println(" pkg=" + n.getPackageName() + " id=" + n.getId() + " importance=" 466 + mTmpRanking.getImportance()); 467 pw.print(indent); 468 pw.println(" notification=" + n.getNotification()); 469 } 470 isSystemNotification(StatusBarNotification sbn)471 private static boolean isSystemNotification(StatusBarNotification sbn) { 472 String sbnPackage = sbn.getPackageName(); 473 return "android".equals(sbnPackage) || "com.android.systemui".equals(sbnPackage); 474 } 475 476 /** 477 * Provides access to keyguard state and user settings dependent data. 478 */ 479 public interface KeyguardEnvironment { isDeviceProvisioned()480 boolean isDeviceProvisioned(); isNotificationForCurrentProfiles(StatusBarNotification sbn)481 boolean isNotificationForCurrentProfiles(StatusBarNotification sbn); 482 } 483 } 484