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.Notification; 20 import android.content.Context; 21 import android.os.SystemClock; 22 import android.service.notification.NotificationListenerService; 23 import android.service.notification.NotificationListenerService.Ranking; 24 import android.service.notification.NotificationListenerService.RankingMap; 25 import android.service.notification.StatusBarNotification; 26 import android.util.ArrayMap; 27 import android.view.View; 28 import android.widget.RemoteViews; 29 30 import com.android.systemui.statusbar.phone.NotificationGroupManager; 31 import com.android.systemui.statusbar.policy.HeadsUpManager; 32 33 import java.io.PrintWriter; 34 import java.util.ArrayList; 35 import java.util.Collections; 36 import java.util.Comparator; 37 import java.util.Objects; 38 39 /** 40 * The list of currently displaying notifications. 41 */ 42 public class NotificationData { 43 44 private final Environment mEnvironment; 45 private HeadsUpManager mHeadsUpManager; 46 47 public static final class Entry { 48 private static final long LAUNCH_COOLDOWN = 2000; 49 private static final long NOT_LAUNCHED_YET = -LAUNCH_COOLDOWN; 50 public String key; 51 public StatusBarNotification notification; 52 public StatusBarIconView icon; 53 public ExpandableNotificationRow row; // the outer expanded view 54 private boolean interruption; 55 public boolean autoRedacted; // whether the redacted notification was generated by us 56 public boolean legacy; // whether the notification has a legacy, dark background 57 public int targetSdk; 58 private long lastFullScreenIntentLaunchTime = NOT_LAUNCHED_YET; 59 public RemoteViews cachedContentView; 60 public RemoteViews cachedBigContentView; 61 public RemoteViews cachedHeadsUpContentView; 62 public RemoteViews cachedPublicContentView; 63 public CharSequence remoteInputText; 64 65 public Entry(StatusBarNotification n, StatusBarIconView ic) { 66 this.key = n.getKey(); 67 this.notification = n; 68 this.icon = ic; 69 } 70 71 public void setInterruption() { 72 interruption = true; 73 } 74 75 public boolean hasInterrupted() { 76 return interruption; 77 } 78 79 /** 80 * Resets the notification entry to be re-used. 81 */ 82 public void reset() { 83 // NOTE: Icon needs to be preserved for now. 84 // We should fix this at some point. 85 autoRedacted = false; 86 legacy = false; 87 lastFullScreenIntentLaunchTime = NOT_LAUNCHED_YET; 88 if (row != null) { 89 row.reset(); 90 } 91 } 92 93 public View getContentView() { 94 return row.getPrivateLayout().getContractedChild(); 95 } 96 97 public View getExpandedContentView() { 98 return row.getPrivateLayout().getExpandedChild(); 99 } 100 101 public View getHeadsUpContentView() { 102 return row.getPrivateLayout().getHeadsUpChild(); 103 } 104 105 public View getPublicContentView() { 106 return row.getPublicLayout().getContractedChild(); 107 } 108 109 public boolean cacheContentViews(Context ctx, Notification updatedNotification) { 110 boolean applyInPlace = false; 111 if (updatedNotification != null) { 112 final Notification.Builder updatedNotificationBuilder 113 = Notification.Builder.recoverBuilder(ctx, updatedNotification); 114 final RemoteViews newContentView = updatedNotificationBuilder.createContentView(); 115 final RemoteViews newBigContentView = 116 updatedNotificationBuilder.createBigContentView(); 117 final RemoteViews newHeadsUpContentView = 118 updatedNotificationBuilder.createHeadsUpContentView(); 119 final RemoteViews newPublicNotification 120 = updatedNotificationBuilder.makePublicContentView(); 121 122 boolean sameCustomView = Objects.equals( 123 notification.getNotification().extras.getBoolean( 124 Notification.EXTRA_CONTAINS_CUSTOM_VIEW), 125 updatedNotification.extras.getBoolean( 126 Notification.EXTRA_CONTAINS_CUSTOM_VIEW)); 127 applyInPlace = compareRemoteViews(cachedContentView, newContentView) 128 && compareRemoteViews(cachedBigContentView, newBigContentView) 129 && compareRemoteViews(cachedHeadsUpContentView, newHeadsUpContentView) 130 && compareRemoteViews(cachedPublicContentView, newPublicNotification) 131 && sameCustomView; 132 cachedPublicContentView = newPublicNotification; 133 cachedHeadsUpContentView = newHeadsUpContentView; 134 cachedBigContentView = newBigContentView; 135 cachedContentView = newContentView; 136 } else { 137 final Notification.Builder builder 138 = Notification.Builder.recoverBuilder(ctx, notification.getNotification()); 139 140 cachedContentView = builder.createContentView(); 141 cachedBigContentView = builder.createBigContentView(); 142 cachedHeadsUpContentView = builder.createHeadsUpContentView(); 143 cachedPublicContentView = builder.makePublicContentView(); 144 145 applyInPlace = false; 146 } 147 return applyInPlace; 148 } 149 150 // Returns true if the RemoteViews are the same. 151 private boolean compareRemoteViews(final RemoteViews a, final RemoteViews b) { 152 return (a == null && b == null) || 153 (a != null && b != null 154 && b.getPackage() != null 155 && a.getPackage() != null 156 && a.getPackage().equals(b.getPackage()) 157 && a.getLayoutId() == b.getLayoutId()); 158 } 159 160 public void notifyFullScreenIntentLaunched() { 161 lastFullScreenIntentLaunchTime = SystemClock.elapsedRealtime(); 162 } 163 164 public boolean hasJustLaunchedFullScreenIntent() { 165 return SystemClock.elapsedRealtime() < lastFullScreenIntentLaunchTime + LAUNCH_COOLDOWN; 166 } 167 } 168 169 private final ArrayMap<String, Entry> mEntries = new ArrayMap<>(); 170 private final ArrayList<Entry> mSortedAndFiltered = new ArrayList<>(); 171 172 private NotificationGroupManager mGroupManager; 173 174 private RankingMap mRankingMap; 175 private final Ranking mTmpRanking = new Ranking(); 176 177 public void setHeadsUpManager(HeadsUpManager headsUpManager) { 178 mHeadsUpManager = headsUpManager; 179 } 180 181 private final Comparator<Entry> mRankingComparator = new Comparator<Entry>() { 182 private final Ranking mRankingA = new Ranking(); 183 private final Ranking mRankingB = new Ranking(); 184 185 @Override 186 public int compare(Entry a, Entry b) { 187 final StatusBarNotification na = a.notification; 188 final StatusBarNotification nb = b.notification; 189 int aImportance = Ranking.IMPORTANCE_DEFAULT; 190 int bImportance = Ranking.IMPORTANCE_DEFAULT; 191 int aRank = 0; 192 int bRank = 0; 193 194 if (mRankingMap != null) { 195 // RankingMap as received from NoMan 196 mRankingMap.getRanking(a.key, mRankingA); 197 mRankingMap.getRanking(b.key, mRankingB); 198 aImportance = mRankingA.getImportance(); 199 bImportance = mRankingB.getImportance(); 200 aRank = mRankingA.getRank(); 201 bRank = mRankingB.getRank(); 202 } 203 204 String mediaNotification = mEnvironment.getCurrentMediaNotificationKey(); 205 206 // IMPORTANCE_MIN media streams are allowed to drift to the bottom 207 final boolean aMedia = a.key.equals(mediaNotification) 208 && aImportance > Ranking.IMPORTANCE_MIN; 209 final boolean bMedia = b.key.equals(mediaNotification) 210 && bImportance > Ranking.IMPORTANCE_MIN; 211 212 boolean aSystemMax = aImportance >= Ranking.IMPORTANCE_MAX && 213 isSystemNotification(na); 214 boolean bSystemMax = bImportance >= Ranking.IMPORTANCE_MAX && 215 isSystemNotification(nb); 216 217 boolean isHeadsUp = a.row.isHeadsUp(); 218 if (isHeadsUp != b.row.isHeadsUp()) { 219 return isHeadsUp ? -1 : 1; 220 } else if (isHeadsUp) { 221 // Provide consistent ranking with headsUpManager 222 return mHeadsUpManager.compare(a, b); 223 } else if (aMedia != bMedia) { 224 // Upsort current media notification. 225 return aMedia ? -1 : 1; 226 } else if (aSystemMax != bSystemMax) { 227 // Upsort PRIORITY_MAX system notifications 228 return aSystemMax ? -1 : 1; 229 } else if (aRank != bRank) { 230 return aRank - bRank; 231 } else { 232 return (int) (nb.getNotification().when - na.getNotification().when); 233 } 234 } 235 }; 236 237 public NotificationData(Environment environment) { 238 mEnvironment = environment; 239 mGroupManager = environment.getGroupManager(); 240 } 241 242 /** 243 * Returns the sorted list of active notifications (depending on {@link Environment} 244 * 245 * <p> 246 * This call doesn't update the list of active notifications. Call {@link #filterAndSort()} 247 * when the environment changes. 248 * <p> 249 * Don't hold on to or modify the returned list. 250 */ 251 public ArrayList<Entry> getActiveNotifications() { 252 return mSortedAndFiltered; 253 } 254 255 public Entry get(String key) { 256 return mEntries.get(key); 257 } 258 259 public void add(Entry entry, RankingMap ranking) { 260 synchronized (mEntries) { 261 mEntries.put(entry.notification.getKey(), entry); 262 } 263 mGroupManager.onEntryAdded(entry); 264 updateRankingAndSort(ranking); 265 } 266 267 public Entry remove(String key, RankingMap ranking) { 268 Entry removed = null; 269 synchronized (mEntries) { 270 removed = mEntries.remove(key); 271 } 272 if (removed == null) return null; 273 mGroupManager.onEntryRemoved(removed); 274 updateRankingAndSort(ranking); 275 return removed; 276 } 277 278 public void updateRanking(RankingMap ranking) { 279 updateRankingAndSort(ranking); 280 } 281 282 public boolean isAmbient(String key) { 283 if (mRankingMap != null) { 284 mRankingMap.getRanking(key, mTmpRanking); 285 return mTmpRanking.isAmbient(); 286 } 287 return false; 288 } 289 290 public int getVisibilityOverride(String key) { 291 if (mRankingMap != null) { 292 mRankingMap.getRanking(key, mTmpRanking); 293 return mTmpRanking.getVisibilityOverride(); 294 } 295 return Ranking.VISIBILITY_NO_OVERRIDE; 296 } 297 298 public boolean shouldSuppressScreenOff(String key) { 299 if (mRankingMap != null) { 300 mRankingMap.getRanking(key, mTmpRanking); 301 return (mTmpRanking.getSuppressedVisualEffects() 302 & NotificationListenerService.SUPPRESSED_EFFECT_SCREEN_OFF) != 0; 303 } 304 return false; 305 } 306 307 public boolean shouldSuppressScreenOn(String key) { 308 if (mRankingMap != null) { 309 mRankingMap.getRanking(key, mTmpRanking); 310 return (mTmpRanking.getSuppressedVisualEffects() 311 & NotificationListenerService.SUPPRESSED_EFFECT_SCREEN_ON) != 0; 312 } 313 return false; 314 } 315 316 public int getImportance(String key) { 317 if (mRankingMap != null) { 318 mRankingMap.getRanking(key, mTmpRanking); 319 return mTmpRanking.getImportance(); 320 } 321 return Ranking.IMPORTANCE_UNSPECIFIED; 322 } 323 324 public String getOverrideGroupKey(String key) { 325 if (mRankingMap != null) { 326 mRankingMap.getRanking(key, mTmpRanking); 327 return mTmpRanking.getOverrideGroupKey(); 328 } 329 return null; 330 } 331 332 private void updateRankingAndSort(RankingMap ranking) { 333 if (ranking != null) { 334 mRankingMap = ranking; 335 synchronized (mEntries) { 336 final int N = mEntries.size(); 337 for (int i = 0; i < N; i++) { 338 Entry entry = mEntries.valueAt(i); 339 final StatusBarNotification oldSbn = entry.notification.clone(); 340 final String overrideGroupKey = getOverrideGroupKey(entry.key); 341 if (!Objects.equals(oldSbn.getOverrideGroupKey(), overrideGroupKey)) { 342 entry.notification.setOverrideGroupKey(overrideGroupKey); 343 mGroupManager.onEntryUpdated(entry, oldSbn); 344 } 345 } 346 } 347 } 348 filterAndSort(); 349 } 350 351 // TODO: This should not be public. Instead the Environment should notify this class when 352 // anything changed, and this class should call back the UI so it updates itself. 353 public void filterAndSort() { 354 mSortedAndFiltered.clear(); 355 356 synchronized (mEntries) { 357 final int N = mEntries.size(); 358 for (int i = 0; i < N; i++) { 359 Entry entry = mEntries.valueAt(i); 360 StatusBarNotification sbn = entry.notification; 361 362 if (shouldFilterOut(sbn)) { 363 continue; 364 } 365 366 mSortedAndFiltered.add(entry); 367 } 368 } 369 370 Collections.sort(mSortedAndFiltered, mRankingComparator); 371 } 372 373 boolean shouldFilterOut(StatusBarNotification sbn) { 374 if (!(mEnvironment.isDeviceProvisioned() || 375 showNotificationEvenIfUnprovisioned(sbn))) { 376 return true; 377 } 378 379 if (!mEnvironment.isNotificationForCurrentProfiles(sbn)) { 380 return true; 381 } 382 383 if (mEnvironment.onSecureLockScreen() && 384 (sbn.getNotification().visibility == Notification.VISIBILITY_SECRET 385 || mEnvironment.shouldHideNotifications(sbn.getUserId()) 386 || mEnvironment.shouldHideNotifications(sbn.getKey()))) { 387 return true; 388 } 389 390 if (!BaseStatusBar.ENABLE_CHILD_NOTIFICATIONS 391 && mGroupManager.isChildInGroupWithSummary(sbn)) { 392 return true; 393 } 394 return false; 395 } 396 397 /** 398 * Return whether there are any clearable notifications (that aren't errors). 399 */ 400 public boolean hasActiveClearableNotifications() { 401 for (Entry e : mSortedAndFiltered) { 402 if (e.getContentView() != null) { // the view successfully inflated 403 if (e.notification.isClearable()) { 404 return true; 405 } 406 } 407 } 408 return false; 409 } 410 411 // Q: What kinds of notifications should show during setup? 412 // A: Almost none! Only things coming from the system (package is "android") that also 413 // have special "kind" tags marking them as relevant for setup (see below). 414 public static boolean showNotificationEvenIfUnprovisioned(StatusBarNotification sbn) { 415 return "android".equals(sbn.getPackageName()) 416 && sbn.getNotification().extras.getBoolean(Notification.EXTRA_ALLOW_DURING_SETUP); 417 } 418 419 public void dump(PrintWriter pw, String indent) { 420 int N = mSortedAndFiltered.size(); 421 pw.print(indent); 422 pw.println("active notifications: " + N); 423 int active; 424 for (active = 0; active < N; active++) { 425 NotificationData.Entry e = mSortedAndFiltered.get(active); 426 dumpEntry(pw, indent, active, e); 427 } 428 synchronized (mEntries) { 429 int M = mEntries.size(); 430 pw.print(indent); 431 pw.println("inactive notifications: " + (M - active)); 432 int inactiveCount = 0; 433 for (int i = 0; i < M; i++) { 434 Entry entry = mEntries.valueAt(i); 435 if (!mSortedAndFiltered.contains(entry)) { 436 dumpEntry(pw, indent, inactiveCount, entry); 437 inactiveCount++; 438 } 439 } 440 } 441 } 442 443 private void dumpEntry(PrintWriter pw, String indent, int i, Entry e) { 444 mRankingMap.getRanking(e.key, mTmpRanking); 445 pw.print(indent); 446 pw.println(" [" + i + "] key=" + e.key + " icon=" + e.icon); 447 StatusBarNotification n = e.notification; 448 pw.print(indent); 449 pw.println(" pkg=" + n.getPackageName() + " id=" + n.getId() + " importance=" + 450 mTmpRanking.getImportance()); 451 pw.print(indent); 452 pw.println(" notification=" + n.getNotification()); 453 pw.print(indent); 454 pw.println(" tickerText=\"" + n.getNotification().tickerText + "\""); 455 } 456 457 private static boolean isSystemNotification(StatusBarNotification sbn) { 458 String sbnPackage = sbn.getPackageName(); 459 return "android".equals(sbnPackage) || "com.android.systemui".equals(sbnPackage); 460 } 461 462 /** 463 * Provides access to keyguard state and user settings dependent data. 464 */ 465 public interface Environment { 466 public boolean onSecureLockScreen(); 467 public boolean shouldHideNotifications(int userid); 468 public boolean shouldHideNotifications(String key); 469 public boolean isDeviceProvisioned(); 470 public boolean isNotificationForCurrentProfiles(StatusBarNotification sbn); 471 public String getCurrentMediaNotificationKey(); 472 public NotificationGroupManager getGroupManager(); 473 } 474 } 475