blob: 59dc9991c832f0145c35f3b9dafed37592968831 [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.app;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.graphics.drawable.Icon;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.Slog;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
* @hide
*/
public final class NotificationHistory implements Parcelable {
/**
* A historical notification. Any new fields added here should also be added to
* {@link #readNotificationFromParcel} and
* {@link #writeNotificationToParcel(HistoricalNotification, Parcel, int)}.
*/
public static final class HistoricalNotification {
private String mPackage;
private String mChannelName;
private String mChannelId;
private int mUid;
private @UserIdInt int mUserId;
private long mPostedTimeMs;
private String mTitle;
private String mText;
private Icon mIcon;
private String mConversationId;
private HistoricalNotification() {}
public String getPackage() {
return mPackage;
}
public String getChannelName() {
return mChannelName;
}
public String getChannelId() {
return mChannelId;
}
public int getUid() {
return mUid;
}
public int getUserId() {
return mUserId;
}
public long getPostedTimeMs() {
return mPostedTimeMs;
}
public String getTitle() {
return mTitle;
}
public String getText() {
return mText;
}
public Icon getIcon() {
return mIcon;
}
public String getKey() {
return mPackage + "|" + mUid + "|" + mPostedTimeMs;
}
public String getConversationId() {
return mConversationId;
}
@Override
public String toString() {
return "HistoricalNotification{" +
"key='" + getKey() + '\'' +
", mChannelName='" + mChannelName + '\'' +
", mChannelId='" + mChannelId + '\'' +
", mUserId=" + mUserId +
", mUid=" + mUid +
", mTitle='" + mTitle + '\'' +
", mText='" + mText + '\'' +
", mIcon=" + mIcon +
", mPostedTimeMs=" + mPostedTimeMs +
", mConversationId=" + mConversationId +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
HistoricalNotification that = (HistoricalNotification) o;
boolean iconsAreSame = getIcon() == null && that.getIcon() == null
|| (getIcon() != null && that.getIcon() != null
&& getIcon().sameAs(that.getIcon()));
return getUid() == that.getUid() &&
getUserId() == that.getUserId() &&
getPostedTimeMs() == that.getPostedTimeMs() &&
Objects.equals(getPackage(), that.getPackage()) &&
Objects.equals(getChannelName(), that.getChannelName()) &&
Objects.equals(getChannelId(), that.getChannelId()) &&
Objects.equals(getTitle(), that.getTitle()) &&
Objects.equals(getText(), that.getText()) &&
Objects.equals(getConversationId(), that.getConversationId()) &&
iconsAreSame;
}
@Override
public int hashCode() {
return Objects.hash(getPackage(), getChannelName(), getChannelId(), getUid(),
getUserId(),
getPostedTimeMs(), getTitle(), getText(), getIcon(), getConversationId());
}
public static final class Builder {
private String mPackage;
private String mChannelName;
private String mChannelId;
private int mUid;
private @UserIdInt int mUserId;
private long mPostedTimeMs;
private String mTitle;
private String mText;
private Icon mIcon;
private String mConversationId;
public Builder() {}
public Builder setPackage(String aPackage) {
mPackage = aPackage;
return this;
}
public Builder setChannelName(String channelName) {
mChannelName = channelName;
return this;
}
public Builder setChannelId(String channelId) {
mChannelId = channelId;
return this;
}
public Builder setUid(int uid) {
mUid = uid;
return this;
}
public Builder setUserId(int userId) {
mUserId = userId;
return this;
}
public Builder setPostedTimeMs(long postedTimeMs) {
mPostedTimeMs = postedTimeMs;
return this;
}
public Builder setTitle(String title) {
mTitle = title;
return this;
}
public Builder setText(String text) {
mText = text;
return this;
}
public Builder setIcon(Icon icon) {
mIcon = icon;
return this;
}
public Builder setConversationId(String conversationId) {
mConversationId = conversationId;
return this;
}
public HistoricalNotification build() {
HistoricalNotification n = new HistoricalNotification();
n.mPackage = mPackage;
n.mChannelName = mChannelName;
n.mChannelId = mChannelId;
n.mUid = mUid;
n.mUserId = mUserId;
n.mPostedTimeMs = mPostedTimeMs;
n.mTitle = mTitle;
n.mText = mText;
n.mIcon = mIcon;
n.mConversationId = mConversationId;
return n;
}
}
}
// Only used when creating the resulting history. Not used for reading/unparceling.
private List<HistoricalNotification> mNotificationsToWrite = new ArrayList<>();
// ditto
private Set<String> mStringsToWrite = new HashSet<>();
// Mostly used for reading/unparceling events.
private Parcel mParcel = null;
private int mHistoryCount;
private int mIndex = 0;
// Sorted array of commonly used strings to shrink the size of the parcel. populated from
// mStringsToWrite on write and the parcel on read.
private String[] mStringPool;
/**
* Construct the iterator from a parcel.
*/
private NotificationHistory(Parcel in) {
byte[] bytes = in.readBlob();
Parcel data = Parcel.obtain();
data.unmarshall(bytes, 0, bytes.length);
data.setDataPosition(0);
mHistoryCount = data.readInt();
mIndex = data.readInt();
if (mHistoryCount > 0) {
mStringPool = data.createStringArray();
final int listByteLength = data.readInt();
final int positionInParcel = data.readInt();
mParcel = Parcel.obtain();
mParcel.setDataPosition(0);
mParcel.appendFrom(data, data.dataPosition(), listByteLength);
mParcel.setDataSize(mParcel.dataPosition());
mParcel.setDataPosition(positionInParcel);
}
}
/**
* Create an empty iterator.
*/
public NotificationHistory() {
mHistoryCount = 0;
}
/**
* Returns whether or not there are more events to read using {@link #getNextNotification()}.
*
* @return true if there are more events, false otherwise.
*/
public boolean hasNextNotification() {
return mIndex < mHistoryCount;
}
/**
* Retrieve the next {@link HistoricalNotification} from the collection and put the
* resulting data into {@code notificationOut}.
*
* @return The next {@link HistoricalNotification} or null if there are no more notifications.
*/
public @Nullable HistoricalNotification getNextNotification() {
if (!hasNextNotification()) {
return null;
}
HistoricalNotification n = readNotificationFromParcel(mParcel);
mIndex++;
if (!hasNextNotification()) {
mParcel.recycle();
mParcel = null;
}
return n;
}
/**
* Adds all of the pooled strings that have been read from disk
*/
public void addPooledStrings(@NonNull List<String> strings) {
mStringsToWrite.addAll(strings);
}
/**
* Builds the pooled strings from pending notifications. Useful if the pooled strings on
* disk contains strings that aren't relevant to the notifications in our collection.
*/
public void poolStringsFromNotifications() {
mStringsToWrite.clear();
for (int i = 0; i < mNotificationsToWrite.size(); i++) {
final HistoricalNotification notification = mNotificationsToWrite.get(i);
mStringsToWrite.add(notification.getPackage());
mStringsToWrite.add(notification.getChannelName());
mStringsToWrite.add(notification.getChannelId());
if (!TextUtils.isEmpty(notification.getConversationId())) {
mStringsToWrite.add(notification.getConversationId());
}
}
}
/**
* Used when populating a history from disk; adds an historical notification.
*/
public void addNotificationToWrite(@NonNull HistoricalNotification notification) {
if (notification == null) {
return;
}
mNotificationsToWrite.add(notification);
mHistoryCount++;
}
/**
* Used when populating a history from disk; adds an historical notification.
*/
public void addNewNotificationToWrite(@NonNull HistoricalNotification notification) {
if (notification == null) {
return;
}
mNotificationsToWrite.add(0, notification);
mHistoryCount++;
}
public void addNotificationsToWrite(@NonNull NotificationHistory notificationHistory) {
for (HistoricalNotification hn : notificationHistory.getNotificationsToWrite()) {
addNotificationToWrite(hn);
}
Collections.sort(mNotificationsToWrite,
(o1, o2) -> -1 * Long.compare(o1.getPostedTimeMs(), o2.getPostedTimeMs()));
poolStringsFromNotifications();
}
/**
* Removes a package's historical notifications and regenerates the string pool
*/
public void removeNotificationsFromWrite(String packageName) {
for (int i = mNotificationsToWrite.size() - 1; i >= 0; i--) {
if (packageName.equals(mNotificationsToWrite.get(i).getPackage())) {
mNotificationsToWrite.remove(i);
}
}
poolStringsFromNotifications();
}
/**
* Removes an individual historical notification and regenerates the string pool
*/
public boolean removeNotificationFromWrite(String packageName, long postedTime) {
boolean removed = false;
for (int i = mNotificationsToWrite.size() - 1; i >= 0; i--) {
HistoricalNotification hn = mNotificationsToWrite.get(i);
if (packageName.equals(hn.getPackage())
&& postedTime == hn.getPostedTimeMs()) {
removed = true;
mNotificationsToWrite.remove(i);
}
}
if (removed) {
poolStringsFromNotifications();
}
return removed;
}
/**
* Removes all notifications from a conversation and regenerates the string pool
*/
public boolean removeConversationFromWrite(String packageName, String conversationId) {
boolean removed = false;
for (int i = mNotificationsToWrite.size() - 1; i >= 0; i--) {
HistoricalNotification hn = mNotificationsToWrite.get(i);
if (packageName.equals(hn.getPackage())
&& conversationId.equals(hn.getConversationId())) {
removed = true;
mNotificationsToWrite.remove(i);
}
}
if (removed) {
poolStringsFromNotifications();
}
return removed;
}
/**
* Gets pooled strings in order to write them to disk
*/
public @NonNull String[] getPooledStringsToWrite() {
String[] stringsToWrite = mStringsToWrite.toArray(new String[]{});
Arrays.sort(stringsToWrite);
return stringsToWrite;
}
/**
* Gets the historical notifications in order to write them to disk
*/
public @NonNull List<HistoricalNotification> getNotificationsToWrite() {
return mNotificationsToWrite;
}
/**
* Gets the number of notifications in the collection
*/
public int getHistoryCount() {
return mHistoryCount;
}
private int findStringIndex(String str) {
final int index = Arrays.binarySearch(mStringPool, str);
if (index < 0) {
throw new IllegalStateException("String '" + str + "' is not in the string pool");
}
return index;
}
/**
* Writes a single notification to the parcel. Modify this when updating member variables of
* {@link HistoricalNotification}.
*/
private void writeNotificationToParcel(HistoricalNotification notification, Parcel p,
int flags) {
final int packageIndex;
if (notification.mPackage != null) {
packageIndex = findStringIndex(notification.mPackage);
} else {
packageIndex = -1;
}
final int channelNameIndex;
if (notification.getChannelName() != null) {
channelNameIndex = findStringIndex(notification.getChannelName());
} else {
channelNameIndex = -1;
}
final int channelIdIndex;
if (notification.getChannelId() != null) {
channelIdIndex = findStringIndex(notification.getChannelId());
} else {
channelIdIndex = -1;
}
final int conversationIdIndex;
if (!TextUtils.isEmpty(notification.getConversationId())) {
conversationIdIndex = findStringIndex(notification.getConversationId());
} else {
conversationIdIndex = -1;
}
p.writeInt(packageIndex);
p.writeInt(channelNameIndex);
p.writeInt(channelIdIndex);
p.writeInt(conversationIdIndex);
p.writeInt(notification.getUid());
p.writeInt(notification.getUserId());
p.writeLong(notification.getPostedTimeMs());
p.writeString(notification.getTitle());
p.writeString(notification.getText());
notification.getIcon().writeToParcel(p, flags);
}
/**
* Reads a single notification from the parcel. Modify this when updating member variables of
* {@link HistoricalNotification}.
*/
private HistoricalNotification readNotificationFromParcel(Parcel p) {
HistoricalNotification.Builder notificationOut = new HistoricalNotification.Builder();
final int packageIndex = p.readInt();
if (packageIndex >= 0) {
notificationOut.mPackage = mStringPool[packageIndex];
} else {
notificationOut.mPackage = null;
}
final int channelNameIndex = p.readInt();
if (channelNameIndex >= 0) {
notificationOut.setChannelName(mStringPool[channelNameIndex]);
} else {
notificationOut.setChannelName(null);
}
final int channelIdIndex = p.readInt();
if (channelIdIndex >= 0) {
notificationOut.setChannelId(mStringPool[channelIdIndex]);
} else {
notificationOut.setChannelId(null);
}
final int conversationIdIndex = p.readInt();
if (conversationIdIndex >= 0) {
notificationOut.setConversationId(mStringPool[conversationIdIndex]);
} else {
notificationOut.setConversationId(null);
}
notificationOut.setUid(p.readInt());
notificationOut.setUserId(p.readInt());
notificationOut.setPostedTimeMs(p.readLong());
notificationOut.setTitle(p.readString());
notificationOut.setText(p.readString());
notificationOut.setIcon(Icon.CREATOR.createFromParcel(p));
return notificationOut.build();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
Parcel data = Parcel.obtain();
data.writeInt(mHistoryCount);
data.writeInt(mIndex);
if (mHistoryCount > 0) {
mStringPool = getPooledStringsToWrite();
data.writeStringArray(mStringPool);
if (!mNotificationsToWrite.isEmpty()) {
// typically system_server to a process
// Write out the events
Parcel p = Parcel.obtain();
try {
p.setDataPosition(0);
for (int i = 0; i < mHistoryCount; i++) {
final HistoricalNotification notification = mNotificationsToWrite.get(i);
writeNotificationToParcel(notification, p, flags);
}
final int listByteLength = p.dataPosition();
// Write the total length of the data.
data.writeInt(listByteLength);
// Write our current position into the data.
data.writeInt(0);
// Write the data.
data.appendFrom(p, 0, listByteLength);
} finally {
p.recycle();
}
} else if (mParcel != null) {
// typically process to process as mNotificationsToWrite is not populated on
// unparcel.
// Write the total length of the data.
data.writeInt(mParcel.dataSize());
// Write out current position into the data.
data.writeInt(mParcel.dataPosition());
// Write the data.
data.appendFrom(mParcel, 0, mParcel.dataSize());
} else {
throw new IllegalStateException(
"Either mParcel or mNotificationsToWrite must not be null");
}
}
// Data can be too large for a transact. Write the data as a Blob, which will be written to
// ashmem if too large.
dest.writeBlob(data.marshall());
}
public static final @NonNull Creator<NotificationHistory> CREATOR
= new Creator<NotificationHistory>() {
@Override
public NotificationHistory createFromParcel(Parcel source) {
return new NotificationHistory(source);
}
@Override
public NotificationHistory[] newArray(int size) {
return new NotificationHistory[size];
}
};
}