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 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