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