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