Add ability to write and read Notification history.

Bug: 137396965
Test: atest
Change-Id: I6bfdd0f6906dcf9c58b592e03bef335c4920d94d
This commit is contained in:
Julia Reynolds
2019-10-21 11:37:35 -04:00
parent c11e23c036
commit e261db3fce
9 changed files with 2143 additions and 0 deletions

View 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;

View 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];
}
};
}

View File

@@ -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));
}
}
}

View File

@@ -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);
}
}
}
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}