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