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