1 /* 2 * Copyright (C) 2008 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; 18 19 import android.app.AppGlobals; 20 import android.app.Notification; 21 import android.app.NotificationChannel; 22 import android.app.NotificationManager; 23 import android.content.pm.IPackageManager; 24 import android.content.pm.PackageManager; 25 import android.content.Context; 26 import android.graphics.drawable.Icon; 27 import android.os.AsyncTask; 28 import android.os.Bundle; 29 import android.os.RemoteException; 30 import android.os.SystemClock; 31 import android.service.notification.NotificationListenerService; 32 import android.service.notification.NotificationListenerService.Ranking; 33 import android.service.notification.NotificationListenerService.RankingMap; 34 import android.service.notification.SnoozeCriterion; 35 import android.service.notification.StatusBarNotification; 36 import android.util.ArrayMap; 37 import android.view.View; 38 import android.widget.ImageView; 39 import android.widget.RemoteViews; 40 import android.Manifest; 41 42 import com.android.internal.annotations.VisibleForTesting; 43 import com.android.internal.messages.nano.SystemMessageProto; 44 import com.android.internal.statusbar.StatusBarIcon; 45 import com.android.internal.util.NotificationColorUtil; 46 import com.android.systemui.Dependency; 47 import com.android.systemui.ForegroundServiceController; 48 import com.android.systemui.statusbar.notification.InflationException; 49 import com.android.systemui.statusbar.phone.NotificationGroupManager; 50 import com.android.systemui.statusbar.phone.StatusBar; 51 import com.android.systemui.statusbar.policy.HeadsUpManager; 52 53 import java.io.PrintWriter; 54 import java.util.ArrayList; 55 import java.util.Collections; 56 import java.util.Comparator; 57 import java.util.List; 58 import java.util.Objects; 59 60 /** 61 * The list of currently displaying notifications. 62 */ 63 public class NotificationData { 64 65 private final Environment mEnvironment; 66 private HeadsUpManager mHeadsUpManager; 67 68 public static final class Entry { 69 private static final long LAUNCH_COOLDOWN = 2000; 70 private static final long NOT_LAUNCHED_YET = -LAUNCH_COOLDOWN; 71 private static final int COLOR_INVALID = 1; 72 public String key; 73 public StatusBarNotification notification; 74 public NotificationChannel channel; 75 public StatusBarIconView icon; 76 public StatusBarIconView expandedIcon; 77 public ExpandableNotificationRow row; // the outer expanded view 78 private boolean interruption; 79 public boolean autoRedacted; // whether the redacted notification was generated by us 80 public int targetSdk; 81 private long lastFullScreenIntentLaunchTime = NOT_LAUNCHED_YET; 82 public RemoteViews cachedContentView; 83 public RemoteViews cachedBigContentView; 84 public RemoteViews cachedHeadsUpContentView; 85 public RemoteViews cachedPublicContentView; 86 public RemoteViews cachedAmbientContentView; 87 public CharSequence remoteInputText; 88 public List<SnoozeCriterion> snoozeCriteria; 89 private int mCachedContrastColor = COLOR_INVALID; 90 private int mCachedContrastColorIsFor = COLOR_INVALID; 91 private InflationTask mRunningTask = null; 92 private Throwable mDebugThrowable; 93 94 public Entry(StatusBarNotification n) { 95 this.key = n.getKey(); 96 this.notification = n; 97 } 98 99 public void setInterruption() { 100 interruption = true; 101 } 102 103 public boolean hasInterrupted() { 104 return interruption; 105 } 106 107 /** 108 * Resets the notification entry to be re-used. 109 */ 110 public void reset() { 111 if (row != null) { 112 row.reset(); 113 } 114 } 115 116 public View getExpandedContentView() { 117 return row.getPrivateLayout().getExpandedChild(); 118 } 119 120 public View getPublicContentView() { 121 return row.getPublicLayout().getContractedChild(); 122 } 123 124 public void notifyFullScreenIntentLaunched() { 125 setInterruption(); 126 lastFullScreenIntentLaunchTime = SystemClock.elapsedRealtime(); 127 } 128 129 public boolean hasJustLaunchedFullScreenIntent() { 130 return SystemClock.elapsedRealtime() < lastFullScreenIntentLaunchTime + LAUNCH_COOLDOWN; 131 } 132 133 /** 134 * Create the icons for a notification 135 * @param context the context to create the icons with 136 * @param sbn the notification 137 * @throws InflationException 138 */ 139 public void createIcons(Context context, StatusBarNotification sbn) 140 throws InflationException { 141 Notification n = sbn.getNotification(); 142 final Icon smallIcon = n.getSmallIcon(); 143 if (smallIcon == null) { 144 throw new InflationException("No small icon in notification from " 145 + sbn.getPackageName()); 146 } 147 148 // Construct the icon. 149 icon = new StatusBarIconView(context, 150 sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn); 151 icon.setScaleType(ImageView.ScaleType.CENTER_INSIDE); 152 153 // Construct the expanded icon. 154 expandedIcon = new StatusBarIconView(context, 155 sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn); 156 expandedIcon.setScaleType(ImageView.ScaleType.CENTER_INSIDE); 157 final StatusBarIcon ic = new StatusBarIcon( 158 sbn.getUser(), 159 sbn.getPackageName(), 160 smallIcon, 161 n.iconLevel, 162 n.number, 163 StatusBarIconView.contentDescForNotification(context, n)); 164 if (!icon.set(ic) || !expandedIcon.set(ic)) { 165 icon = null; 166 expandedIcon = null; 167 throw new InflationException("Couldn't create icon: " + ic); 168 } 169 expandedIcon.setVisibility(View.INVISIBLE); 170 expandedIcon.setOnVisibilityChangedListener( 171 newVisibility -> { 172 if (row != null) { 173 row.setIconsVisible(newVisibility != View.VISIBLE); 174 } 175 }); 176 } 177 178 public void setIconTag(int key, Object tag) { 179 if (icon != null) { 180 icon.setTag(key, tag); 181 expandedIcon.setTag(key, tag); 182 } 183 } 184 185 /** 186 * Update the notification icons. 187 * @param context the context to create the icons with. 188 * @param n the notification to read the icon from. 189 * @throws InflationException 190 */ 191 public void updateIcons(Context context, StatusBarNotification sbn) 192 throws InflationException { 193 if (icon != null) { 194 // Update the icon 195 Notification n = sbn.getNotification(); 196 final StatusBarIcon ic = new StatusBarIcon( 197 notification.getUser(), 198 notification.getPackageName(), 199 n.getSmallIcon(), 200 n.iconLevel, 201 n.number, 202 StatusBarIconView.contentDescForNotification(context, n)); 203 icon.setNotification(sbn); 204 expandedIcon.setNotification(sbn); 205 if (!icon.set(ic) || !expandedIcon.set(ic)) { 206 throw new InflationException("Couldn't update icon: " + ic); 207 } 208 } 209 } 210 211 public int getContrastedColor(Context context, boolean isLowPriority, 212 int backgroundColor) { 213 int rawColor = isLowPriority ? Notification.COLOR_DEFAULT : 214 notification.getNotification().color; 215 if (mCachedContrastColorIsFor == rawColor && mCachedContrastColor != COLOR_INVALID) { 216 return mCachedContrastColor; 217 } 218 final int contrasted = NotificationColorUtil.resolveContrastColor(context, rawColor, 219 backgroundColor); 220 mCachedContrastColorIsFor = rawColor; 221 mCachedContrastColor = contrasted; 222 return mCachedContrastColor; 223 } 224 225 /** 226 * Abort all existing inflation tasks 227 */ 228 public void abortTask() { 229 if (mRunningTask != null) { 230 mRunningTask.abort(); 231 mRunningTask = null; 232 } 233 } 234 235 public void setInflationTask(InflationTask abortableTask) { 236 // abort any existing inflation 237 InflationTask existing = mRunningTask; 238 abortTask(); 239 mRunningTask = abortableTask; 240 if (existing != null && mRunningTask != null) { 241 mRunningTask.supersedeTask(existing); 242 } 243 } 244 245 public void onInflationTaskFinished() { 246 mRunningTask = null; 247 } 248 249 @VisibleForTesting 250 public InflationTask getRunningTask() { 251 return mRunningTask; 252 } 253 254 /** 255 * Set a throwable that is used for debugging 256 * 257 * @param debugThrowable the throwable to save 258 */ 259 public void setDebugThrowable(Throwable debugThrowable) { 260 mDebugThrowable = debugThrowable; 261 } 262 263 public Throwable getDebugThrowable() { 264 return mDebugThrowable; 265 } 266 } 267 268 private final ArrayMap<String, Entry> mEntries = new ArrayMap<>(); 269 private final ArrayList<Entry> mSortedAndFiltered = new ArrayList<>(); 270 271 private NotificationGroupManager mGroupManager; 272 273 private RankingMap mRankingMap; 274 private final Ranking mTmpRanking = new Ranking(); 275 276 public void setHeadsUpManager(HeadsUpManager headsUpManager) { 277 mHeadsUpManager = headsUpManager; 278 } 279 280 private final Comparator<Entry> mRankingComparator = new Comparator<Entry>() { 281 private final Ranking mRankingA = new Ranking(); 282 private final Ranking mRankingB = new Ranking(); 283 284 @Override 285 public int compare(Entry a, Entry b) { 286 final StatusBarNotification na = a.notification; 287 final StatusBarNotification nb = b.notification; 288 int aImportance = NotificationManager.IMPORTANCE_DEFAULT; 289 int bImportance = NotificationManager.IMPORTANCE_DEFAULT; 290 int aRank = 0; 291 int bRank = 0; 292 293 if (mRankingMap != null) { 294 // RankingMap as received from NoMan 295 getRanking(a.key, mRankingA); 296 getRanking(b.key, mRankingB); 297 aImportance = mRankingA.getImportance(); 298 bImportance = mRankingB.getImportance(); 299 aRank = mRankingA.getRank(); 300 bRank = mRankingB.getRank(); 301 } 302 303 String mediaNotification = mEnvironment.getCurrentMediaNotificationKey(); 304 305 // IMPORTANCE_MIN media streams are allowed to drift to the bottom 306 final boolean aMedia = a.key.equals(mediaNotification) 307 && aImportance > NotificationManager.IMPORTANCE_MIN; 308 final boolean bMedia = b.key.equals(mediaNotification) 309 && bImportance > NotificationManager.IMPORTANCE_MIN; 310 311 boolean aSystemMax = aImportance >= NotificationManager.IMPORTANCE_HIGH && 312 isSystemNotification(na); 313 boolean bSystemMax = bImportance >= NotificationManager.IMPORTANCE_HIGH && 314 isSystemNotification(nb); 315 316 boolean isHeadsUp = a.row.isHeadsUp(); 317 if (isHeadsUp != b.row.isHeadsUp()) { 318 return isHeadsUp ? -1 : 1; 319 } else if (isHeadsUp) { 320 // Provide consistent ranking with headsUpManager 321 return mHeadsUpManager.compare(a, b); 322 } else if (aMedia != bMedia) { 323 // Upsort current media notification. 324 return aMedia ? -1 : 1; 325 } else if (aSystemMax != bSystemMax) { 326 // Upsort PRIORITY_MAX system notifications 327 return aSystemMax ? -1 : 1; 328 } else if (aRank != bRank) { 329 return aRank - bRank; 330 } else { 331 return Long.compare(nb.getNotification().when, na.getNotification().when); 332 } 333 } 334 }; 335 336 public NotificationData(Environment environment) { 337 mEnvironment = environment; 338 mGroupManager = environment.getGroupManager(); 339 } 340 341 /** 342 * Returns the sorted list of active notifications (depending on {@link Environment} 343 * 344 * <p> 345 * This call doesn't update the list of active notifications. Call {@link #filterAndSort()} 346 * when the environment changes. 347 * <p> 348 * Don't hold on to or modify the returned list. 349 */ 350 public ArrayList<Entry> getActiveNotifications() { 351 return mSortedAndFiltered; 352 } 353 354 public Entry get(String key) { 355 return mEntries.get(key); 356 } 357 358 public void add(Entry entry) { 359 synchronized (mEntries) { 360 mEntries.put(entry.notification.getKey(), entry); 361 } 362 mGroupManager.onEntryAdded(entry); 363 364 updateRankingAndSort(mRankingMap); 365 } 366 367 public Entry remove(String key, RankingMap ranking) { 368 Entry removed = null; 369 synchronized (mEntries) { 370 removed = mEntries.remove(key); 371 } 372 if (removed == null) return null; 373 mGroupManager.onEntryRemoved(removed); 374 updateRankingAndSort(ranking); 375 return removed; 376 } 377 378 public void updateRanking(RankingMap ranking) { 379 updateRankingAndSort(ranking); 380 } 381 382 public boolean isAmbient(String key) { 383 if (mRankingMap != null) { 384 getRanking(key, mTmpRanking); 385 return mTmpRanking.isAmbient(); 386 } 387 return false; 388 } 389 390 public int getVisibilityOverride(String key) { 391 if (mRankingMap != null) { 392 getRanking(key, mTmpRanking); 393 return mTmpRanking.getVisibilityOverride(); 394 } 395 return Ranking.VISIBILITY_NO_OVERRIDE; 396 } 397 398 public boolean shouldSuppressScreenOff(String key) { 399 if (mRankingMap != null) { 400 getRanking(key, mTmpRanking); 401 return (mTmpRanking.getSuppressedVisualEffects() 402 & NotificationListenerService.SUPPRESSED_EFFECT_SCREEN_OFF) != 0; 403 } 404 return false; 405 } 406 407 public boolean shouldSuppressScreenOn(String key) { 408 if (mRankingMap != null) { 409 getRanking(key, mTmpRanking); 410 return (mTmpRanking.getSuppressedVisualEffects() 411 & NotificationListenerService.SUPPRESSED_EFFECT_SCREEN_ON) != 0; 412 } 413 return false; 414 } 415 416 public int getImportance(String key) { 417 if (mRankingMap != null) { 418 getRanking(key, mTmpRanking); 419 return mTmpRanking.getImportance(); 420 } 421 return NotificationManager.IMPORTANCE_UNSPECIFIED; 422 } 423 424 public String getOverrideGroupKey(String key) { 425 if (mRankingMap != null) { 426 getRanking(key, mTmpRanking); 427 return mTmpRanking.getOverrideGroupKey(); 428 } 429 return null; 430 } 431 432 public List<SnoozeCriterion> getSnoozeCriteria(String key) { 433 if (mRankingMap != null) { 434 getRanking(key, mTmpRanking); 435 return mTmpRanking.getSnoozeCriteria(); 436 } 437 return null; 438 } 439 440 public NotificationChannel getChannel(String key) { 441 if (mRankingMap != null) { 442 getRanking(key, mTmpRanking); 443 return mTmpRanking.getChannel(); 444 } 445 return null; 446 } 447 448 private void updateRankingAndSort(RankingMap ranking) { 449 if (ranking != null) { 450 mRankingMap = ranking; 451 synchronized (mEntries) { 452 final int N = mEntries.size(); 453 for (int i = 0; i < N; i++) { 454 Entry entry = mEntries.valueAt(i); 455 if (!getRanking(entry.key, mTmpRanking)) { 456 continue; 457 } 458 final StatusBarNotification oldSbn = entry.notification.cloneLight(); 459 final String overrideGroupKey = getOverrideGroupKey(entry.key); 460 if (!Objects.equals(oldSbn.getOverrideGroupKey(), overrideGroupKey)) { 461 entry.notification.setOverrideGroupKey(overrideGroupKey); 462 mGroupManager.onEntryUpdated(entry, oldSbn); 463 } 464 entry.channel = getChannel(entry.key); 465 entry.snoozeCriteria = getSnoozeCriteria(entry.key); 466 } 467 } 468 } 469 filterAndSort(); 470 } 471 472 /** 473 * Get the ranking from the current ranking map. 474 * 475 * @param key the key to look up 476 * @param outRanking the ranking to populate 477 * 478 * @return {@code true} if the ranking was properly obtained. 479 */ 480 @VisibleForTesting 481 protected boolean getRanking(String key, Ranking outRanking) { 482 return mRankingMap.getRanking(key, outRanking); 483 } 484 485 // TODO: This should not be public. Instead the Environment should notify this class when 486 // anything changed, and this class should call back the UI so it updates itself. 487 public void filterAndSort() { 488 mSortedAndFiltered.clear(); 489 490 synchronized (mEntries) { 491 final int N = mEntries.size(); 492 for (int i = 0; i < N; i++) { 493 Entry entry = mEntries.valueAt(i); 494 StatusBarNotification sbn = entry.notification; 495 496 if (shouldFilterOut(sbn)) { 497 continue; 498 } 499 500 mSortedAndFiltered.add(entry); 501 } 502 } 503 504 Collections.sort(mSortedAndFiltered, mRankingComparator); 505 } 506 507 /** 508 * @param sbn 509 * @return true if this notification should NOT be shown right now 510 */ 511 public boolean shouldFilterOut(StatusBarNotification sbn) { 512 if (!(mEnvironment.isDeviceProvisioned() || 513 showNotificationEvenIfUnprovisioned(sbn))) { 514 return true; 515 } 516 517 if (!mEnvironment.isNotificationForCurrentProfiles(sbn)) { 518 return true; 519 } 520 521 if (mEnvironment.isSecurelyLocked(sbn.getUserId()) && 522 (sbn.getNotification().visibility == Notification.VISIBILITY_SECRET 523 || mEnvironment.shouldHideNotifications(sbn.getUserId()) 524 || mEnvironment.shouldHideNotifications(sbn.getKey()))) { 525 return true; 526 } 527 528 if (!StatusBar.ENABLE_CHILD_NOTIFICATIONS 529 && mGroupManager.isChildInGroupWithSummary(sbn)) { 530 return true; 531 } 532 533 final ForegroundServiceController fsc = Dependency.get(ForegroundServiceController.class); 534 if (fsc.isDungeonNotification(sbn) && !fsc.isDungeonNeededForUser(sbn.getUserId())) { 535 // this is a foreground-service disclosure for a user that does not need to show one 536 return true; 537 } 538 539 return false; 540 } 541 542 // Q: What kinds of notifications should show during setup? 543 // A: Almost none! Only things coming from packages with permission 544 // android.permission.NOTIFICATION_DURING_SETUP that also have special "kind" tags marking them 545 // as relevant for setup (see below). 546 public static boolean showNotificationEvenIfUnprovisioned(StatusBarNotification sbn) { 547 return showNotificationEvenIfUnprovisioned(AppGlobals.getPackageManager(), sbn); 548 } 549 550 @VisibleForTesting 551 static boolean showNotificationEvenIfUnprovisioned(IPackageManager packageManager, 552 StatusBarNotification sbn) { 553 return checkUidPermission(packageManager, Manifest.permission.NOTIFICATION_DURING_SETUP, 554 sbn.getUid()) == PackageManager.PERMISSION_GRANTED 555 && sbn.getNotification().extras.getBoolean(Notification.EXTRA_ALLOW_DURING_SETUP); 556 } 557 558 private static int checkUidPermission(IPackageManager packageManager, String permission, 559 int uid) { 560 try { 561 return packageManager.checkUidPermission(permission, uid); 562 } catch (RemoteException e) { 563 throw e.rethrowFromSystemServer(); 564 } 565 } 566 567 public void dump(PrintWriter pw, String indent) { 568 int N = mSortedAndFiltered.size(); 569 pw.print(indent); 570 pw.println("active notifications: " + N); 571 int active; 572 for (active = 0; active < N; active++) { 573 NotificationData.Entry e = mSortedAndFiltered.get(active); 574 dumpEntry(pw, indent, active, e); 575 } 576 synchronized (mEntries) { 577 int M = mEntries.size(); 578 pw.print(indent); 579 pw.println("inactive notifications: " + (M - active)); 580 int inactiveCount = 0; 581 for (int i = 0; i < M; i++) { 582 Entry entry = mEntries.valueAt(i); 583 if (!mSortedAndFiltered.contains(entry)) { 584 dumpEntry(pw, indent, inactiveCount, entry); 585 inactiveCount++; 586 } 587 } 588 } 589 } 590 591 private void dumpEntry(PrintWriter pw, String indent, int i, Entry e) { 592 getRanking(e.key, mTmpRanking); 593 pw.print(indent); 594 pw.println(" [" + i + "] key=" + e.key + " icon=" + e.icon); 595 StatusBarNotification n = e.notification; 596 pw.print(indent); 597 pw.println(" pkg=" + n.getPackageName() + " id=" + n.getId() + " importance=" + 598 mTmpRanking.getImportance()); 599 pw.print(indent); 600 pw.println(" notification=" + n.getNotification()); 601 } 602 603 private static boolean isSystemNotification(StatusBarNotification sbn) { 604 String sbnPackage = sbn.getPackageName(); 605 return "android".equals(sbnPackage) || "com.android.systemui".equals(sbnPackage); 606 } 607 608 /** 609 * Provides access to keyguard state and user settings dependent data. 610 */ 611 public interface Environment { 612 public boolean isSecurelyLocked(int userId); 613 public boolean shouldHideNotifications(int userid); 614 public boolean shouldHideNotifications(String key); 615 public boolean isDeviceProvisioned(); 616 public boolean isNotificationForCurrentProfiles(StatusBarNotification sbn); 617 public String getCurrentMediaNotificationKey(); 618 public NotificationGroupManager getGroupManager(); 619 } 620 } 621