1 /* 2 * Copyright (C) 2014 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 package com.android.server.notification; 17 18 import static android.service.notification.NotificationListenerService.Ranking.IMPORTANCE_MIN; 19 import static android.service.notification.NotificationListenerService.Ranking.IMPORTANCE_UNSPECIFIED; 20 import static android.service.notification.NotificationListenerService.Ranking.IMPORTANCE_DEFAULT; 21 import static android.service.notification.NotificationListenerService.Ranking.IMPORTANCE_HIGH; 22 import static android.service.notification.NotificationListenerService.Ranking.IMPORTANCE_LOW; 23 import static android.service.notification.NotificationListenerService.Ranking.IMPORTANCE_MAX; 24 25 import android.app.Notification; 26 import android.content.Context; 27 import android.content.pm.PackageManager.NameNotFoundException; 28 import android.content.res.Resources; 29 import android.graphics.Bitmap; 30 import android.graphics.drawable.Icon; 31 import android.media.AudioAttributes; 32 import android.os.UserHandle; 33 import android.service.notification.NotificationListenerService; 34 import android.service.notification.StatusBarNotification; 35 import android.util.Log; 36 37 import com.android.internal.annotations.VisibleForTesting; 38 import com.android.server.EventLogTags; 39 40 import java.io.PrintWriter; 41 import java.lang.reflect.Array; 42 import java.util.Arrays; 43 import java.util.Objects; 44 45 /** 46 * Holds data about notifications that should not be shared with the 47 * {@link android.service.notification.NotificationListenerService}s. 48 * 49 * <p>These objects should not be mutated unless the code is synchronized 50 * on {@link NotificationManagerService#mNotificationList}, and any 51 * modification should be followed by a sorting of that list.</p> 52 * 53 * <p>Is sortable by {@link NotificationComparator}.</p> 54 * 55 * {@hide} 56 */ 57 public final class NotificationRecord { 58 static final String TAG = "NotificationRecord"; 59 static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); 60 final StatusBarNotification sbn; 61 final int mOriginalFlags; 62 private final Context mContext; 63 64 NotificationUsageStats.SingleNotificationStats stats; 65 boolean isCanceled; 66 /** Whether the notification was seen by the user via one of the notification listeners. */ 67 boolean mIsSeen; 68 69 // These members are used by NotificationSignalExtractors 70 // to communicate with the ranking module. 71 private float mContactAffinity; 72 private boolean mRecentlyIntrusive; 73 74 // is this notification currently being intercepted by Zen Mode? 75 private boolean mIntercept; 76 77 // The timestamp used for ranking. 78 private long mRankingTimeMs; 79 80 // The first post time, stable across updates. 81 private long mCreationTimeMs; 82 83 // The most recent visibility event. 84 private long mVisibleSinceMs; 85 86 // The most recent update time, or the creation time if no updates. 87 private long mUpdateTimeMs; 88 89 // Is this record an update of an old record? 90 public boolean isUpdate; 91 private int mPackagePriority; 92 93 private int mAuthoritativeRank; 94 private String mGlobalSortKey; 95 private int mPackageVisibility; 96 private int mUserImportance = IMPORTANCE_UNSPECIFIED; 97 private int mImportance = IMPORTANCE_UNSPECIFIED; 98 private CharSequence mImportanceExplanation = null; 99 100 private int mSuppressedVisualEffects = 0; 101 private String mUserExplanation; 102 private String mPeopleExplanation; 103 104 @VisibleForTesting 105 public NotificationRecord(Context context, StatusBarNotification sbn) 106 { 107 this.sbn = sbn; 108 mOriginalFlags = sbn.getNotification().flags; 109 mRankingTimeMs = calculateRankingTimeMs(0L); 110 mCreationTimeMs = sbn.getPostTime(); 111 mUpdateTimeMs = mCreationTimeMs; 112 mContext = context; 113 stats = new NotificationUsageStats.SingleNotificationStats(); 114 mImportance = defaultImportance(); 115 } 116 117 private int defaultImportance() { 118 final Notification n = sbn.getNotification(); 119 int importance = IMPORTANCE_DEFAULT; 120 121 // Migrate notification flags to scores 122 if (0 != (n.flags & Notification.FLAG_HIGH_PRIORITY)) { 123 n.priority = Notification.PRIORITY_MAX; 124 } 125 126 switch (n.priority) { 127 case Notification.PRIORITY_MIN: 128 importance = IMPORTANCE_MIN; 129 break; 130 case Notification.PRIORITY_LOW: 131 importance = IMPORTANCE_LOW; 132 break; 133 case Notification.PRIORITY_DEFAULT: 134 importance = IMPORTANCE_DEFAULT; 135 break; 136 case Notification.PRIORITY_HIGH: 137 importance = IMPORTANCE_HIGH; 138 break; 139 case Notification.PRIORITY_MAX: 140 importance = IMPORTANCE_MAX; 141 break; 142 } 143 stats.requestedImportance = importance; 144 145 boolean isNoisy = (n.defaults & Notification.DEFAULT_SOUND) != 0 146 || (n.defaults & Notification.DEFAULT_VIBRATE) != 0 147 || n.sound != null 148 || n.vibrate != null; 149 stats.isNoisy = isNoisy; 150 151 if (!isNoisy && importance > IMPORTANCE_LOW) { 152 importance = IMPORTANCE_LOW; 153 } 154 155 if (isNoisy) { 156 if (importance < IMPORTANCE_DEFAULT) { 157 importance = IMPORTANCE_DEFAULT; 158 } 159 } 160 161 if (n.fullScreenIntent != null) { 162 importance = IMPORTANCE_MAX; 163 } 164 165 stats.naturalImportance = importance; 166 return importance; 167 } 168 169 // copy any notes that the ranking system may have made before the update 170 public void copyRankingInformation(NotificationRecord previous) { 171 mContactAffinity = previous.mContactAffinity; 172 mRecentlyIntrusive = previous.mRecentlyIntrusive; 173 mPackagePriority = previous.mPackagePriority; 174 mPackageVisibility = previous.mPackageVisibility; 175 mIntercept = previous.mIntercept; 176 mRankingTimeMs = calculateRankingTimeMs(previous.getRankingTimeMs()); 177 mCreationTimeMs = previous.mCreationTimeMs; 178 mVisibleSinceMs = previous.mVisibleSinceMs; 179 if (previous.sbn.getOverrideGroupKey() != null && !sbn.isAppGroup()) { 180 sbn.setOverrideGroupKey(previous.sbn.getOverrideGroupKey()); 181 } 182 // Don't copy importance information or mGlobalSortKey, recompute them. 183 } 184 185 public Notification getNotification() { return sbn.getNotification(); } 186 public int getFlags() { return sbn.getNotification().flags; } 187 public UserHandle getUser() { return sbn.getUser(); } 188 public String getKey() { return sbn.getKey(); } 189 /** @deprecated Use {@link #getUser()} instead. */ 190 public int getUserId() { return sbn.getUserId(); } 191 192 void dump(PrintWriter pw, String prefix, Context baseContext, boolean redact) { 193 final Notification notification = sbn.getNotification(); 194 final Icon icon = notification.getSmallIcon(); 195 String iconStr = String.valueOf(icon); 196 if (icon != null && icon.getType() == Icon.TYPE_RESOURCE) { 197 iconStr += " / " + idDebugString(baseContext, icon.getResPackage(), icon.getResId()); 198 } 199 pw.println(prefix + this); 200 pw.println(prefix + " uid=" + sbn.getUid() + " userId=" + sbn.getUserId()); 201 pw.println(prefix + " icon=" + iconStr); 202 pw.println(prefix + " pri=" + notification.priority); 203 pw.println(prefix + " key=" + sbn.getKey()); 204 pw.println(prefix + " seen=" + mIsSeen); 205 pw.println(prefix + " groupKey=" + getGroupKey()); 206 pw.println(prefix + " contentIntent=" + notification.contentIntent); 207 pw.println(prefix + " deleteIntent=" + notification.deleteIntent); 208 pw.println(prefix + " tickerText=" + notification.tickerText); 209 pw.println(prefix + " contentView=" + notification.contentView); 210 pw.println(prefix + String.format(" defaults=0x%08x flags=0x%08x", 211 notification.defaults, notification.flags)); 212 pw.println(prefix + " sound=" + notification.sound); 213 pw.println(prefix + " audioStreamType=" + notification.audioStreamType); 214 pw.println(prefix + " audioAttributes=" + notification.audioAttributes); 215 pw.println(prefix + String.format(" color=0x%08x", notification.color)); 216 pw.println(prefix + " vibrate=" + Arrays.toString(notification.vibrate)); 217 pw.println(prefix + String.format(" led=0x%08x onMs=%d offMs=%d", 218 notification.ledARGB, notification.ledOnMS, notification.ledOffMS)); 219 if (notification.actions != null && notification.actions.length > 0) { 220 pw.println(prefix + " actions={"); 221 final int N = notification.actions.length; 222 for (int i=0; i<N; i++) { 223 final Notification.Action action = notification.actions[i]; 224 if (action != null) { 225 pw.println(String.format("%s [%d] \"%s\" -> %s", 226 prefix, 227 i, 228 action.title, 229 action.actionIntent == null ? "null" : action.actionIntent.toString() 230 )); 231 } 232 } 233 pw.println(prefix + " }"); 234 } 235 if (notification.extras != null && notification.extras.size() > 0) { 236 pw.println(prefix + " extras={"); 237 for (String key : notification.extras.keySet()) { 238 pw.print(prefix + " " + key + "="); 239 Object val = notification.extras.get(key); 240 if (val == null) { 241 pw.println("null"); 242 } else { 243 pw.print(val.getClass().getSimpleName()); 244 if (redact && (val instanceof CharSequence || val instanceof String)) { 245 // redact contents from bugreports 246 } else if (val instanceof Bitmap) { 247 pw.print(String.format(" (%dx%d)", 248 ((Bitmap) val).getWidth(), 249 ((Bitmap) val).getHeight())); 250 } else if (val.getClass().isArray()) { 251 final int N = Array.getLength(val); 252 pw.print(" (" + N + ")"); 253 if (!redact) { 254 for (int j=0; j<N; j++) { 255 pw.println(); 256 pw.print(String.format("%s [%d] %s", 257 prefix, j, String.valueOf(Array.get(val, j)))); 258 } 259 } 260 } else { 261 pw.print(" (" + String.valueOf(val) + ")"); 262 } 263 pw.println(); 264 } 265 } 266 pw.println(prefix + " }"); 267 } 268 pw.println(prefix + " stats=" + stats.toString()); 269 pw.println(prefix + " mContactAffinity=" + mContactAffinity); 270 pw.println(prefix + " mRecentlyIntrusive=" + mRecentlyIntrusive); 271 pw.println(prefix + " mPackagePriority=" + mPackagePriority); 272 pw.println(prefix + " mPackageVisibility=" + mPackageVisibility); 273 pw.println(prefix + " mUserImportance=" 274 + NotificationListenerService.Ranking.importanceToString(mUserImportance)); 275 pw.println(prefix + " mImportance=" 276 + NotificationListenerService.Ranking.importanceToString(mImportance)); 277 pw.println(prefix + " mImportanceExplanation=" + mImportanceExplanation); 278 pw.println(prefix + " mIntercept=" + mIntercept); 279 pw.println(prefix + " mGlobalSortKey=" + mGlobalSortKey); 280 pw.println(prefix + " mRankingTimeMs=" + mRankingTimeMs); 281 pw.println(prefix + " mCreationTimeMs=" + mCreationTimeMs); 282 pw.println(prefix + " mVisibleSinceMs=" + mVisibleSinceMs); 283 pw.println(prefix + " mUpdateTimeMs=" + mUpdateTimeMs); 284 pw.println(prefix + " mSuppressedVisualEffects= " + mSuppressedVisualEffects); 285 } 286 287 288 static String idDebugString(Context baseContext, String packageName, int id) { 289 Context c; 290 291 if (packageName != null) { 292 try { 293 c = baseContext.createPackageContext(packageName, 0); 294 } catch (NameNotFoundException e) { 295 c = baseContext; 296 } 297 } else { 298 c = baseContext; 299 } 300 301 Resources r = c.getResources(); 302 try { 303 return r.getResourceName(id); 304 } catch (Resources.NotFoundException e) { 305 return "<name unknown>"; 306 } 307 } 308 309 @Override 310 public final String toString() { 311 return String.format( 312 "NotificationRecord(0x%08x: pkg=%s user=%s id=%d tag=%s importance=%d key=%s: %s)", 313 System.identityHashCode(this), 314 this.sbn.getPackageName(), this.sbn.getUser(), this.sbn.getId(), 315 this.sbn.getTag(), this.mImportance, this.sbn.getKey(), 316 this.sbn.getNotification()); 317 } 318 319 public void setContactAffinity(float contactAffinity) { 320 mContactAffinity = contactAffinity; 321 if (mImportance < IMPORTANCE_DEFAULT && 322 mContactAffinity > ValidateNotificationPeople.VALID_CONTACT) { 323 setImportance(IMPORTANCE_DEFAULT, getPeopleExplanation()); 324 } 325 } 326 327 public float getContactAffinity() { 328 return mContactAffinity; 329 } 330 331 public void setRecentlyIntrusive(boolean recentlyIntrusive) { 332 mRecentlyIntrusive = recentlyIntrusive; 333 } 334 335 public boolean isRecentlyIntrusive() { 336 return mRecentlyIntrusive; 337 } 338 339 public void setPackagePriority(int packagePriority) { 340 mPackagePriority = packagePriority; 341 } 342 343 public int getPackagePriority() { 344 return mPackagePriority; 345 } 346 347 public void setPackageVisibilityOverride(int packageVisibility) { 348 mPackageVisibility = packageVisibility; 349 } 350 351 public int getPackageVisibilityOverride() { 352 return mPackageVisibility; 353 } 354 355 public void setUserImportance(int importance) { 356 mUserImportance = importance; 357 applyUserImportance(); 358 } 359 360 private String getUserExplanation() { 361 if (mUserExplanation == null) { 362 mUserExplanation = 363 mContext.getString(com.android.internal.R.string.importance_from_user); 364 } 365 return mUserExplanation; 366 } 367 368 private String getPeopleExplanation() { 369 if (mPeopleExplanation == null) { 370 mPeopleExplanation = 371 mContext.getString(com.android.internal.R.string.importance_from_person); 372 } 373 return mPeopleExplanation; 374 } 375 376 private void applyUserImportance() { 377 if (mUserImportance != NotificationListenerService.Ranking.IMPORTANCE_UNSPECIFIED) { 378 mImportance = mUserImportance; 379 mImportanceExplanation = getUserExplanation(); 380 } 381 } 382 383 public int getUserImportance() { 384 return mUserImportance; 385 } 386 387 public void setImportance(int importance, CharSequence explanation) { 388 if (importance != NotificationListenerService.Ranking.IMPORTANCE_UNSPECIFIED) { 389 mImportance = importance; 390 mImportanceExplanation = explanation; 391 } 392 applyUserImportance(); 393 } 394 395 public int getImportance() { 396 return mImportance; 397 } 398 399 public CharSequence getImportanceExplanation() { 400 return mImportanceExplanation; 401 } 402 403 public boolean setIntercepted(boolean intercept) { 404 mIntercept = intercept; 405 return mIntercept; 406 } 407 408 public boolean isIntercepted() { 409 return mIntercept; 410 } 411 412 public void setSuppressedVisualEffects(int effects) { 413 mSuppressedVisualEffects = effects; 414 } 415 416 public int getSuppressedVisualEffects() { 417 return mSuppressedVisualEffects; 418 } 419 420 public boolean isCategory(String category) { 421 return Objects.equals(getNotification().category, category); 422 } 423 424 public boolean isAudioStream(int stream) { 425 return getNotification().audioStreamType == stream; 426 } 427 428 public boolean isAudioAttributesUsage(int usage) { 429 final AudioAttributes attributes = getNotification().audioAttributes; 430 return attributes != null && attributes.getUsage() == usage; 431 } 432 433 /** 434 * Returns the timestamp to use for time-based sorting in the ranker. 435 */ 436 public long getRankingTimeMs() { 437 return mRankingTimeMs; 438 } 439 440 /** 441 * @param now this current time in milliseconds. 442 * @returns the number of milliseconds since the most recent update, or the post time if none. 443 */ 444 public int getFreshnessMs(long now) { 445 return (int) (now - mUpdateTimeMs); 446 } 447 448 /** 449 * @param now this current time in milliseconds. 450 * @returns the number of milliseconds since the the first post, ignoring updates. 451 */ 452 public int getLifespanMs(long now) { 453 return (int) (now - mCreationTimeMs); 454 } 455 456 /** 457 * @param now this current time in milliseconds. 458 * @returns the number of milliseconds since the most recent visibility event, or 0 if never. 459 */ 460 public int getExposureMs(long now) { 461 return mVisibleSinceMs == 0 ? 0 : (int) (now - mVisibleSinceMs); 462 } 463 464 /** 465 * Set the visibility of the notification. 466 */ 467 public void setVisibility(boolean visible, int rank) { 468 final long now = System.currentTimeMillis(); 469 mVisibleSinceMs = visible ? now : mVisibleSinceMs; 470 stats.onVisibilityChanged(visible); 471 EventLogTags.writeNotificationVisibility(getKey(), visible ? 1 : 0, 472 (int) (now - mCreationTimeMs), 473 (int) (now - mUpdateTimeMs), 474 0, // exposure time 475 rank); 476 } 477 478 /** 479 * @param previousRankingTimeMs for updated notifications, {@link #getRankingTimeMs()} 480 * of the previous notification record, 0 otherwise 481 */ 482 private long calculateRankingTimeMs(long previousRankingTimeMs) { 483 Notification n = getNotification(); 484 // Take developer provided 'when', unless it's in the future. 485 if (n.when != 0 && n.when <= sbn.getPostTime()) { 486 return n.when; 487 } 488 // If we've ranked a previous instance with a timestamp, inherit it. This case is 489 // important in order to have ranking stability for updating notifications. 490 if (previousRankingTimeMs > 0) { 491 return previousRankingTimeMs; 492 } 493 return sbn.getPostTime(); 494 } 495 496 public void setGlobalSortKey(String globalSortKey) { 497 mGlobalSortKey = globalSortKey; 498 } 499 500 public String getGlobalSortKey() { 501 return mGlobalSortKey; 502 } 503 504 /** Check if any of the listeners have marked this notification as seen by the user. */ 505 public boolean isSeen() { 506 return mIsSeen; 507 } 508 509 /** Mark the notification as seen by the user. */ 510 public void setSeen() { 511 mIsSeen = true; 512 } 513 514 public void setAuthoritativeRank(int authoritativeRank) { 515 mAuthoritativeRank = authoritativeRank; 516 } 517 518 public int getAuthoritativeRank() { 519 return mAuthoritativeRank; 520 } 521 522 public String getGroupKey() { 523 return sbn.getGroupKey(); 524 } 525 526 public boolean isImportanceFromUser() { 527 return mImportance == mUserImportance; 528 } 529 } 530