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