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 android.service.notification; 18 19 import static android.text.TextUtils.formatSimple; 20 21 import static com.android.window.flags.Flags.enablePerDisplayPackageContextCacheInStatusbarNotif; 22 23 import android.annotation.NonNull; 24 import android.app.Notification; 25 import android.app.NotificationManager; 26 import android.app.Person; 27 import android.compat.annotation.UnsupportedAppUsage; 28 import android.content.Context; 29 import android.content.pm.ApplicationInfo; 30 import android.content.pm.PackageManager; 31 import android.metrics.LogMaker; 32 import android.os.Build; 33 import android.os.Parcel; 34 import android.os.Parcelable; 35 import android.os.Trace; 36 import android.os.UserHandle; 37 import android.util.ArrayMap; 38 39 import com.android.internal.logging.InstanceId; 40 import com.android.internal.logging.nano.MetricsProto; 41 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 42 43 import java.util.ArrayList; 44 import java.util.Collections; 45 import java.util.Map; 46 47 /** 48 * Class encapsulating a Notification. Sent by the NotificationManagerService to clients including 49 * the status bar and any {@link android.service.notification.NotificationListenerService}s. 50 */ 51 public class StatusBarNotification implements Parcelable { 52 static final int MAX_LOG_TAG_LENGTH = 36; 53 54 @UnsupportedAppUsage 55 private final String pkg; 56 @UnsupportedAppUsage 57 private final int id; 58 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 59 private final String tag; 60 private final String key; 61 private String groupKey; 62 private String overrideGroupKey; 63 64 @UnsupportedAppUsage 65 private final int uid; 66 private final String opPkg; 67 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 68 private final int initialPid; 69 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 70 private final Notification notification; 71 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 72 private final UserHandle user; 73 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 74 private final long postTime; 75 // A small per-notification ID, used for statsd logging. 76 private InstanceId mInstanceId; // Not final, see setInstanceId() 77 78 /** 79 * @deprecated This field is only used when 80 * {@link enablePerDisplayPackageContextCacheInStatusbarNotif} 81 * is disabled. 82 */ 83 @Deprecated 84 private Context mContext; // used for inflation & icon expansion 85 // Maps display id to context used for remote view content inflation and status bar icon. 86 private final Map<Integer, Context> mContextForDisplayId = 87 Collections.synchronizedMap(new ArrayMap<>()); 88 89 /** @hide */ StatusBarNotification(String pkg, String opPkg, int id, String tag, int uid, int initialPid, Notification notification, UserHandle user, String overrideGroupKey, long postTime)90 public StatusBarNotification(String pkg, String opPkg, int id, 91 String tag, int uid, int initialPid, Notification notification, UserHandle user, 92 String overrideGroupKey, long postTime) { 93 if (pkg == null) throw new NullPointerException(); 94 if (notification == null) throw new NullPointerException(); 95 96 this.pkg = pkg; 97 this.opPkg = opPkg; 98 this.id = id; 99 this.tag = tag; 100 this.uid = uid; 101 this.initialPid = initialPid; 102 this.notification = notification; 103 this.user = user; 104 this.postTime = postTime; 105 this.overrideGroupKey = overrideGroupKey; 106 this.key = key(); 107 this.groupKey = groupKey(); 108 } 109 110 /** 111 * @deprecated Non-system apps should not need to create StatusBarNotifications. 112 */ 113 @Deprecated StatusBarNotification(String pkg, String opPkg, int id, String tag, int uid, int initialPid, int score, Notification notification, UserHandle user, long postTime)114 public StatusBarNotification(String pkg, String opPkg, int id, String tag, int uid, 115 int initialPid, int score, Notification notification, UserHandle user, 116 long postTime) { 117 if (pkg == null) throw new NullPointerException(); 118 if (notification == null) throw new NullPointerException(); 119 120 this.pkg = pkg; 121 this.opPkg = opPkg; 122 this.id = id; 123 this.tag = tag; 124 this.uid = uid; 125 this.initialPid = initialPid; 126 this.notification = notification; 127 this.user = user; 128 this.postTime = postTime; 129 this.key = key(); 130 this.groupKey = groupKey(); 131 } 132 StatusBarNotification(Parcel in)133 public StatusBarNotification(Parcel in) { 134 this.pkg = in.readString(); 135 this.opPkg = in.readString(); 136 this.id = in.readInt(); 137 if (in.readInt() != 0) { 138 this.tag = in.readString(); 139 } else { 140 this.tag = null; 141 } 142 this.uid = in.readInt(); 143 this.initialPid = in.readInt(); 144 this.notification = new Notification(in); 145 this.user = UserHandle.readFromParcel(in); 146 this.postTime = in.readLong(); 147 if (in.readInt() != 0) { 148 this.overrideGroupKey = in.readString(); 149 } 150 if (in.readInt() != 0) { 151 this.mInstanceId = InstanceId.CREATOR.createFromParcel(in); 152 } 153 this.key = key(); 154 this.groupKey = groupKey(); 155 } 156 157 /** 158 * @hide 159 */ getUidFromKey(@onNull String key)160 public static int getUidFromKey(@NonNull String key) { 161 String[] parts = key.split("\\|"); 162 if (parts.length >= 5) { 163 try { 164 int uid = Integer.parseInt(parts[4]); 165 return uid; 166 } catch (NumberFormatException e) { 167 return -1; 168 } 169 } 170 return -1; 171 } 172 173 /** 174 * @hide 175 */ getPkgFromKey(@onNull String key)176 public static String getPkgFromKey(@NonNull String key) { 177 String[] parts = key.split("\\|"); 178 if (parts.length >= 2) { 179 return parts[1]; 180 } 181 return null; 182 } 183 key()184 private String key() { 185 String sbnKey = user.getIdentifier() + "|" + pkg + "|" + id + "|" + tag + "|" + uid; 186 if (overrideGroupKey != null && getNotification().isGroupSummary()) { 187 sbnKey = sbnKey + "|" + overrideGroupKey; 188 } 189 return sbnKey; 190 } 191 groupKey()192 private String groupKey() { 193 if (overrideGroupKey != null) { 194 if (Flags.notificationForceGrouping()) { 195 return overrideGroupKey; 196 } else { 197 return user.getIdentifier() + "|" + pkg + "|" + "g:" + overrideGroupKey; 198 } 199 } 200 final String group = getNotification().getGroup(); 201 final String sortKey = getNotification().getSortKey(); 202 if (group == null && sortKey == null) { 203 // a group of one 204 return key; 205 } 206 return user.getIdentifier() + "|" + pkg + "|" + 207 (group == null 208 ? "c:" + notification.getChannelId() 209 : "g:" + group); 210 } 211 212 /** 213 * Returns true if this notification is part of a group. 214 */ isGroup()215 public boolean isGroup() { 216 if (overrideGroupKey != null || isAppGroup()) { 217 return true; 218 } 219 return false; 220 } 221 222 /** 223 * Returns true if application asked that this notification be part of a group. 224 */ isAppGroup()225 public boolean isAppGroup() { 226 if (getNotification().getGroup() != null || getNotification().getSortKey() != null) { 227 return true; 228 } 229 return false; 230 } 231 writeToParcel(Parcel out, int flags)232 public void writeToParcel(Parcel out, int flags) { 233 out.writeString(this.pkg); 234 out.writeString(this.opPkg); 235 out.writeInt(this.id); 236 if (this.tag != null) { 237 out.writeInt(1); 238 out.writeString(this.tag); 239 } else { 240 out.writeInt(0); 241 } 242 out.writeInt(this.uid); 243 out.writeInt(this.initialPid); 244 this.notification.writeToParcel(out, flags); 245 user.writeToParcel(out, flags); 246 out.writeLong(this.postTime); 247 if (this.overrideGroupKey != null) { 248 out.writeInt(1); 249 out.writeString(this.overrideGroupKey); 250 } else { 251 out.writeInt(0); 252 } 253 if (this.mInstanceId != null) { 254 out.writeInt(1); 255 mInstanceId.writeToParcel(out, flags); 256 } else { 257 out.writeInt(0); 258 } 259 } 260 describeContents()261 public int describeContents() { 262 return 0; 263 } 264 265 public static final @android.annotation.NonNull 266 Parcelable.Creator<StatusBarNotification> CREATOR = 267 new Parcelable.Creator<StatusBarNotification>() { 268 public StatusBarNotification createFromParcel(Parcel parcel) { 269 return new StatusBarNotification(parcel); 270 } 271 272 public StatusBarNotification[] newArray(int size) { 273 return new StatusBarNotification[size]; 274 } 275 }; 276 277 /** 278 * @hide 279 */ cloneLight()280 public StatusBarNotification cloneLight() { 281 final Notification no = new Notification(); 282 this.notification.cloneInto(no, false); // light copy 283 return cloneShallow(no); 284 } 285 286 @Override clone()287 public StatusBarNotification clone() { 288 return cloneShallow(this.notification.clone()); 289 } 290 291 /** 292 * @param notification Some kind of clone of this.notification. 293 * @return A shallow copy of self, with notification in place of this.notification. 294 * 295 * @hide 296 */ cloneShallow(Notification notification)297 public StatusBarNotification cloneShallow(Notification notification) { 298 StatusBarNotification result = new StatusBarNotification(this.pkg, this.opPkg, 299 this.id, this.tag, this.uid, this.initialPid, 300 notification, this.user, this.overrideGroupKey, this.postTime); 301 result.setInstanceId(this.mInstanceId); 302 return result; 303 } 304 305 @Override toString()306 public String toString() { 307 return formatSimple( 308 "StatusBarNotification(pkg=%s user=%s id=%d tag=%s key=%s: %s)", 309 this.pkg, this.user, this.id, this.tag, 310 this.key, this.notification); 311 } 312 313 /** 314 * Convenience method to check the notification's flags for 315 * {@link Notification#FLAG_ONGOING_EVENT}. 316 */ isOngoing()317 public boolean isOngoing() { 318 return (notification.flags & Notification.FLAG_ONGOING_EVENT) != 0; 319 } 320 321 /** 322 * @hide 323 * 324 * Convenience method to check the notification's flags for 325 * {@link Notification#FLAG_NO_DISMISS}. 326 */ isNonDismissable()327 public boolean isNonDismissable() { 328 return (notification.flags & Notification.FLAG_NO_DISMISS) != 0; 329 } 330 331 /** 332 * Convenience method to check the notification's flags for 333 * either {@link Notification#FLAG_ONGOING_EVENT} or 334 * {@link Notification#FLAG_NO_CLEAR}. 335 */ isClearable()336 public boolean isClearable() { 337 return ((notification.flags & Notification.FLAG_ONGOING_EVENT) == 0) 338 && ((notification.flags & Notification.FLAG_NO_CLEAR) == 0); 339 } 340 341 /** 342 * Returns a userid for whom this notification is intended. 343 * 344 * @deprecated Use {@link #getUser()} instead. 345 */ 346 @Deprecated getUserId()347 public int getUserId() { 348 return this.user.getIdentifier(); 349 } 350 351 /** 352 * Like {@link #getUserId()} but handles special users. 353 * @hide 354 */ getNormalizedUserId()355 public int getNormalizedUserId() { 356 int userId = getUserId(); 357 if (userId == UserHandle.USER_ALL) { 358 userId = UserHandle.USER_SYSTEM; 359 } 360 return userId; 361 } 362 363 /** The package that the notification belongs to. */ getPackageName()364 public String getPackageName() { 365 return pkg; 366 } 367 368 /** The id supplied to {@link android.app.NotificationManager#notify(int, Notification)}. */ getId()369 public int getId() { 370 return id; 371 } 372 373 /** 374 * The tag supplied to {@link android.app.NotificationManager#notify(int, Notification)}, 375 * or null if no tag was specified. 376 */ getTag()377 public String getTag() { 378 return tag; 379 } 380 381 /** 382 * The notifying app's ({@link #getPackageName()}'s) uid. 383 */ getUid()384 public int getUid() { 385 return uid; 386 } 387 388 /** 389 * The package that posted the notification. 390 * <p> Might be different from {@link #getPackageName()} if the app owning the notification has 391 * a {@link NotificationManager#setNotificationDelegate(String) notification delegate}. 392 */ getOpPkg()393 public @NonNull String getOpPkg() { 394 return opPkg; 395 } 396 397 /** @hide */ 398 @UnsupportedAppUsage getInitialPid()399 public int getInitialPid() { 400 return initialPid; 401 } 402 403 /** 404 * The {@link android.app.Notification} supplied to 405 * {@link android.app.NotificationManager#notify(int, Notification)}. 406 */ getNotification()407 public Notification getNotification() { 408 return notification; 409 } 410 411 /** 412 * The {@link android.os.UserHandle} for whom this notification is intended. 413 */ getUser()414 public UserHandle getUser() { 415 return user; 416 } 417 418 /** 419 * The time (in {@link System#currentTimeMillis} time) the notification was posted, 420 * which may be different than {@link android.app.Notification#when}. 421 */ getPostTime()422 public long getPostTime() { 423 return postTime; 424 } 425 426 /** 427 * A unique instance key for this notification record. 428 */ getKey()429 public String getKey() { 430 return key; 431 } 432 433 /** 434 * A key that indicates the group with which this message ranks. 435 */ getGroupKey()436 public String getGroupKey() { 437 return groupKey; 438 } 439 440 /** 441 * The ID passed to setGroup(), or the override, or null. 442 * 443 * @hide 444 */ getGroup()445 public String getGroup() { 446 if (overrideGroupKey != null) { 447 return overrideGroupKey; 448 } 449 return getNotification().getGroup(); 450 } 451 452 /** 453 * Sets the override group key. 454 */ setOverrideGroupKey(String overrideGroupKey)455 public void setOverrideGroupKey(String overrideGroupKey) { 456 this.overrideGroupKey = overrideGroupKey; 457 groupKey = groupKey(); 458 } 459 460 /** 461 * Returns the override group key. 462 */ getOverrideGroupKey()463 public String getOverrideGroupKey() { 464 return overrideGroupKey; 465 } 466 467 /** 468 * @hide 469 */ clearPackageContext()470 public void clearPackageContext() { 471 if (enablePerDisplayPackageContextCacheInStatusbarNotif()) { 472 mContextForDisplayId.clear(); 473 } else { 474 mContext = null; 475 } 476 } 477 478 /** 479 * @hide 480 */ getInstanceId()481 public InstanceId getInstanceId() { 482 return mInstanceId; 483 } 484 485 /** 486 * @hide 487 */ setInstanceId(InstanceId instanceId)488 public void setInstanceId(InstanceId instanceId) { 489 mInstanceId = instanceId; 490 } 491 492 /** 493 * @hide 494 */ 495 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) getPackageContext(Context context)496 public Context getPackageContext(Context context) { 497 if (enablePerDisplayPackageContextCacheInStatusbarNotif()) { 498 if (context == null) return null; 499 return mContextForDisplayId.computeIfAbsent(context.getDisplayId(), 500 (displayId) -> createPackageContext(context)); 501 } else { 502 if (mContext == null) { 503 try { 504 ApplicationInfo ai = context.getPackageManager() 505 .getApplicationInfoAsUser(pkg, 506 PackageManager.MATCH_UNINSTALLED_PACKAGES, 507 getNormalizedUserId()); 508 mContext = context.createApplicationContext(ai, 509 Context.CONTEXT_RESTRICTED); 510 } catch (PackageManager.NameNotFoundException e) { 511 mContext = null; 512 } 513 } 514 if (mContext == null) { 515 mContext = context; 516 } 517 return mContext; 518 } 519 } 520 createPackageContext(Context context)521 private Context createPackageContext(Context context) { 522 try { 523 Trace.beginSection("StatusBarNotification#createPackageContext"); 524 ApplicationInfo ai = context.getPackageManager() 525 .getApplicationInfoAsUser(pkg, PackageManager.MATCH_UNINSTALLED_PACKAGES, 526 getNormalizedUserId()); 527 return context.createApplicationContext(ai, Context.CONTEXT_RESTRICTED); 528 } catch (PackageManager.NameNotFoundException e) { 529 return context; 530 } finally { 531 Trace.endSection(); 532 } 533 } 534 535 /** 536 * Returns a LogMaker that contains all basic information of the notification. 537 * 538 * @hide 539 */ getLogMaker()540 public LogMaker getLogMaker() { 541 LogMaker logMaker = new LogMaker(MetricsEvent.VIEW_UNKNOWN).setPackageName(getPackageName()) 542 .addTaggedData(MetricsEvent.NOTIFICATION_ID, getId()) 543 .addTaggedData(MetricsEvent.NOTIFICATION_TAG, getTag()) 544 .addTaggedData(MetricsEvent.FIELD_NOTIFICATION_CHANNEL_ID, getChannelIdLogTag()) 545 .addTaggedData(MetricsEvent.FIELD_NOTIFICATION_GROUP_ID, getGroupLogTag()) 546 .addTaggedData(MetricsEvent.FIELD_NOTIFICATION_GROUP_SUMMARY, 547 getNotification().isGroupSummary() ? 1 : 0) 548 .addTaggedData(MetricsProto.MetricsEvent.FIELD_NOTIFICATION_CATEGORY, 549 getNotification().category); 550 if (getNotification().extras != null) { 551 // Log the style used, if present. We only log the hash here, as notification log 552 // events are frequent, while there are few styles (hence low chance of collisions). 553 String template = getNotification().extras.getString(Notification.EXTRA_TEMPLATE); 554 if (template != null && !template.isEmpty()) { 555 logMaker.addTaggedData(MetricsEvent.FIELD_NOTIFICATION_STYLE, 556 template.hashCode()); 557 } 558 ArrayList<Person> people = getNotification().extras.getParcelableArrayList( 559 Notification.EXTRA_PEOPLE_LIST, android.app.Person.class); 560 if (people != null && !people.isEmpty()) { 561 logMaker.addTaggedData(MetricsEvent.FIELD_NOTIFICATION_PEOPLE, people.size()); 562 } 563 } 564 return logMaker; 565 } 566 567 /** 568 * @hide 569 */ getShortcutId()570 public String getShortcutId() { 571 return getNotification().getShortcutId(); 572 } 573 574 /** 575 * Returns a probably-unique string based on the notification's group name, 576 * with no more than MAX_LOG_TAG_LENGTH characters. 577 * @return String based on group name of notification. 578 * @hide 579 */ getGroupLogTag()580 public String getGroupLogTag() { 581 return shortenTag(getGroup()); 582 } 583 584 /** 585 * Returns a probably-unique string based on the notification's channel ID, 586 * with no more than MAX_LOG_TAG_LENGTH characters. 587 * @return String based on channel ID of notification. 588 * @hide 589 */ getChannelIdLogTag()590 public String getChannelIdLogTag() { 591 if (notification.getChannelId() == null) { 592 return null; 593 } 594 return shortenTag(notification.getChannelId()); 595 } 596 597 // Make logTag with max size MAX_LOG_TAG_LENGTH. 598 // For shorter or equal tags, returns the tag. 599 // For longer tags, truncate the tag and append a hash of the full tag to 600 // fill the maximum size. shortenTag(String logTag)601 private String shortenTag(String logTag) { 602 if (logTag == null || logTag.length() <= MAX_LOG_TAG_LENGTH) { 603 return logTag; 604 } 605 String hash = Integer.toHexString(logTag.hashCode()); 606 return logTag.substring(0, MAX_LOG_TAG_LENGTH - hash.length() - 1) + "-" 607 + hash; 608 } 609 } 610