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 Entry(StatusBarNotification n, StatusBarIconView ic)65 public Entry(StatusBarNotification n, StatusBarIconView ic) { 66 this.key = n.getKey(); 67 this.notification = n; 68 this.icon = ic; 69 } 70 setInterruption()71 public void setInterruption() { 72 interruption = true; 73 } 74 hasInterrupted()75 public boolean hasInterrupted() { 76 return interruption; 77 } 78 79 /** 80 * Resets the notification entry to be re-used. 81 */ reset()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 getContentView()93 public View getContentView() { 94 return row.getPrivateLayout().getContractedChild(); 95 } 96 getExpandedContentView()97 public View getExpandedContentView() { 98 return row.getPrivateLayout().getExpandedChild(); 99 } 100 getHeadsUpContentView()101 public View getHeadsUpContentView() { 102 return row.getPrivateLayout().getHeadsUpChild(); 103 } 104 getPublicContentView()105 public View getPublicContentView() { 106 return row.getPublicLayout().getContractedChild(); 107 } 108 cacheContentViews(Context ctx, Notification updatedNotification)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. compareRemoteViews(final RemoteViews a, final RemoteViews b)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 notifyFullScreenIntentLaunched()160 public void notifyFullScreenIntentLaunched() { 161 lastFullScreenIntentLaunchTime = SystemClock.elapsedRealtime(); 162 } 163 hasJustLaunchedFullScreenIntent()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 setHeadsUpManager(HeadsUpManager headsUpManager)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 NotificationData(Environment environment)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 */ getActiveNotifications()251 public ArrayList<Entry> getActiveNotifications() { 252 return mSortedAndFiltered; 253 } 254 get(String key)255 public Entry get(String key) { 256 return mEntries.get(key); 257 } 258 add(Entry entry, RankingMap ranking)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 remove(String key, RankingMap ranking)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 updateRanking(RankingMap ranking)278 public void updateRanking(RankingMap ranking) { 279 updateRankingAndSort(ranking); 280 } 281 isAmbient(String key)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 getVisibilityOverride(String key)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 shouldSuppressScreenOff(String key)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 shouldSuppressScreenOn(String key)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 getImportance(String key)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 getOverrideGroupKey(String key)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 updateRankingAndSort(RankingMap ranking)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. filterAndSort()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 shouldFilterOut(StatusBarNotification sbn)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 // Q: What kinds of notifications should show during setup? 398 // A: Almost none! Only things coming from the system (package is "android") that also 399 // have special "kind" tags marking them as relevant for setup (see below). showNotificationEvenIfUnprovisioned(StatusBarNotification sbn)400 public static boolean showNotificationEvenIfUnprovisioned(StatusBarNotification sbn) { 401 return "android".equals(sbn.getPackageName()) 402 && sbn.getNotification().extras.getBoolean(Notification.EXTRA_ALLOW_DURING_SETUP); 403 } 404 dump(PrintWriter pw, String indent)405 public void dump(PrintWriter pw, String indent) { 406 int N = mSortedAndFiltered.size(); 407 pw.print(indent); 408 pw.println("active notifications: " + N); 409 int active; 410 for (active = 0; active < N; active++) { 411 NotificationData.Entry e = mSortedAndFiltered.get(active); 412 dumpEntry(pw, indent, active, e); 413 } 414 synchronized (mEntries) { 415 int M = mEntries.size(); 416 pw.print(indent); 417 pw.println("inactive notifications: " + (M - active)); 418 int inactiveCount = 0; 419 for (int i = 0; i < M; i++) { 420 Entry entry = mEntries.valueAt(i); 421 if (!mSortedAndFiltered.contains(entry)) { 422 dumpEntry(pw, indent, inactiveCount, entry); 423 inactiveCount++; 424 } 425 } 426 } 427 } 428 dumpEntry(PrintWriter pw, String indent, int i, Entry e)429 private void dumpEntry(PrintWriter pw, String indent, int i, Entry e) { 430 mRankingMap.getRanking(e.key, mTmpRanking); 431 pw.print(indent); 432 pw.println(" [" + i + "] key=" + e.key + " icon=" + e.icon); 433 StatusBarNotification n = e.notification; 434 pw.print(indent); 435 pw.println(" pkg=" + n.getPackageName() + " id=" + n.getId() + " importance=" + 436 mTmpRanking.getImportance()); 437 pw.print(indent); 438 pw.println(" notification=" + n.getNotification()); 439 pw.print(indent); 440 pw.println(" tickerText=\"" + n.getNotification().tickerText + "\""); 441 } 442 isSystemNotification(StatusBarNotification sbn)443 private static boolean isSystemNotification(StatusBarNotification sbn) { 444 String sbnPackage = sbn.getPackageName(); 445 return "android".equals(sbnPackage) || "com.android.systemui".equals(sbnPackage); 446 } 447 448 /** 449 * Provides access to keyguard state and user settings dependent data. 450 */ 451 public interface Environment { onSecureLockScreen()452 public boolean onSecureLockScreen(); shouldHideNotifications(int userid)453 public boolean shouldHideNotifications(int userid); shouldHideNotifications(String key)454 public boolean shouldHideNotifications(String key); isDeviceProvisioned()455 public boolean isDeviceProvisioned(); isNotificationForCurrentProfiles(StatusBarNotification sbn)456 public boolean isNotificationForCurrentProfiles(StatusBarNotification sbn); getCurrentMediaNotificationKey()457 public String getCurrentMediaNotificationKey(); getGroupManager()458 public NotificationGroupManager getGroupManager(); 459 } 460 } 461