• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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