Add ability to write and read Notification history.
Bug: 137396965 Test: atest Change-Id: I6bfdd0f6906dcf9c58b592e03bef335c4920d94d
This commit is contained in:
19
core/java/android/app/NotificationHistory.aidl
Normal file
19
core/java/android/app/NotificationHistory.aidl
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
parcelable NotificationHistory;
|
||||
506
core/java/android/app/NotificationHistory.java
Normal file
506
core/java/android/app/NotificationHistory.java
Normal file
@@ -0,0 +1,506 @@
|
||||
/*
|
||||
* 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 java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
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 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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "HistoricalNotification{" +
|
||||
"key='" + getKey() + '\'' +
|
||||
", mChannelName='" + mChannelName + '\'' +
|
||||
", mChannelId='" + mChannelId + '\'' +
|
||||
", mUserId=" + mUserId +
|
||||
", mTitle='" + mTitle + '\'' +
|
||||
", mText='" + mText + '\'' +
|
||||
", mIcon=" + mIcon +
|
||||
'}';
|
||||
}
|
||||
|
||||
@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()) &&
|
||||
iconsAreSame;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(getPackage(), getChannelName(), getChannelId(), getUid(),
|
||||
getUserId(),
|
||||
getPostedTimeMs(), getTitle(), getText(), getIcon());
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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 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;
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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++;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
p.writeInt(packageIndex);
|
||||
p.writeInt(channelNameIndex);
|
||||
p.writeInt(channelIdIndex);
|
||||
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);
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* 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 static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.app.NotificationHistory.HistoricalNotification;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class NotificationHistoryTest {
|
||||
|
||||
private HistoricalNotification getHistoricalNotification(int index) {
|
||||
return getHistoricalNotification("package" + index, index);
|
||||
}
|
||||
|
||||
private HistoricalNotification getHistoricalNotification(String packageName, int index) {
|
||||
String expectedChannelName = "channelName" + index;
|
||||
String expectedChannelId = "channelId" + index;
|
||||
int expectedUid = 1123456 + index;
|
||||
int expectedUserId = 11 + index;
|
||||
long expectedPostTime = 987654321 + index;
|
||||
String expectedTitle = "title" + index;
|
||||
String expectedText = "text" + index;
|
||||
Icon expectedIcon = Icon.createWithResource(InstrumentationRegistry.getContext(),
|
||||
index);
|
||||
|
||||
return new HistoricalNotification.Builder()
|
||||
.setPackage(packageName)
|
||||
.setChannelName(expectedChannelName)
|
||||
.setChannelId(expectedChannelId)
|
||||
.setUid(expectedUid)
|
||||
.setUserId(expectedUserId)
|
||||
.setPostedTimeMs(expectedPostTime)
|
||||
.setTitle(expectedTitle)
|
||||
.setText(expectedText)
|
||||
.setIcon(expectedIcon)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHistoricalNotificationBuilder() {
|
||||
String expectedPackage = "package";
|
||||
String expectedChannelName = "channelName";
|
||||
String expectedChannelId = "channelId";
|
||||
int expectedUid = 1123456;
|
||||
int expectedUserId = 11;
|
||||
long expectedPostTime = 987654321;
|
||||
String expectedTitle = "title";
|
||||
String expectedText = "text";
|
||||
Icon expectedIcon = Icon.createWithResource(InstrumentationRegistry.getContext(),
|
||||
android.R.drawable.btn_star);
|
||||
|
||||
HistoricalNotification n = new HistoricalNotification.Builder()
|
||||
.setPackage(expectedPackage)
|
||||
.setChannelName(expectedChannelName)
|
||||
.setChannelId(expectedChannelId)
|
||||
.setUid(expectedUid)
|
||||
.setUserId(expectedUserId)
|
||||
.setPostedTimeMs(expectedPostTime)
|
||||
.setTitle(expectedTitle)
|
||||
.setText(expectedText)
|
||||
.setIcon(expectedIcon)
|
||||
.build();
|
||||
|
||||
assertThat(n.getPackage()).isEqualTo(expectedPackage);
|
||||
assertThat(n.getChannelName()).isEqualTo(expectedChannelName);
|
||||
assertThat(n.getChannelId()).isEqualTo(expectedChannelId);
|
||||
assertThat(n.getUid()).isEqualTo(expectedUid);
|
||||
assertThat(n.getUserId()).isEqualTo(expectedUserId);
|
||||
assertThat(n.getPostedTimeMs()).isEqualTo(expectedPostTime);
|
||||
assertThat(n.getTitle()).isEqualTo(expectedTitle);
|
||||
assertThat(n.getText()).isEqualTo(expectedText);
|
||||
assertThat(expectedIcon.sameAs(n.getIcon())).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddNotificationToWrite() {
|
||||
NotificationHistory history = new NotificationHistory();
|
||||
HistoricalNotification n = getHistoricalNotification(0);
|
||||
HistoricalNotification n2 = getHistoricalNotification(1);
|
||||
|
||||
history.addNotificationToWrite(n2);
|
||||
history.addNotificationToWrite(n);
|
||||
|
||||
assertThat(history.getNotificationsToWrite().size()).isEqualTo(2);
|
||||
assertThat(history.getNotificationsToWrite().get(0)).isSameAs(n2);
|
||||
assertThat(history.getNotificationsToWrite().get(1)).isSameAs(n);
|
||||
assertThat(history.getHistoryCount()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPoolStringsFromNotifications() {
|
||||
NotificationHistory history = new NotificationHistory();
|
||||
|
||||
List<String> expectedStrings = new ArrayList<>();
|
||||
for (int i = 1; i <= 10; i++) {
|
||||
HistoricalNotification n = getHistoricalNotification(i);
|
||||
expectedStrings.add(n.getPackage());
|
||||
expectedStrings.add(n.getChannelName());
|
||||
expectedStrings.add(n.getChannelId());
|
||||
history.addNotificationToWrite(n);
|
||||
}
|
||||
|
||||
history.poolStringsFromNotifications();
|
||||
|
||||
assertThat(history.getPooledStringsToWrite().length).isEqualTo(expectedStrings.size());
|
||||
String previous = null;
|
||||
for (String actual : history.getPooledStringsToWrite()) {
|
||||
assertThat(expectedStrings).contains(actual);
|
||||
|
||||
if (previous != null) {
|
||||
assertThat(actual).isGreaterThan(previous);
|
||||
}
|
||||
previous = actual;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddPooledStrings() {
|
||||
NotificationHistory history = new NotificationHistory();
|
||||
|
||||
List<String> expectedStrings = new ArrayList<>();
|
||||
for (int i = 1; i <= 10; i++) {
|
||||
HistoricalNotification n = getHistoricalNotification(i);
|
||||
expectedStrings.add(n.getPackage());
|
||||
expectedStrings.add(n.getChannelName());
|
||||
expectedStrings.add(n.getChannelId());
|
||||
history.addNotificationToWrite(n);
|
||||
}
|
||||
|
||||
history.addPooledStrings(expectedStrings);
|
||||
|
||||
String[] actualStrings = history.getPooledStringsToWrite();
|
||||
assertThat(actualStrings.length).isEqualTo(expectedStrings.size());
|
||||
String previous = null;
|
||||
for (String actual : actualStrings) {
|
||||
assertThat(expectedStrings).contains(actual);
|
||||
|
||||
if (previous != null) {
|
||||
assertThat(actual).isGreaterThan(previous);
|
||||
}
|
||||
previous = actual;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveNotificationsFromWrite() {
|
||||
NotificationHistory history = new NotificationHistory();
|
||||
|
||||
List<HistoricalNotification> postRemoveExpectedEntries = new ArrayList<>();
|
||||
List<String> postRemoveExpectedStrings = new ArrayList<>();
|
||||
for (int i = 1; i <= 10; i++) {
|
||||
HistoricalNotification n =
|
||||
getHistoricalNotification((i % 2 == 0) ? "pkgEven" : "pkgOdd", i);
|
||||
|
||||
if (i % 2 == 0) {
|
||||
postRemoveExpectedStrings.add(n.getPackage());
|
||||
postRemoveExpectedStrings.add(n.getChannelName());
|
||||
postRemoveExpectedStrings.add(n.getChannelId());
|
||||
postRemoveExpectedEntries.add(n);
|
||||
}
|
||||
|
||||
history.addNotificationToWrite(n);
|
||||
}
|
||||
|
||||
history.poolStringsFromNotifications();
|
||||
|
||||
assertThat(history.getNotificationsToWrite().size()).isEqualTo(10);
|
||||
// 2 package names and 10 * 2 unique channel names and ids
|
||||
assertThat(history.getPooledStringsToWrite().length).isEqualTo(22);
|
||||
|
||||
history.removeNotificationsFromWrite("pkgOdd");
|
||||
|
||||
|
||||
// 1 package names and 5 * 2 unique channel names and ids
|
||||
assertThat(history.getPooledStringsToWrite().length).isEqualTo(11);
|
||||
assertThat(history.getNotificationsToWrite())
|
||||
.containsExactlyElementsIn(postRemoveExpectedEntries);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParceling() {
|
||||
NotificationHistory history = new NotificationHistory();
|
||||
|
||||
List<HistoricalNotification> expectedEntries = new ArrayList<>();
|
||||
for (int i = 10; i >= 1; i--) {
|
||||
HistoricalNotification n = getHistoricalNotification(i);
|
||||
expectedEntries.add(n);
|
||||
history.addNotificationToWrite(n);
|
||||
}
|
||||
history.poolStringsFromNotifications();
|
||||
|
||||
Parcel parcel = Parcel.obtain();
|
||||
history.writeToParcel(parcel, 0);
|
||||
parcel.setDataPosition(0);
|
||||
NotificationHistory parceledHistory = NotificationHistory.CREATOR.createFromParcel(parcel);
|
||||
|
||||
assertThat(parceledHistory.getHistoryCount()).isEqualTo(expectedEntries.size());
|
||||
|
||||
for (int i = 0; i < expectedEntries.size(); i++) {
|
||||
assertThat(parceledHistory.hasNextNotification()).isTrue();
|
||||
|
||||
HistoricalNotification postParcelNotification = parceledHistory.getNextNotification();
|
||||
assertThat(postParcelNotification).isEqualTo(expectedEntries.get(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
/*
|
||||
* 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 com.android.server.notification;
|
||||
|
||||
import android.app.NotificationHistory;
|
||||
import android.app.NotificationHistory.HistoricalNotification;
|
||||
import android.os.Handler;
|
||||
import android.util.AtomicFile;
|
||||
import android.util.Slog;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
|
||||
/**
|
||||
* Provides an interface to write and query for notification history data for a user from a Protocol
|
||||
* Buffer database.
|
||||
*
|
||||
* Periodically writes the buffered history to disk but can also accept force writes based on
|
||||
* outside changes (like a pending shutdown).
|
||||
*/
|
||||
public class NotificationHistoryDatabase {
|
||||
private static final int DEFAULT_CURRENT_VERSION = 1;
|
||||
|
||||
private static final String TAG = "NotiHistoryDatabase";
|
||||
private static final boolean DEBUG = NotificationManagerService.DBG;
|
||||
private static final int HISTORY_RETENTION_DAYS = 2;
|
||||
private static final long WRITE_BUFFER_INTERVAL_MS = 1000 * 60 * 20;
|
||||
|
||||
private final Object mLock = new Object();
|
||||
private Handler mFileWriteHandler;
|
||||
@VisibleForTesting
|
||||
// List of files holding history information, sorted newest to oldest
|
||||
final LinkedList<AtomicFile> mHistoryFiles;
|
||||
private final GregorianCalendar mCal;
|
||||
private final File mHistoryDir;
|
||||
private final File mVersionFile;
|
||||
// Current version of the database files schema
|
||||
private int mCurrentVersion;
|
||||
private final WriteBufferRunnable mWriteBufferRunnable;
|
||||
|
||||
// Object containing posted notifications that have not yet been written to disk
|
||||
@VisibleForTesting
|
||||
NotificationHistory mBuffer;
|
||||
|
||||
public NotificationHistoryDatabase(File dir) {
|
||||
mCurrentVersion = DEFAULT_CURRENT_VERSION;
|
||||
mVersionFile = new File(dir, "version");
|
||||
mHistoryDir = new File(dir, "history");
|
||||
mHistoryFiles = new LinkedList<>();
|
||||
mCal = new GregorianCalendar();
|
||||
mBuffer = new NotificationHistory();
|
||||
mWriteBufferRunnable = new WriteBufferRunnable();
|
||||
}
|
||||
|
||||
public void init(Handler fileWriteHandler) {
|
||||
synchronized (mLock) {
|
||||
mFileWriteHandler = fileWriteHandler;
|
||||
|
||||
try {
|
||||
mHistoryDir.mkdir();
|
||||
mVersionFile.createNewFile();
|
||||
} catch (Exception e) {
|
||||
Slog.e(TAG, "could not create needed files", e);
|
||||
}
|
||||
|
||||
checkVersionAndBuildLocked();
|
||||
indexFilesLocked();
|
||||
prune(HISTORY_RETENTION_DAYS, System.currentTimeMillis());
|
||||
}
|
||||
}
|
||||
|
||||
private void indexFilesLocked() {
|
||||
mHistoryFiles.clear();
|
||||
final File[] files = mHistoryDir.listFiles();
|
||||
if (files == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort with newest files first
|
||||
Arrays.sort(files, (lhs, rhs) -> Long.compare(rhs.lastModified(), lhs.lastModified()));
|
||||
|
||||
for (File file : files) {
|
||||
mHistoryFiles.addLast(new AtomicFile(file));
|
||||
}
|
||||
}
|
||||
|
||||
private void checkVersionAndBuildLocked() {
|
||||
int version;
|
||||
try (BufferedReader reader = new BufferedReader(new FileReader(mVersionFile))) {
|
||||
version = Integer.parseInt(reader.readLine());
|
||||
} catch (NumberFormatException | IOException e) {
|
||||
version = 0;
|
||||
}
|
||||
|
||||
if (version != mCurrentVersion && mVersionFile.exists()) {
|
||||
try (BufferedWriter writer = new BufferedWriter(new FileWriter(mVersionFile))) {
|
||||
writer.write(Integer.toString(mCurrentVersion));
|
||||
writer.write("\n");
|
||||
writer.flush();
|
||||
} catch (IOException e) {
|
||||
Slog.e(TAG, "Failed to write new version");
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void forceWriteToDisk() {
|
||||
if (!mFileWriteHandler.hasCallbacks(mWriteBufferRunnable)) {
|
||||
mFileWriteHandler.post(mWriteBufferRunnable);
|
||||
}
|
||||
}
|
||||
|
||||
void onPackageRemoved(String packageName) {
|
||||
RemovePackageRunnable rpr = new RemovePackageRunnable(packageName);
|
||||
mFileWriteHandler.post(rpr);
|
||||
}
|
||||
|
||||
public void addNotification(final HistoricalNotification notification) {
|
||||
synchronized (mLock) {
|
||||
mBuffer.addNotificationToWrite(notification);
|
||||
// Each time we have new history to write to disk, schedule a write in [interval] ms
|
||||
if (mBuffer.getHistoryCount() == 1) {
|
||||
mFileWriteHandler.postDelayed(mWriteBufferRunnable, WRITE_BUFFER_INTERVAL_MS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public NotificationHistory readNotificationHistory() {
|
||||
synchronized (mLock) {
|
||||
NotificationHistory notifications = new NotificationHistory();
|
||||
|
||||
for (AtomicFile file : mHistoryFiles) {
|
||||
try {
|
||||
readLocked(
|
||||
file, notifications, new NotificationHistoryFilter.Builder().build());
|
||||
} catch (Exception e) {
|
||||
Slog.e(TAG, "error reading " + file.getBaseFile().getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
return notifications;
|
||||
}
|
||||
}
|
||||
|
||||
public NotificationHistory readNotificationHistory(String packageName, String channelId,
|
||||
int maxNotifications) {
|
||||
synchronized (mLock) {
|
||||
NotificationHistory notifications = new NotificationHistory();
|
||||
|
||||
for (AtomicFile file : mHistoryFiles) {
|
||||
try {
|
||||
readLocked(file, notifications,
|
||||
new NotificationHistoryFilter.Builder()
|
||||
.setPackage(packageName)
|
||||
.setChannel(packageName, channelId)
|
||||
.setMaxNotifications(maxNotifications)
|
||||
.build());
|
||||
if (maxNotifications == notifications.getHistoryCount()) {
|
||||
// No need to read any more files
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Slog.e(TAG, "error reading " + file.getBaseFile().getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
return notifications;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any files that are too old.
|
||||
*/
|
||||
public void prune(final int retentionDays, final long currentTimeMillis) {
|
||||
synchronized (mLock) {
|
||||
mCal.setTimeInMillis(currentTimeMillis);
|
||||
mCal.add(Calendar.DATE, -1 * retentionDays);
|
||||
|
||||
while (!mHistoryFiles.isEmpty()) {
|
||||
final AtomicFile currentOldestFile = mHistoryFiles.getLast();
|
||||
final long age = currentTimeMillis
|
||||
- currentOldestFile.getBaseFile().lastModified();
|
||||
if (age > mCal.getTimeInMillis()) {
|
||||
if (DEBUG) {
|
||||
Slog.d(TAG, "Removed " + currentOldestFile.getBaseFile().getName());
|
||||
}
|
||||
currentOldestFile.delete();
|
||||
mHistoryFiles.removeLast();
|
||||
} else {
|
||||
// all remaining files are newer than the cut off
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void writeLocked(AtomicFile file, NotificationHistory notifications)
|
||||
throws IOException {
|
||||
FileOutputStream fos = file.startWrite();
|
||||
try {
|
||||
NotificationHistoryProtoHelper.write(fos, notifications, mCurrentVersion);
|
||||
file.finishWrite(fos);
|
||||
fos = null;
|
||||
} finally {
|
||||
// When fos is null (successful write), this will no-op
|
||||
file.failWrite(fos);
|
||||
}
|
||||
}
|
||||
|
||||
private static void readLocked(AtomicFile file, NotificationHistory notificationsOut,
|
||||
NotificationHistoryFilter filter) throws IOException {
|
||||
try (FileInputStream in = file.openRead()) {
|
||||
NotificationHistoryProtoHelper.read(in, notificationsOut, filter);
|
||||
} catch (FileNotFoundException e) {
|
||||
Slog.e(TAG, "Cannot file " + file.getBaseFile().getName(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private final class WriteBufferRunnable implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
if (DEBUG) Slog.d(TAG, "WriteBufferRunnable");
|
||||
synchronized (mLock) {
|
||||
final AtomicFile latestNotificationsFiles = new AtomicFile(
|
||||
new File(mHistoryDir, String.valueOf(System.currentTimeMillis())));
|
||||
try {
|
||||
writeLocked(latestNotificationsFiles, mBuffer);
|
||||
mHistoryFiles.addFirst(latestNotificationsFiles);
|
||||
mBuffer = new NotificationHistory();
|
||||
} catch (IOException e) {
|
||||
Slog.e(TAG, "Failed to write buffer to disk. not flushing buffer", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class RemovePackageRunnable implements Runnable {
|
||||
private String mPkg;
|
||||
|
||||
public RemovePackageRunnable(String pkg) {
|
||||
mPkg = pkg;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (DEBUG) Slog.d(TAG, "RemovePackageRunnable");
|
||||
synchronized (mLock) {
|
||||
// Remove packageName entries from pending history
|
||||
mBuffer.removeNotificationsFromWrite(mPkg);
|
||||
|
||||
// Remove packageName entries from files on disk, and rewrite them to disk
|
||||
// Since we sort by modified date, we have to update the files oldest to newest to
|
||||
// maintain the original ordering
|
||||
Iterator<AtomicFile> historyFileItr = mHistoryFiles.descendingIterator();
|
||||
while (historyFileItr.hasNext()) {
|
||||
final AtomicFile af = historyFileItr.next();
|
||||
try {
|
||||
final NotificationHistory notifications = new NotificationHistory();
|
||||
readLocked(af, notifications,
|
||||
new NotificationHistoryFilter.Builder().build());
|
||||
notifications.removeNotificationsFromWrite(mPkg);
|
||||
writeLocked(af, notifications);
|
||||
} catch (Exception e) {
|
||||
Slog.e(TAG, "Cannot clean up file on pkg removal "
|
||||
+ af.getBaseFile().getName(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* 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 com.android.server.notification;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.app.NotificationHistory;
|
||||
import android.app.NotificationHistory.HistoricalNotification;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.internal.util.Preconditions;
|
||||
|
||||
public final class NotificationHistoryFilter {
|
||||
private String mPackage;
|
||||
private String mChannel;
|
||||
private int mNotificationCount;
|
||||
|
||||
private NotificationHistoryFilter() {}
|
||||
|
||||
public String getPackage() {
|
||||
return mPackage;
|
||||
}
|
||||
|
||||
public String getChannel() {
|
||||
return mChannel;
|
||||
}
|
||||
|
||||
public int getMaxNotifications() {
|
||||
return mNotificationCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether any of the filtering conditions are set
|
||||
*/
|
||||
public boolean isFiltering() {
|
||||
return getPackage() != null || getChannel() != null
|
||||
|| mNotificationCount < Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this notification passes the package and channel name filter, false
|
||||
* otherwise.
|
||||
*/
|
||||
public boolean matchesPackageAndChannelFilter(HistoricalNotification notification) {
|
||||
if (!TextUtils.isEmpty(getPackage())) {
|
||||
if (!getPackage().equals(notification.getPackage())) {
|
||||
return false;
|
||||
} else {
|
||||
if (!TextUtils.isEmpty(getChannel())
|
||||
&& !getChannel().equals(notification.getChannelId())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the NotificationHistory can accept another notification.
|
||||
*/
|
||||
public boolean matchesCountFilter(NotificationHistory notifications) {
|
||||
return notifications.getHistoryCount() < mNotificationCount;
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private String mPackage = null;
|
||||
private String mChannel = null;
|
||||
private int mNotificationCount = Integer.MAX_VALUE;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public Builder() {}
|
||||
|
||||
/**
|
||||
* Sets a package name filter
|
||||
*/
|
||||
public Builder setPackage(String aPackage) {
|
||||
mPackage = aPackage;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a channel name filter. Only valid if there is also a package name filter
|
||||
*/
|
||||
public Builder setChannel(String pkg, String channel) {
|
||||
setPackage(pkg);
|
||||
mChannel = channel;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the max historical notifications
|
||||
*/
|
||||
public Builder setMaxNotifications(int notificationCount) {
|
||||
mNotificationCount = notificationCount;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a NotificationHistoryFilter
|
||||
*/
|
||||
public NotificationHistoryFilter build() {
|
||||
NotificationHistoryFilter filter = new NotificationHistoryFilter();
|
||||
filter.mPackage = mPackage;
|
||||
filter.mChannel = mChannel;
|
||||
filter.mNotificationCount = mNotificationCount;
|
||||
return filter;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
/*
|
||||
* 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 com.android.server.notification;
|
||||
|
||||
import android.app.NotificationHistory;
|
||||
import android.app.NotificationHistory.HistoricalNotification;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.util.Slog;
|
||||
import android.util.proto.ProtoInputStream;
|
||||
import android.util.proto.ProtoOutputStream;
|
||||
|
||||
import com.android.server.notification.NotificationHistoryProto.Notification;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Notification history reader/writer for Protocol Buffer format
|
||||
*/
|
||||
final class NotificationHistoryProtoHelper {
|
||||
private static final String TAG = "NotifHistoryProto";
|
||||
|
||||
// Static-only utility class.
|
||||
private NotificationHistoryProtoHelper() {}
|
||||
|
||||
private static List<String> readStringPool(ProtoInputStream proto) throws IOException {
|
||||
final long token = proto.start(NotificationHistoryProto.STRING_POOL);
|
||||
List<String> stringPool;
|
||||
if (proto.nextField(NotificationHistoryProto.StringPool.SIZE)) {
|
||||
stringPool = new ArrayList(proto.readInt(NotificationHistoryProto.StringPool.SIZE));
|
||||
} else {
|
||||
stringPool = new ArrayList();
|
||||
}
|
||||
while (proto.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
|
||||
switch (proto.getFieldNumber()) {
|
||||
case (int) NotificationHistoryProto.StringPool.STRINGS:
|
||||
stringPool.add(proto.readString(NotificationHistoryProto.StringPool.STRINGS));
|
||||
break;
|
||||
}
|
||||
}
|
||||
proto.end(token);
|
||||
return stringPool;
|
||||
}
|
||||
|
||||
private static void writeStringPool(ProtoOutputStream proto,
|
||||
final NotificationHistory notifications) {
|
||||
final long token = proto.start(NotificationHistoryProto.STRING_POOL);
|
||||
final String[] pooledStrings = notifications.getPooledStringsToWrite();
|
||||
proto.write(NotificationHistoryProto.StringPool.SIZE, pooledStrings.length);
|
||||
for (int i = 0; i < pooledStrings.length; i++) {
|
||||
proto.write(NotificationHistoryProto.StringPool.STRINGS, pooledStrings[i]);
|
||||
}
|
||||
proto.end(token);
|
||||
}
|
||||
|
||||
private static void readNotification(ProtoInputStream proto, List<String> stringPool,
|
||||
NotificationHistory notifications, NotificationHistoryFilter filter)
|
||||
throws IOException {
|
||||
final long token = proto.start(NotificationHistoryProto.NOTIFICATION);
|
||||
try {
|
||||
HistoricalNotification notification = readNotification(proto, stringPool);
|
||||
if (filter.matchesPackageAndChannelFilter(notification)
|
||||
&& filter.matchesCountFilter(notifications)) {
|
||||
notifications.addNotificationToWrite(notification);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Slog.e(TAG, "Error reading notification", e);
|
||||
} finally {
|
||||
proto.end(token);
|
||||
}
|
||||
}
|
||||
|
||||
private static HistoricalNotification readNotification(ProtoInputStream parser,
|
||||
List<String> stringPool) throws IOException {
|
||||
final HistoricalNotification.Builder notification = new HistoricalNotification.Builder();
|
||||
String pkg = null;
|
||||
while (true) {
|
||||
switch (parser.nextField()) {
|
||||
case (int) NotificationHistoryProto.Notification.PACKAGE:
|
||||
pkg = parser.readString(Notification.PACKAGE);
|
||||
notification.setPackage(pkg);
|
||||
stringPool.add(pkg);
|
||||
break;
|
||||
case (int) Notification.PACKAGE_INDEX:
|
||||
pkg = stringPool.get(parser.readInt(Notification.PACKAGE_INDEX) - 1);
|
||||
notification.setPackage(pkg);
|
||||
break;
|
||||
case (int) Notification.CHANNEL_NAME:
|
||||
String channelName = parser.readString(Notification.CHANNEL_NAME);
|
||||
notification.setChannelName(channelName);
|
||||
stringPool.add(channelName);
|
||||
break;
|
||||
case (int) Notification.CHANNEL_NAME_INDEX:
|
||||
notification.setChannelName(stringPool.get(parser.readInt(
|
||||
Notification.CHANNEL_NAME_INDEX) - 1));
|
||||
break;
|
||||
case (int) Notification.CHANNEL_ID:
|
||||
String channelId = parser.readString(Notification.CHANNEL_ID);
|
||||
notification.setChannelId(channelId);
|
||||
stringPool.add(channelId);
|
||||
break;
|
||||
case (int) Notification.CHANNEL_ID_INDEX:
|
||||
notification.setChannelId(stringPool.get(parser.readInt(
|
||||
Notification.CHANNEL_ID_INDEX) - 1));
|
||||
break;
|
||||
case (int) Notification.UID:
|
||||
notification.setUid(parser.readInt(Notification.UID));
|
||||
break;
|
||||
case (int) Notification.USER_ID:
|
||||
notification.setUserId(parser.readInt(Notification.USER_ID));
|
||||
break;
|
||||
case (int) Notification.POSTED_TIME_MS:
|
||||
notification.setPostedTimeMs(parser.readLong(Notification.POSTED_TIME_MS));
|
||||
break;
|
||||
case (int) Notification.TITLE:
|
||||
notification.setTitle(parser.readString(Notification.TITLE));
|
||||
break;
|
||||
case (int) Notification.TEXT:
|
||||
notification.setText(parser.readString(Notification.TEXT));
|
||||
break;
|
||||
case (int) Notification.ICON:
|
||||
final long iconToken = parser.start(Notification.ICON);
|
||||
loadIcon(parser, notification, pkg);
|
||||
parser.end(iconToken);
|
||||
break;
|
||||
case ProtoInputStream.NO_MORE_FIELDS:
|
||||
return notification.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void loadIcon(ProtoInputStream parser,
|
||||
HistoricalNotification.Builder notification, String pkg) throws IOException {
|
||||
int iconType = Notification.TYPE_UNKNOWN;
|
||||
String imageBitmapFileName = null;
|
||||
int imageResourceId = Resources.ID_NULL;
|
||||
String imageResourceIdPackage = null;
|
||||
byte[] imageByteData = null;
|
||||
int imageByteDataLength = 0;
|
||||
int imageByteDataOffset = 0;
|
||||
String imageUri = null;
|
||||
|
||||
while (true) {
|
||||
switch (parser.nextField()) {
|
||||
case (int) Notification.Icon.IMAGE_TYPE:
|
||||
iconType = parser.readInt(Notification.Icon.IMAGE_TYPE);
|
||||
break;
|
||||
case (int) Notification.Icon.IMAGE_DATA:
|
||||
imageByteData = parser.readBytes(Notification.Icon.IMAGE_DATA);
|
||||
break;
|
||||
case (int) Notification.Icon.IMAGE_DATA_LENGTH:
|
||||
imageByteDataLength = parser.readInt(Notification.Icon.IMAGE_DATA_LENGTH);
|
||||
break;
|
||||
case (int) Notification.Icon.IMAGE_DATA_OFFSET:
|
||||
imageByteDataOffset = parser.readInt(Notification.Icon.IMAGE_DATA_OFFSET);
|
||||
break;
|
||||
case (int) Notification.Icon.IMAGE_BITMAP_FILENAME:
|
||||
imageBitmapFileName = parser.readString(
|
||||
Notification.Icon.IMAGE_BITMAP_FILENAME);
|
||||
break;
|
||||
case (int) Notification.Icon.IMAGE_RESOURCE_ID:
|
||||
imageResourceId = parser.readInt(Notification.Icon.IMAGE_RESOURCE_ID);
|
||||
break;
|
||||
case (int) Notification.Icon.IMAGE_RESOURCE_ID_PACKAGE:
|
||||
imageResourceIdPackage = parser.readString(
|
||||
Notification.Icon.IMAGE_RESOURCE_ID_PACKAGE);
|
||||
break;
|
||||
case (int) Notification.Icon.IMAGE_URI:
|
||||
imageUri = parser.readString(Notification.Icon.IMAGE_URI);
|
||||
break;
|
||||
case ProtoInputStream.NO_MORE_FIELDS:
|
||||
if (iconType == Icon.TYPE_DATA) {
|
||||
|
||||
if (imageByteData != null) {
|
||||
notification.setIcon(Icon.createWithData(
|
||||
imageByteData, imageByteDataOffset, imageByteDataLength));
|
||||
}
|
||||
} else if (iconType == Icon.TYPE_RESOURCE) {
|
||||
if (imageResourceId != Resources.ID_NULL) {
|
||||
notification.setIcon(Icon.createWithResource(
|
||||
imageResourceIdPackage != null
|
||||
? imageResourceIdPackage
|
||||
: pkg,
|
||||
imageResourceId));
|
||||
}
|
||||
} else if (iconType == Icon.TYPE_URI) {
|
||||
if (imageUri != null) {
|
||||
notification.setIcon(Icon.createWithContentUri(imageUri));
|
||||
}
|
||||
} else if (iconType == Icon.TYPE_BITMAP) {
|
||||
// TODO: read file from disk
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeIcon(ProtoOutputStream proto, HistoricalNotification notification) {
|
||||
final long token = proto.start(Notification.ICON);
|
||||
|
||||
proto.write(Notification.Icon.IMAGE_TYPE, notification.getIcon().getType());
|
||||
switch (notification.getIcon().getType()) {
|
||||
case Icon.TYPE_DATA:
|
||||
proto.write(Notification.Icon.IMAGE_DATA, notification.getIcon().getDataBytes());
|
||||
proto.write(Notification.Icon.IMAGE_DATA_LENGTH,
|
||||
notification.getIcon().getDataLength());
|
||||
proto.write(Notification.Icon.IMAGE_DATA_OFFSET,
|
||||
notification.getIcon().getDataOffset());
|
||||
break;
|
||||
case Icon.TYPE_RESOURCE:
|
||||
proto.write(Notification.Icon.IMAGE_RESOURCE_ID, notification.getIcon().getResId());
|
||||
if (!notification.getPackage().equals(notification.getIcon().getResPackage())) {
|
||||
proto.write(Notification.Icon.IMAGE_RESOURCE_ID_PACKAGE,
|
||||
notification.getIcon().getResPackage());
|
||||
}
|
||||
break;
|
||||
case Icon.TYPE_URI:
|
||||
proto.write(Notification.Icon.IMAGE_URI, notification.getIcon().getUriString());
|
||||
break;
|
||||
case Icon.TYPE_BITMAP:
|
||||
// TODO: write file to disk
|
||||
break;
|
||||
}
|
||||
|
||||
proto.end(token);
|
||||
}
|
||||
|
||||
private static void writeNotification(ProtoOutputStream proto,
|
||||
final String[] stringPool, final HistoricalNotification notification) {
|
||||
final long token = proto.start(NotificationHistoryProto.NOTIFICATION);
|
||||
final int packageIndex = Arrays.binarySearch(stringPool, notification.getPackage());
|
||||
if (packageIndex >= 0) {
|
||||
proto.write(Notification.PACKAGE_INDEX, packageIndex + 1);
|
||||
} else {
|
||||
// Package not in Stringpool for some reason, write full string instead
|
||||
Slog.w(TAG, "notification package name (" + notification.getPackage()
|
||||
+ ") not found in string cache");
|
||||
proto.write(Notification.PACKAGE, notification.getPackage());
|
||||
}
|
||||
final int channelNameIndex = Arrays.binarySearch(stringPool, notification.getChannelName());
|
||||
if (channelNameIndex >= 0) {
|
||||
proto.write(Notification.CHANNEL_NAME_INDEX, channelNameIndex + 1);
|
||||
} else {
|
||||
Slog.w(TAG, "notification channel name (" + notification.getChannelName()
|
||||
+ ") not found in string cache");
|
||||
proto.write(Notification.CHANNEL_NAME, notification.getChannelName());
|
||||
}
|
||||
final int channelIdIndex = Arrays.binarySearch(stringPool, notification.getChannelId());
|
||||
if (channelIdIndex >= 0) {
|
||||
proto.write(Notification.CHANNEL_ID_INDEX, channelIdIndex + 1);
|
||||
} else {
|
||||
Slog.w(TAG, "notification channel id (" + notification.getChannelId()
|
||||
+ ") not found in string cache");
|
||||
proto.write(Notification.CHANNEL_ID, notification.getChannelId());
|
||||
}
|
||||
proto.write(Notification.UID, notification.getUid());
|
||||
proto.write(Notification.USER_ID, notification.getUserId());
|
||||
proto.write(Notification.POSTED_TIME_MS, notification.getPostedTimeMs());
|
||||
proto.write(Notification.TITLE, notification.getTitle());
|
||||
proto.write(Notification.TEXT, notification.getText());
|
||||
writeIcon(proto, notification);
|
||||
proto.end(token);
|
||||
}
|
||||
|
||||
public static void read(InputStream in, NotificationHistory notifications,
|
||||
NotificationHistoryFilter filter) throws IOException {
|
||||
final ProtoInputStream proto = new ProtoInputStream(in);
|
||||
List<String> stringPool = new ArrayList<>();
|
||||
while (true) {
|
||||
switch (proto.nextField()) {
|
||||
case (int) NotificationHistoryProto.STRING_POOL:
|
||||
stringPool = readStringPool(proto);
|
||||
break;
|
||||
case (int) NotificationHistoryProto.NOTIFICATION:
|
||||
readNotification(proto, stringPool, notifications, filter);
|
||||
break;
|
||||
case ProtoInputStream.NO_MORE_FIELDS:
|
||||
if (filter.isFiltering()) {
|
||||
notifications.poolStringsFromNotifications();
|
||||
} else {
|
||||
notifications.addPooledStrings(stringPool);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void write(OutputStream out, NotificationHistory notifications, int version) {
|
||||
final ProtoOutputStream proto = new ProtoOutputStream(out);
|
||||
proto.write(NotificationHistoryProto.MAJOR_VERSION, version);
|
||||
// String pool should be written before the history itself
|
||||
writeStringPool(proto, notifications);
|
||||
|
||||
List<HistoricalNotification> notificationsToWrite = notifications.getNotificationsToWrite();
|
||||
final int count = notificationsToWrite.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
writeNotification(proto, notifications.getPooledStringsToWrite(),
|
||||
notificationsToWrite.get(i));
|
||||
}
|
||||
|
||||
proto.flush();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* 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 com.android.server.notification;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.app.NotificationHistory.HistoricalNotification;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Handler;
|
||||
import android.util.AtomicFile;
|
||||
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import com.android.server.UiServiceTestCase;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class NotificationHistoryDatabaseTest extends UiServiceTestCase {
|
||||
|
||||
File mRootDir;
|
||||
@Mock
|
||||
Handler mFileWriteHandler;
|
||||
|
||||
NotificationHistoryDatabase mDataBase;
|
||||
|
||||
private HistoricalNotification getHistoricalNotification(int index) {
|
||||
return getHistoricalNotification("package" + index, index);
|
||||
}
|
||||
|
||||
private HistoricalNotification getHistoricalNotification(String packageName, int index) {
|
||||
String expectedChannelName = "channelName" + index;
|
||||
String expectedChannelId = "channelId" + index;
|
||||
int expectedUid = 1123456 + index;
|
||||
int expectedUserId = 11 + index;
|
||||
long expectedPostTime = 987654321 + index;
|
||||
String expectedTitle = "title" + index;
|
||||
String expectedText = "text" + index;
|
||||
Icon expectedIcon = Icon.createWithResource(InstrumentationRegistry.getContext(),
|
||||
index);
|
||||
|
||||
return new HistoricalNotification.Builder()
|
||||
.setPackage(packageName)
|
||||
.setChannelName(expectedChannelName)
|
||||
.setChannelId(expectedChannelId)
|
||||
.setUid(expectedUid)
|
||||
.setUserId(expectedUserId)
|
||||
.setPostedTimeMs(expectedPostTime)
|
||||
.setTitle(expectedTitle)
|
||||
.setText(expectedText)
|
||||
.setIcon(expectedIcon)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
mRootDir = new File(mContext.getFilesDir(), "NotificationHistoryDatabaseTest");
|
||||
|
||||
mDataBase = new NotificationHistoryDatabase(mRootDir);
|
||||
mDataBase.init(mFileWriteHandler);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPrune() {
|
||||
int retainDays = 1;
|
||||
for (long i = 10; i >= 5; i--) {
|
||||
File file = mock(File.class);
|
||||
when(file.lastModified()).thenReturn(i);
|
||||
AtomicFile af = new AtomicFile(file);
|
||||
mDataBase.mHistoryFiles.addLast(af);
|
||||
}
|
||||
GregorianCalendar cal = new GregorianCalendar();
|
||||
cal.setTimeInMillis(5);
|
||||
cal.add(Calendar.DATE, -1 * retainDays);
|
||||
for (int i = 5; i >= 0; i--) {
|
||||
File file = mock(File.class);
|
||||
when(file.lastModified()).thenReturn(cal.getTimeInMillis() - i);
|
||||
AtomicFile af = new AtomicFile(file);
|
||||
mDataBase.mHistoryFiles.addLast(af);
|
||||
}
|
||||
mDataBase.prune(retainDays, 10);
|
||||
|
||||
for (AtomicFile file : mDataBase.mHistoryFiles) {
|
||||
assertThat(file.getBaseFile().lastModified() > 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnPackageRemove_posts() {
|
||||
mDataBase.onPackageRemoved("test");
|
||||
verify(mFileWriteHandler, times(1)).post(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testForceWriteToDisk() {
|
||||
mDataBase.forceWriteToDisk();
|
||||
verify(mFileWriteHandler, times(1)).post(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnlyOneWriteRunnableInQueue() {
|
||||
when(mFileWriteHandler.hasCallbacks(any())).thenReturn(true);
|
||||
mDataBase.forceWriteToDisk();
|
||||
verify(mFileWriteHandler, never()).post(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddNotification() {
|
||||
HistoricalNotification n = getHistoricalNotification(1);
|
||||
HistoricalNotification n2 = getHistoricalNotification(2);
|
||||
|
||||
mDataBase.addNotification(n);
|
||||
assertThat(mDataBase.mBuffer.getNotificationsToWrite()).contains(n);
|
||||
verify(mFileWriteHandler, times(1)).postDelayed(any(), anyLong());
|
||||
|
||||
// second add should not trigger another write
|
||||
mDataBase.addNotification(n2);
|
||||
assertThat(mDataBase.mBuffer.getNotificationsToWrite()).contains(n2);
|
||||
verify(mFileWriteHandler, times(1)).postDelayed(any(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadNotificationHistory_readsAllFiles() throws Exception {
|
||||
for (long i = 10; i >= 5; i--) {
|
||||
AtomicFile af = mock(AtomicFile.class);
|
||||
mDataBase.mHistoryFiles.addLast(af);
|
||||
}
|
||||
|
||||
mDataBase.readNotificationHistory();
|
||||
|
||||
for (AtomicFile file : mDataBase.mHistoryFiles) {
|
||||
verify(file, times(1)).openRead();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadNotificationHistory_withNumFilterDoesNotReadExtraFiles() throws Exception {
|
||||
AtomicFile af = mock(AtomicFile.class);
|
||||
when(af.getBaseFile()).thenReturn(new File(mRootDir, "af"));
|
||||
mDataBase.mHistoryFiles.addLast(af);
|
||||
|
||||
AtomicFile af2 = mock(AtomicFile.class);
|
||||
when(af2.getBaseFile()).thenReturn(new File(mRootDir, "af2"));
|
||||
mDataBase.mHistoryFiles.addLast(af2);
|
||||
|
||||
mDataBase.readNotificationHistory(null, null, 0);
|
||||
|
||||
verify(af, times(1)).openRead();
|
||||
verify(af2, never()).openRead();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* 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 com.android.server.notification;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.app.NotificationHistory;
|
||||
import android.app.NotificationHistory.HistoricalNotification;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.test.suitebuilder.annotation.SmallTest;
|
||||
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import com.android.server.UiServiceTestCase;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class NotificationHistoryFilterTest extends UiServiceTestCase {
|
||||
|
||||
private HistoricalNotification getHistoricalNotification(int index) {
|
||||
return getHistoricalNotification("package" + index, "channelId" + index, index);
|
||||
}
|
||||
private HistoricalNotification getHistoricalNotification(String pkg, int index) {
|
||||
return getHistoricalNotification(pkg, "channelId" + index, index);
|
||||
}
|
||||
|
||||
private HistoricalNotification getHistoricalNotification(String packageName, String channelId,
|
||||
int index) {
|
||||
String expectedChannelName = "channelName" + index;
|
||||
int expectedUid = 1123456 + index;
|
||||
int expectedUserId = 11 + index;
|
||||
long expectedPostTime = 987654321 + index;
|
||||
String expectedTitle = "title" + index;
|
||||
String expectedText = "text" + index;
|
||||
Icon expectedIcon = Icon.createWithResource(InstrumentationRegistry.getContext(),
|
||||
index);
|
||||
|
||||
return new HistoricalNotification.Builder()
|
||||
.setPackage(packageName)
|
||||
.setChannelName(expectedChannelName)
|
||||
.setChannelId(channelId)
|
||||
.setUid(expectedUid)
|
||||
.setUserId(expectedUserId)
|
||||
.setPostedTimeMs(expectedPostTime)
|
||||
.setTitle(expectedTitle)
|
||||
.setText(expectedText)
|
||||
.setIcon(expectedIcon)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuilder() {
|
||||
NotificationHistoryFilter filter = new NotificationHistoryFilter.Builder()
|
||||
.setChannel("pkg", "channel")
|
||||
.setMaxNotifications(3)
|
||||
.build();
|
||||
|
||||
assertThat(filter.getPackage()).isEqualTo("pkg");
|
||||
assertThat(filter.getChannel()).isEqualTo("channel");
|
||||
assertThat(filter.getMaxNotifications()).isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatchesCountFilter() {
|
||||
NotificationHistoryFilter filter = new NotificationHistoryFilter.Builder()
|
||||
.setMaxNotifications(3)
|
||||
.build();
|
||||
|
||||
NotificationHistory history = new NotificationHistory();
|
||||
assertThat(filter.matchesCountFilter(history)).isTrue();
|
||||
history.addNotificationToWrite(getHistoricalNotification(1));
|
||||
assertThat(filter.matchesCountFilter(history)).isTrue();
|
||||
history.addNotificationToWrite(getHistoricalNotification(2));
|
||||
assertThat(filter.matchesCountFilter(history)).isTrue();
|
||||
history.addNotificationToWrite(getHistoricalNotification(3));
|
||||
assertThat(filter.matchesCountFilter(history)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatchesCountFilter_noCountFilter() {
|
||||
NotificationHistoryFilter filter = new NotificationHistoryFilter.Builder()
|
||||
.build();
|
||||
|
||||
NotificationHistory history = new NotificationHistory();
|
||||
assertThat(filter.matchesCountFilter(history)).isTrue();
|
||||
history.addNotificationToWrite(getHistoricalNotification(1));
|
||||
assertThat(filter.matchesCountFilter(history)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatchesPackageAndChannelFilter_pkgOnly() {
|
||||
NotificationHistoryFilter filter = new NotificationHistoryFilter.Builder()
|
||||
.setPackage("pkg")
|
||||
.build();
|
||||
|
||||
HistoricalNotification hnMatches = getHistoricalNotification("pkg", 1);
|
||||
assertThat(filter.matchesPackageAndChannelFilter(hnMatches)).isTrue();
|
||||
HistoricalNotification hnMatches2 = getHistoricalNotification("pkg", 2);
|
||||
assertThat(filter.matchesPackageAndChannelFilter(hnMatches2)).isTrue();
|
||||
|
||||
HistoricalNotification hnNoMatch = getHistoricalNotification("pkg2", 2);
|
||||
assertThat(filter.matchesPackageAndChannelFilter(hnNoMatch)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatchesPackageAndChannelFilter_channelAlso() {
|
||||
NotificationHistoryFilter filter = new NotificationHistoryFilter.Builder()
|
||||
.setChannel("pkg", "channel")
|
||||
.build();
|
||||
|
||||
HistoricalNotification hn1 = getHistoricalNotification("pkg", 1);
|
||||
assertThat(filter.matchesPackageAndChannelFilter(hn1)).isFalse();
|
||||
|
||||
HistoricalNotification hn2 = getHistoricalNotification("pkg", "channel", 1);
|
||||
assertThat(filter.matchesPackageAndChannelFilter(hn2)).isTrue();
|
||||
|
||||
HistoricalNotification hn3 = getHistoricalNotification("pkg2", "channel", 1);
|
||||
assertThat(filter.matchesPackageAndChannelFilter(hn3)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsFiltering() {
|
||||
NotificationHistoryFilter filter = new NotificationHistoryFilter.Builder()
|
||||
.build();
|
||||
assertThat(filter.isFiltering()).isFalse();
|
||||
|
||||
filter = new NotificationHistoryFilter.Builder()
|
||||
.setPackage("pkg")
|
||||
.build();
|
||||
assertThat(filter.isFiltering()).isTrue();
|
||||
|
||||
filter = new NotificationHistoryFilter.Builder()
|
||||
.setChannel("pkg", "channel")
|
||||
.build();
|
||||
assertThat(filter.isFiltering()).isTrue();
|
||||
|
||||
filter = new NotificationHistoryFilter.Builder()
|
||||
.setMaxNotifications(5)
|
||||
.build();
|
||||
assertThat(filter.isFiltering()).isTrue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
/*
|
||||
* 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 com.android.server.notification;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.app.NotificationHistory;
|
||||
import android.app.NotificationHistory.HistoricalNotification;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.test.suitebuilder.annotation.SmallTest;
|
||||
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import com.android.server.UiServiceTestCase;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class NotificationHistoryProtoHelperTest extends UiServiceTestCase {
|
||||
|
||||
private HistoricalNotification getHistoricalNotification(int index) {
|
||||
return getHistoricalNotification("package" + index, index);
|
||||
}
|
||||
|
||||
private HistoricalNotification getHistoricalNotification(String packageName, int index) {
|
||||
String expectedChannelName = "channelName" + index;
|
||||
String expectedChannelId = "channelId" + index;
|
||||
int expectedUid = 1123456 + index;
|
||||
int expectedUserId = 11 + index;
|
||||
long expectedPostTime = 987654321 + index;
|
||||
String expectedTitle = "title" + index;
|
||||
String expectedText = "text" + index;
|
||||
Icon expectedIcon = Icon.createWithResource(InstrumentationRegistry.getContext(),
|
||||
index);
|
||||
|
||||
return new HistoricalNotification.Builder()
|
||||
.setPackage(packageName)
|
||||
.setChannelName(expectedChannelName)
|
||||
.setChannelId(expectedChannelId)
|
||||
.setUid(expectedUid)
|
||||
.setUserId(expectedUserId)
|
||||
.setPostedTimeMs(expectedPostTime)
|
||||
.setTitle(expectedTitle)
|
||||
.setText(expectedText)
|
||||
.setIcon(expectedIcon)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadWriteNotifications() throws Exception {
|
||||
NotificationHistory history = new NotificationHistory();
|
||||
|
||||
List<HistoricalNotification> expectedEntries = new ArrayList<>();
|
||||
// loops backwards just to maintain the post time newest -> oldest expectation
|
||||
for (int i = 10; i >= 1; i--) {
|
||||
HistoricalNotification n = getHistoricalNotification(i);
|
||||
expectedEntries.add(n);
|
||||
history.addNotificationToWrite(n);
|
||||
}
|
||||
history.poolStringsFromNotifications();
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
NotificationHistoryProtoHelper.write(baos, history, 1);
|
||||
|
||||
NotificationHistory actualHistory = new NotificationHistory();
|
||||
NotificationHistoryProtoHelper.read(
|
||||
new BufferedInputStream(new ByteArrayInputStream(baos.toByteArray())),
|
||||
actualHistory,
|
||||
new NotificationHistoryFilter.Builder().build());
|
||||
|
||||
assertThat(actualHistory.getHistoryCount()).isEqualTo(history.getHistoryCount());
|
||||
assertThat(actualHistory.getNotificationsToWrite())
|
||||
.containsExactlyElementsIn(expectedEntries);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadWriteNotifications_stringFieldsPersistedEvenIfNoPool() throws Exception {
|
||||
NotificationHistory history = new NotificationHistory();
|
||||
|
||||
List<HistoricalNotification> expectedEntries = new ArrayList<>();
|
||||
// loops backwards just to maintain the post time newest -> oldest expectation
|
||||
for (int i = 10; i >= 1; i--) {
|
||||
HistoricalNotification n = getHistoricalNotification(i);
|
||||
expectedEntries.add(n);
|
||||
history.addNotificationToWrite(n);
|
||||
}
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
NotificationHistoryProtoHelper.write(baos, history, 1);
|
||||
|
||||
NotificationHistory actualHistory = new NotificationHistory();
|
||||
NotificationHistoryProtoHelper.read(
|
||||
new BufferedInputStream(new ByteArrayInputStream(baos.toByteArray())),
|
||||
actualHistory,
|
||||
new NotificationHistoryFilter.Builder().build());
|
||||
|
||||
assertThat(actualHistory.getHistoryCount()).isEqualTo(history.getHistoryCount());
|
||||
assertThat(actualHistory.getNotificationsToWrite())
|
||||
.containsExactlyElementsIn(expectedEntries);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadNotificationsWithPkgFilter() throws Exception {
|
||||
NotificationHistory history = new NotificationHistory();
|
||||
|
||||
List<HistoricalNotification> expectedEntries = new ArrayList<>();
|
||||
Set<String> expectedStrings = new HashSet<>();
|
||||
// loops backwards just to maintain the post time newest -> oldest expectation
|
||||
for (int i = 10; i >= 1; i--) {
|
||||
HistoricalNotification n =
|
||||
getHistoricalNotification((i % 2 == 0) ? "pkgEven" : "pkgOdd", i);
|
||||
|
||||
if (i % 2 == 0) {
|
||||
expectedStrings.add(n.getPackage());
|
||||
expectedStrings.add(n.getChannelName());
|
||||
expectedStrings.add(n.getChannelId());
|
||||
expectedEntries.add(n);
|
||||
}
|
||||
history.addNotificationToWrite(n);
|
||||
}
|
||||
history.poolStringsFromNotifications();
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
NotificationHistoryProtoHelper.write(baos, history, 1);
|
||||
|
||||
NotificationHistory actualHistory = new NotificationHistory();
|
||||
|
||||
NotificationHistoryFilter filter = new NotificationHistoryFilter.Builder()
|
||||
.setPackage("pkgEven")
|
||||
.build();
|
||||
NotificationHistoryProtoHelper.read(
|
||||
new BufferedInputStream(new ByteArrayInputStream(baos.toByteArray())),
|
||||
actualHistory,
|
||||
filter);
|
||||
|
||||
assertThat(actualHistory.getNotificationsToWrite())
|
||||
.containsExactlyElementsIn(expectedEntries);
|
||||
assertThat(Arrays.asList(actualHistory.getPooledStringsToWrite()))
|
||||
.containsExactlyElementsIn(expectedStrings);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadNotificationsWithNumberFilter() throws Exception {
|
||||
int maxCount = 3;
|
||||
NotificationHistory history = new NotificationHistory();
|
||||
|
||||
List<HistoricalNotification> expectedEntries = new ArrayList<>();
|
||||
Set<String> expectedStrings = new HashSet<>();
|
||||
for (int i = 1; i < 10; i++) {
|
||||
HistoricalNotification n = getHistoricalNotification(i);
|
||||
|
||||
if (i <= maxCount) {
|
||||
expectedStrings.add(n.getPackage());
|
||||
expectedStrings.add(n.getChannelName());
|
||||
expectedStrings.add(n.getChannelId());
|
||||
expectedEntries.add(n);
|
||||
}
|
||||
history.addNotificationToWrite(n);
|
||||
}
|
||||
history.poolStringsFromNotifications();
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
NotificationHistoryProtoHelper.write(baos, history, 1);
|
||||
|
||||
NotificationHistory actualHistory = new NotificationHistory();
|
||||
|
||||
NotificationHistoryFilter filter = new NotificationHistoryFilter.Builder()
|
||||
.setMaxNotifications(maxCount)
|
||||
.build();
|
||||
NotificationHistoryProtoHelper.read(
|
||||
new BufferedInputStream(new ByteArrayInputStream(baos.toByteArray())),
|
||||
actualHistory,
|
||||
filter);
|
||||
|
||||
assertThat(actualHistory.getNotificationsToWrite())
|
||||
.containsExactlyElementsIn(expectedEntries);
|
||||
assertThat(Arrays.asList(actualHistory.getPooledStringsToWrite()))
|
||||
.containsExactlyElementsIn(expectedStrings);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadNotificationsWithNumberFilter_preExistingNotifs() throws Exception {
|
||||
List<HistoricalNotification> expectedEntries = new ArrayList<>();
|
||||
Set<String> expectedStrings = new HashSet<>();
|
||||
int maxCount = 3;
|
||||
|
||||
NotificationHistory history = new NotificationHistory();
|
||||
HistoricalNotification old1 = getHistoricalNotification(40);
|
||||
history.addNotificationToWrite(old1);
|
||||
expectedEntries.add(old1);
|
||||
|
||||
HistoricalNotification old2 = getHistoricalNotification(50);
|
||||
history.addNotificationToWrite(old2);
|
||||
expectedEntries.add(old2);
|
||||
history.poolStringsFromNotifications();
|
||||
expectedStrings.addAll(Arrays.asList(history.getPooledStringsToWrite()));
|
||||
|
||||
for (int i = 1; i < 10; i++) {
|
||||
HistoricalNotification n = getHistoricalNotification(i);
|
||||
|
||||
if (i <= (maxCount - 2)) {
|
||||
expectedStrings.add(n.getPackage());
|
||||
expectedStrings.add(n.getChannelName());
|
||||
expectedStrings.add(n.getChannelId());
|
||||
expectedEntries.add(n);
|
||||
}
|
||||
history.addNotificationToWrite(n);
|
||||
}
|
||||
history.poolStringsFromNotifications();
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
NotificationHistoryProtoHelper.write(baos, history, 1);
|
||||
|
||||
NotificationHistory actualHistory = new NotificationHistory();
|
||||
|
||||
NotificationHistoryFilter filter = new NotificationHistoryFilter.Builder()
|
||||
.setMaxNotifications(maxCount)
|
||||
.build();
|
||||
NotificationHistoryProtoHelper.read(
|
||||
new BufferedInputStream(new ByteArrayInputStream(baos.toByteArray())),
|
||||
actualHistory,
|
||||
filter);
|
||||
|
||||
assertThat(actualHistory.getNotificationsToWrite())
|
||||
.containsExactlyElementsIn(expectedEntries);
|
||||
assertThat(Arrays.asList(actualHistory.getPooledStringsToWrite()))
|
||||
.containsExactlyElementsIn(expectedStrings);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadMergeIntoExistingHistory() throws Exception {
|
||||
NotificationHistory history = new NotificationHistory();
|
||||
|
||||
List<HistoricalNotification> expectedEntries = new ArrayList<>();
|
||||
Set<String> expectedStrings = new HashSet<>();
|
||||
for (int i = 1; i < 10; i++) {
|
||||
HistoricalNotification n = getHistoricalNotification(i);
|
||||
expectedEntries.add(n);
|
||||
expectedStrings.add(n.getPackage());
|
||||
expectedStrings.add(n.getChannelName());
|
||||
expectedStrings.add(n.getChannelId());
|
||||
history.addNotificationToWrite(n);
|
||||
}
|
||||
history.poolStringsFromNotifications();
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
NotificationHistoryProtoHelper.write(baos, history, 1);
|
||||
|
||||
// set up pre-existing notification history, as though read from a different file
|
||||
NotificationHistory actualHistory = new NotificationHistory();
|
||||
for (int i = 10; i < 20; i++) {
|
||||
HistoricalNotification n = getHistoricalNotification(i);
|
||||
expectedEntries.add(n);
|
||||
expectedStrings.add(n.getPackage());
|
||||
expectedStrings.add(n.getChannelName());
|
||||
expectedStrings.add(n.getChannelId());
|
||||
actualHistory.addNotificationToWrite(n);
|
||||
}
|
||||
actualHistory.poolStringsFromNotifications();
|
||||
|
||||
NotificationHistoryProtoHelper.read(
|
||||
new BufferedInputStream(new ByteArrayInputStream(baos.toByteArray())),
|
||||
actualHistory,
|
||||
new NotificationHistoryFilter.Builder().build());
|
||||
|
||||
// Make sure history contains the original and new entries
|
||||
assertThat(actualHistory.getNotificationsToWrite())
|
||||
.containsExactlyElementsIn(expectedEntries);
|
||||
assertThat(Arrays.asList(actualHistory.getPooledStringsToWrite()))
|
||||
.containsExactlyElementsIn(expectedStrings);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user