1 /* 2 * Copyright (C) 2019 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 android.app; 17 18 import android.annotation.NonNull; 19 import android.annotation.Nullable; 20 import android.annotation.UserIdInt; 21 import android.graphics.drawable.Icon; 22 import android.os.Parcel; 23 import android.os.Parcelable; 24 import android.text.TextUtils; 25 import android.util.Slog; 26 27 import java.util.ArrayList; 28 import java.util.Arrays; 29 import java.util.Collections; 30 import java.util.Comparator; 31 import java.util.HashSet; 32 import java.util.List; 33 import java.util.Objects; 34 import java.util.Set; 35 36 /** 37 * @hide 38 */ 39 public final class NotificationHistory implements Parcelable { 40 41 /** 42 * A historical notification. Any new fields added here should also be added to 43 * {@link #readNotificationFromParcel} and 44 * {@link #writeNotificationToParcel(HistoricalNotification, Parcel, int)}. 45 */ 46 public static final class HistoricalNotification { 47 private String mPackage; 48 private String mChannelName; 49 private String mChannelId; 50 private int mUid; 51 private @UserIdInt int mUserId; 52 private long mPostedTimeMs; 53 private String mTitle; 54 private String mText; 55 private Icon mIcon; 56 private String mConversationId; 57 HistoricalNotification()58 private HistoricalNotification() {} 59 getPackage()60 public String getPackage() { 61 return mPackage; 62 } 63 getChannelName()64 public String getChannelName() { 65 return mChannelName; 66 } 67 getChannelId()68 public String getChannelId() { 69 return mChannelId; 70 } 71 getUid()72 public int getUid() { 73 return mUid; 74 } 75 getUserId()76 public int getUserId() { 77 return mUserId; 78 } 79 getPostedTimeMs()80 public long getPostedTimeMs() { 81 return mPostedTimeMs; 82 } 83 getTitle()84 public String getTitle() { 85 return mTitle; 86 } 87 getText()88 public String getText() { 89 return mText; 90 } 91 getIcon()92 public Icon getIcon() { 93 return mIcon; 94 } 95 getKey()96 public String getKey() { 97 return mPackage + "|" + mUid + "|" + mPostedTimeMs; 98 } 99 getConversationId()100 public String getConversationId() { 101 return mConversationId; 102 } 103 104 @Override toString()105 public String toString() { 106 return "HistoricalNotification{" + 107 "key='" + getKey() + '\'' + 108 ", mChannelName='" + mChannelName + '\'' + 109 ", mChannelId='" + mChannelId + '\'' + 110 ", mUserId=" + mUserId + 111 ", mUid=" + mUid + 112 ", mTitle='" + mTitle + '\'' + 113 ", mText='" + mText + '\'' + 114 ", mIcon=" + mIcon + 115 ", mPostedTimeMs=" + mPostedTimeMs + 116 ", mConversationId=" + mConversationId + 117 '}'; 118 } 119 120 @Override equals(Object o)121 public boolean equals(Object o) { 122 if (this == o) return true; 123 if (o == null || getClass() != o.getClass()) return false; 124 HistoricalNotification that = (HistoricalNotification) o; 125 boolean iconsAreSame = getIcon() == null && that.getIcon() == null 126 || (getIcon() != null && that.getIcon() != null 127 && getIcon().sameAs(that.getIcon())); 128 return getUid() == that.getUid() && 129 getUserId() == that.getUserId() && 130 getPostedTimeMs() == that.getPostedTimeMs() && 131 Objects.equals(getPackage(), that.getPackage()) && 132 Objects.equals(getChannelName(), that.getChannelName()) && 133 Objects.equals(getChannelId(), that.getChannelId()) && 134 Objects.equals(getTitle(), that.getTitle()) && 135 Objects.equals(getText(), that.getText()) && 136 Objects.equals(getConversationId(), that.getConversationId()) && 137 iconsAreSame; 138 } 139 140 @Override hashCode()141 public int hashCode() { 142 return Objects.hash(getPackage(), getChannelName(), getChannelId(), getUid(), 143 getUserId(), 144 getPostedTimeMs(), getTitle(), getText(), getIcon(), getConversationId()); 145 } 146 147 public static final class Builder { 148 private String mPackage; 149 private String mChannelName; 150 private String mChannelId; 151 private int mUid; 152 private @UserIdInt int mUserId; 153 private long mPostedTimeMs; 154 private String mTitle; 155 private String mText; 156 private Icon mIcon; 157 private String mConversationId; 158 Builder()159 public Builder() {} 160 setPackage(String aPackage)161 public Builder setPackage(String aPackage) { 162 mPackage = aPackage; 163 return this; 164 } 165 setChannelName(String channelName)166 public Builder setChannelName(String channelName) { 167 mChannelName = channelName; 168 return this; 169 } 170 setChannelId(String channelId)171 public Builder setChannelId(String channelId) { 172 mChannelId = channelId; 173 return this; 174 } 175 setUid(int uid)176 public Builder setUid(int uid) { 177 mUid = uid; 178 return this; 179 } 180 setUserId(int userId)181 public Builder setUserId(int userId) { 182 mUserId = userId; 183 return this; 184 } 185 setPostedTimeMs(long postedTimeMs)186 public Builder setPostedTimeMs(long postedTimeMs) { 187 mPostedTimeMs = postedTimeMs; 188 return this; 189 } 190 setTitle(String title)191 public Builder setTitle(String title) { 192 mTitle = title; 193 return this; 194 } 195 setText(String text)196 public Builder setText(String text) { 197 mText = text; 198 return this; 199 } 200 setIcon(Icon icon)201 public Builder setIcon(Icon icon) { 202 mIcon = icon; 203 return this; 204 } 205 setConversationId(String conversationId)206 public Builder setConversationId(String conversationId) { 207 mConversationId = conversationId; 208 return this; 209 } 210 build()211 public HistoricalNotification build() { 212 HistoricalNotification n = new HistoricalNotification(); 213 n.mPackage = mPackage; 214 n.mChannelName = mChannelName; 215 n.mChannelId = mChannelId; 216 n.mUid = mUid; 217 n.mUserId = mUserId; 218 n.mPostedTimeMs = mPostedTimeMs; 219 n.mTitle = mTitle; 220 n.mText = mText; 221 n.mIcon = mIcon; 222 n.mConversationId = mConversationId; 223 return n; 224 } 225 } 226 } 227 228 // Only used when creating the resulting history. Not used for reading/unparceling. 229 private List<HistoricalNotification> mNotificationsToWrite = new ArrayList<>(); 230 // ditto 231 private Set<String> mStringsToWrite = new HashSet<>(); 232 233 // Mostly used for reading/unparceling events. 234 private Parcel mParcel = null; 235 private int mHistoryCount; 236 private int mIndex = 0; 237 238 // Sorted array of commonly used strings to shrink the size of the parcel. populated from 239 // mStringsToWrite on write and the parcel on read. 240 private String[] mStringPool; 241 242 /** 243 * Construct the iterator from a parcel. 244 */ NotificationHistory(Parcel in)245 private NotificationHistory(Parcel in) { 246 byte[] bytes = in.readBlob(); 247 Parcel data = Parcel.obtain(); 248 data.unmarshall(bytes, 0, bytes.length); 249 data.setDataPosition(0); 250 mHistoryCount = data.readInt(); 251 mIndex = data.readInt(); 252 if (mHistoryCount > 0) { 253 mStringPool = data.createStringArray(); 254 255 final int listByteLength = data.readInt(); 256 final int positionInParcel = data.readInt(); 257 mParcel = Parcel.obtain(); 258 mParcel.setDataPosition(0); 259 mParcel.appendFrom(data, data.dataPosition(), listByteLength); 260 mParcel.setDataSize(mParcel.dataPosition()); 261 mParcel.setDataPosition(positionInParcel); 262 } 263 } 264 265 /** 266 * Create an empty iterator. 267 */ NotificationHistory()268 public NotificationHistory() { 269 mHistoryCount = 0; 270 } 271 272 /** 273 * Returns whether or not there are more events to read using {@link #getNextNotification()}. 274 * 275 * @return true if there are more events, false otherwise. 276 */ hasNextNotification()277 public boolean hasNextNotification() { 278 return mIndex < mHistoryCount; 279 } 280 281 /** 282 * Retrieve the next {@link HistoricalNotification} from the collection and put the 283 * resulting data into {@code notificationOut}. 284 * 285 * @return The next {@link HistoricalNotification} or null if there are no more notifications. 286 */ getNextNotification()287 public @Nullable HistoricalNotification getNextNotification() { 288 if (!hasNextNotification()) { 289 return null; 290 } 291 HistoricalNotification n = readNotificationFromParcel(mParcel); 292 mIndex++; 293 if (!hasNextNotification()) { 294 mParcel.recycle(); 295 mParcel = null; 296 } 297 return n; 298 } 299 300 /** 301 * Adds all of the pooled strings that have been read from disk 302 */ addPooledStrings(@onNull List<String> strings)303 public void addPooledStrings(@NonNull List<String> strings) { 304 mStringsToWrite.addAll(strings); 305 } 306 307 /** 308 * Builds the pooled strings from pending notifications. Useful if the pooled strings on 309 * disk contains strings that aren't relevant to the notifications in our collection. 310 */ poolStringsFromNotifications()311 public void poolStringsFromNotifications() { 312 mStringsToWrite.clear(); 313 for (int i = 0; i < mNotificationsToWrite.size(); i++) { 314 final HistoricalNotification notification = mNotificationsToWrite.get(i); 315 mStringsToWrite.add(notification.getPackage()); 316 mStringsToWrite.add(notification.getChannelName()); 317 mStringsToWrite.add(notification.getChannelId()); 318 if (!TextUtils.isEmpty(notification.getConversationId())) { 319 mStringsToWrite.add(notification.getConversationId()); 320 } 321 } 322 } 323 324 /** 325 * Used when populating a history from disk; adds an historical notification. 326 */ addNotificationToWrite(@onNull HistoricalNotification notification)327 public void addNotificationToWrite(@NonNull HistoricalNotification notification) { 328 if (notification == null) { 329 return; 330 } 331 mNotificationsToWrite.add(notification); 332 mHistoryCount++; 333 } 334 335 /** 336 * Used when populating a history from disk; adds an historical notification. 337 */ addNewNotificationToWrite(@onNull HistoricalNotification notification)338 public void addNewNotificationToWrite(@NonNull HistoricalNotification notification) { 339 if (notification == null) { 340 return; 341 } 342 mNotificationsToWrite.add(0, notification); 343 mHistoryCount++; 344 } 345 addNotificationsToWrite(@onNull NotificationHistory notificationHistory)346 public void addNotificationsToWrite(@NonNull NotificationHistory notificationHistory) { 347 for (HistoricalNotification hn : notificationHistory.getNotificationsToWrite()) { 348 addNotificationToWrite(hn); 349 } 350 Collections.sort(mNotificationsToWrite, 351 (o1, o2) -> -1 * Long.compare(o1.getPostedTimeMs(), o2.getPostedTimeMs())); 352 poolStringsFromNotifications(); 353 } 354 355 /** 356 * Removes a package's historical notifications and regenerates the string pool 357 */ removeNotificationsFromWrite(String packageName)358 public void removeNotificationsFromWrite(String packageName) { 359 for (int i = mNotificationsToWrite.size() - 1; i >= 0; i--) { 360 if (packageName.equals(mNotificationsToWrite.get(i).getPackage())) { 361 mNotificationsToWrite.remove(i); 362 } 363 } 364 poolStringsFromNotifications(); 365 } 366 367 /** 368 * Removes an individual historical notification and regenerates the string pool 369 */ removeNotificationFromWrite(String packageName, long postedTime)370 public boolean removeNotificationFromWrite(String packageName, long postedTime) { 371 boolean removed = false; 372 for (int i = mNotificationsToWrite.size() - 1; i >= 0; i--) { 373 HistoricalNotification hn = mNotificationsToWrite.get(i); 374 if (packageName.equals(hn.getPackage()) 375 && postedTime == hn.getPostedTimeMs()) { 376 removed = true; 377 mNotificationsToWrite.remove(i); 378 } 379 } 380 if (removed) { 381 poolStringsFromNotifications(); 382 } 383 384 return removed; 385 } 386 387 /** 388 * Removes all notifications from a conversation and regenerates the string pool 389 */ removeConversationFromWrite(String packageName, String conversationId)390 public boolean removeConversationFromWrite(String packageName, String conversationId) { 391 boolean removed = false; 392 for (int i = mNotificationsToWrite.size() - 1; i >= 0; i--) { 393 HistoricalNotification hn = mNotificationsToWrite.get(i); 394 if (packageName.equals(hn.getPackage()) 395 && conversationId.equals(hn.getConversationId())) { 396 removed = true; 397 mNotificationsToWrite.remove(i); 398 } 399 } 400 if (removed) { 401 poolStringsFromNotifications(); 402 } 403 404 return removed; 405 } 406 407 /** 408 * Gets pooled strings in order to write them to disk 409 */ getPooledStringsToWrite()410 public @NonNull String[] getPooledStringsToWrite() { 411 String[] stringsToWrite = mStringsToWrite.toArray(new String[]{}); 412 Arrays.sort(stringsToWrite); 413 return stringsToWrite; 414 } 415 416 /** 417 * Gets the historical notifications in order to write them to disk 418 */ getNotificationsToWrite()419 public @NonNull List<HistoricalNotification> getNotificationsToWrite() { 420 return mNotificationsToWrite; 421 } 422 423 /** 424 * Gets the number of notifications in the collection 425 */ getHistoryCount()426 public int getHistoryCount() { 427 return mHistoryCount; 428 } 429 findStringIndex(String str)430 private int findStringIndex(String str) { 431 final int index = Arrays.binarySearch(mStringPool, str); 432 if (index < 0) { 433 throw new IllegalStateException("String '" + str + "' is not in the string pool"); 434 } 435 return index; 436 } 437 438 /** 439 * Writes a single notification to the parcel. Modify this when updating member variables of 440 * {@link HistoricalNotification}. 441 */ writeNotificationToParcel(HistoricalNotification notification, Parcel p, int flags)442 private void writeNotificationToParcel(HistoricalNotification notification, Parcel p, 443 int flags) { 444 final int packageIndex; 445 if (notification.mPackage != null) { 446 packageIndex = findStringIndex(notification.mPackage); 447 } else { 448 packageIndex = -1; 449 } 450 451 final int channelNameIndex; 452 if (notification.getChannelName() != null) { 453 channelNameIndex = findStringIndex(notification.getChannelName()); 454 } else { 455 channelNameIndex = -1; 456 } 457 458 final int channelIdIndex; 459 if (notification.getChannelId() != null) { 460 channelIdIndex = findStringIndex(notification.getChannelId()); 461 } else { 462 channelIdIndex = -1; 463 } 464 465 final int conversationIdIndex; 466 if (!TextUtils.isEmpty(notification.getConversationId())) { 467 conversationIdIndex = findStringIndex(notification.getConversationId()); 468 } else { 469 conversationIdIndex = -1; 470 } 471 472 p.writeInt(packageIndex); 473 p.writeInt(channelNameIndex); 474 p.writeInt(channelIdIndex); 475 p.writeInt(conversationIdIndex); 476 p.writeInt(notification.getUid()); 477 p.writeInt(notification.getUserId()); 478 p.writeLong(notification.getPostedTimeMs()); 479 p.writeString(notification.getTitle()); 480 p.writeString(notification.getText()); 481 notification.getIcon().writeToParcel(p, flags); 482 } 483 484 /** 485 * Reads a single notification from the parcel. Modify this when updating member variables of 486 * {@link HistoricalNotification}. 487 */ readNotificationFromParcel(Parcel p)488 private HistoricalNotification readNotificationFromParcel(Parcel p) { 489 HistoricalNotification.Builder notificationOut = new HistoricalNotification.Builder(); 490 final int packageIndex = p.readInt(); 491 if (packageIndex >= 0) { 492 notificationOut.mPackage = mStringPool[packageIndex]; 493 } else { 494 notificationOut.mPackage = null; 495 } 496 497 final int channelNameIndex = p.readInt(); 498 if (channelNameIndex >= 0) { 499 notificationOut.setChannelName(mStringPool[channelNameIndex]); 500 } else { 501 notificationOut.setChannelName(null); 502 } 503 504 final int channelIdIndex = p.readInt(); 505 if (channelIdIndex >= 0) { 506 notificationOut.setChannelId(mStringPool[channelIdIndex]); 507 } else { 508 notificationOut.setChannelId(null); 509 } 510 511 final int conversationIdIndex = p.readInt(); 512 if (conversationIdIndex >= 0) { 513 notificationOut.setConversationId(mStringPool[conversationIdIndex]); 514 } else { 515 notificationOut.setConversationId(null); 516 } 517 518 notificationOut.setUid(p.readInt()); 519 notificationOut.setUserId(p.readInt()); 520 notificationOut.setPostedTimeMs(p.readLong()); 521 notificationOut.setTitle(p.readString()); 522 notificationOut.setText(p.readString()); 523 notificationOut.setIcon(Icon.CREATOR.createFromParcel(p)); 524 525 return notificationOut.build(); 526 } 527 528 @Override describeContents()529 public int describeContents() { 530 return 0; 531 } 532 533 @Override writeToParcel(Parcel dest, int flags)534 public void writeToParcel(Parcel dest, int flags) { 535 Parcel data = Parcel.obtain(); 536 data.writeInt(mHistoryCount); 537 data.writeInt(mIndex); 538 if (mHistoryCount > 0) { 539 mStringPool = getPooledStringsToWrite(); 540 data.writeStringArray(mStringPool); 541 542 if (!mNotificationsToWrite.isEmpty()) { 543 // typically system_server to a process 544 545 // Write out the events 546 Parcel p = Parcel.obtain(); 547 try { 548 p.setDataPosition(0); 549 for (int i = 0; i < mHistoryCount; i++) { 550 final HistoricalNotification notification = mNotificationsToWrite.get(i); 551 writeNotificationToParcel(notification, p, flags); 552 } 553 554 final int listByteLength = p.dataPosition(); 555 556 // Write the total length of the data. 557 data.writeInt(listByteLength); 558 559 // Write our current position into the data. 560 data.writeInt(0); 561 562 // Write the data. 563 data.appendFrom(p, 0, listByteLength); 564 } finally { 565 p.recycle(); 566 } 567 568 } else if (mParcel != null) { 569 // typically process to process as mNotificationsToWrite is not populated on 570 // unparcel. 571 572 // Write the total length of the data. 573 data.writeInt(mParcel.dataSize()); 574 575 // Write out current position into the data. 576 data.writeInt(mParcel.dataPosition()); 577 578 // Write the data. 579 data.appendFrom(mParcel, 0, mParcel.dataSize()); 580 } else { 581 throw new IllegalStateException( 582 "Either mParcel or mNotificationsToWrite must not be null"); 583 } 584 } 585 // Data can be too large for a transact. Write the data as a Blob, which will be written to 586 // ashmem if too large. 587 dest.writeBlob(data.marshall()); 588 } 589 590 public static final @NonNull Creator<NotificationHistory> CREATOR 591 = new Creator<NotificationHistory>() { 592 @Override 593 public NotificationHistory createFromParcel(Parcel source) { 594 return new NotificationHistory(source); 595 } 596 597 @Override 598 public NotificationHistory[] newArray(int size) { 599 return new NotificationHistory[size]; 600 } 601 }; 602 } 603