Merge "Separate adapter logic from ChooserActivity and ResolverActivity"
diff --git a/api/system-current.txt b/api/system-current.txt
index adf9e39..e6a3e9b 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -3639,7 +3639,7 @@
public final class MediaRecorder.AudioSource {
field @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_OUTPUT) public static final int ECHO_REFERENCE = 1997; // 0x7cd
field @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_HOTWORD) public static final int HOTWORD = 1999; // 0x7cf
- field public static final int RADIO_TUNER = 1998; // 0x7ce
+ field @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_OUTPUT) public static final int RADIO_TUNER = 1998; // 0x7ce
}
public class PlayerProxy {
diff --git a/core/java/android/app/NotificationHistory.aidl b/core/java/android/app/NotificationHistory.aidl
new file mode 100644
index 0000000..8150e74
--- /dev/null
+++ b/core/java/android/app/NotificationHistory.aidl
@@ -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;
\ No newline at end of file
diff --git a/core/java/android/app/NotificationHistory.java b/core/java/android/app/NotificationHistory.java
new file mode 100644
index 0000000..c35246b
--- /dev/null
+++ b/core/java/android/app/NotificationHistory.java
@@ -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];
+ }
+ };
+}
diff --git a/core/jni/fd_utils.cpp b/core/jni/fd_utils.cpp
index bb57805..c0e4e1f 100644
--- a/core/jni/fd_utils.cpp
+++ b/core/jni/fd_utils.cpp
@@ -59,6 +59,10 @@
return instance_;
}
+static bool IsMemfd(const std::string& path) {
+ return android::base::StartsWith(path, "/memfd:");
+}
+
bool FileDescriptorWhitelist::IsAllowed(const std::string& path) const {
// Check the static whitelist path.
for (const auto& whitelist_path : kPathWhitelist) {
@@ -87,6 +91,11 @@
return true;
}
+ // In-memory files created through memfd_create are allowed.
+ if (IsMemfd(path)) {
+ return true;
+ }
+
// Whitelist files needed for Runtime Resource Overlay, like these:
// /system/vendor/overlay/framework-res.apk
// /system/vendor/overlay-subdir/pg/framework-res.apk
@@ -312,6 +321,11 @@
return DetachSocket(fail_fn);
}
+ // Children can directly use in-memory files created through memfd_create.
+ if (IsMemfd(file_path)) {
+ return;
+ }
+
// NOTE: This might happen if the file was unlinked after being opened.
// It's a common pattern in the case of temporary files and the like but
// we should not allow such usage from the zygote.
diff --git a/core/tests/coretests/src/android/app/NotificationHistoryTest.java b/core/tests/coretests/src/android/app/NotificationHistoryTest.java
new file mode 100644
index 0000000..08595bb
--- /dev/null
+++ b/core/tests/coretests/src/android/app/NotificationHistoryTest.java
@@ -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));
+ }
+ }
+}
diff --git a/media/java/android/media/MediaRecorder.java b/media/java/android/media/MediaRecorder.java
index bf7da23..9723652 100644
--- a/media/java/android/media/MediaRecorder.java
+++ b/media/java/android/media/MediaRecorder.java
@@ -337,9 +337,14 @@
/**
* Audio source for capturing broadcast radio tuner output.
+ * Capturing the radio tuner output requires the
+ * {@link android.Manifest.permission#CAPTURE_AUDIO_OUTPUT} permission.
+ * This permission is reserved for use by system components and is not available to
+ * third-party applications.
* @hide
*/
@SystemApi
+ @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_OUTPUT)
public static final int RADIO_TUNER = 1998;
/**
diff --git a/media/jni/audioeffect/Visualizer.h b/media/jni/audioeffect/Visualizer.h
index 8078e36..d4672a9 100644
--- a/media/jni/audioeffect/Visualizer.h
+++ b/media/jni/audioeffect/Visualizer.h
@@ -73,7 +73,8 @@
~Visualizer();
- virtual status_t setEnabled(bool enabled);
+ // Declared 'final' because we call this in ~Visualizer().
+ status_t setEnabled(bool enabled) final;
// maximum capture size in samples
static uint32_t getMaxCaptureSize() { return VISUALIZER_CAPTURE_SIZE_MAX; }
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
index a1a6ab4..faf6ee4 100644
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
+++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
@@ -32,6 +32,7 @@
import android.content.res.Configuration;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
+import android.os.PowerManager;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.GestureDetector;
@@ -107,6 +108,8 @@
import com.android.systemui.statusbar.phone.BiometricUnlockController;
import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment;
import com.android.systemui.statusbar.phone.DozeParameters;
+import com.android.systemui.statusbar.phone.DozeScrimController;
+import com.android.systemui.statusbar.phone.DozeServiceHost;
import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.phone.LightBarController;
@@ -297,6 +300,9 @@
ScrimController scrimController,
Lazy<LockscreenWallpaper> lockscreenWallpaperLazy,
Lazy<BiometricUnlockController> biometricUnlockControllerLazy,
+ DozeServiceHost dozeServiceHost,
+ PowerManager powerManager,
+ DozeScrimController dozeScrimController,
/* Car Settings injected components. */
CarNavigationBarController carNavigationBarController) {
@@ -360,7 +366,10 @@
dozeParameters,
scrimController,
lockscreenWallpaperLazy,
- biometricUnlockControllerLazy);
+ biometricUnlockControllerLazy,
+ dozeServiceHost,
+ powerManager,
+ dozeScrimController);
mScrimController = scrimController;
mCarNavigationBarController = carNavigationBarController;
}
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index 80faf476..fdc987f 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -1080,6 +1080,9 @@
Slog.v(LOG_TAG, "getAllConfigFlags() for " + prefix);
}
+ DeviceConfig.enforceReadPermission(getContext(),
+ prefix != null ? prefix.split("/")[0] : null);
+
synchronized (mLock) {
// Get the settings.
SettingsState settingsState = mSettingsRegistry.getSettingsLocked(
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DependencyBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DependencyBinder.java
index 6674c12..9032c6f 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/DependencyBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/DependencyBinder.java
@@ -20,6 +20,7 @@
import com.android.systemui.appops.AppOpsController;
import com.android.systemui.appops.AppOpsControllerImpl;
import com.android.systemui.classifier.FalsingManagerProxy;
+import com.android.systemui.doze.DozeHost;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.DarkIconDispatcher;
import com.android.systemui.plugins.FalsingManager;
@@ -33,6 +34,7 @@
import com.android.systemui.statusbar.StatusBarStateControllerImpl;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.phone.DarkIconDispatcherImpl;
+import com.android.systemui.statusbar.phone.DozeServiceHost;
import com.android.systemui.statusbar.phone.ManagedProfileController;
import com.android.systemui.statusbar.phone.ManagedProfileControllerImpl;
import com.android.systemui.statusbar.phone.StatusBarIconController;
@@ -241,5 +243,10 @@
/**
*/
@Binds
- public abstract FalsingManager provideFalsingmanager(FalsingManagerProxy falsingManagerImpl);
+ public abstract FalsingManager provideFalsingManager(FalsingManagerProxy falsingManagerImpl);
+
+ /**
+ */
+ @Binds
+ public abstract DozeHost provideDozeHost(DozeServiceHost dozeServiceHost);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
index 033171a..1e8e28f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -55,10 +55,10 @@
@Singleton
public class BiometricUnlockController extends KeyguardUpdateMonitorCallback {
- private static final String TAG = "BiometricUnlockController";
+ private static final String TAG = "BiometricUnlockCtrl";
private static final boolean DEBUG_BIO_WAKELOCK = KeyguardConstants.DEBUG_BIOMETRIC_WAKELOCK;
private static final long BIOMETRIC_WAKELOCK_TIMEOUT_MS = 15 * 1000;
- private static final String BIOMETRIC_WAKE_LOCK_NAME = "wake-and-unlock wakelock";
+ private static final String BIOMETRIC_WAKE_LOCK_NAME = "wake-and-unlock:wakelock";
@IntDef(prefix = { "MODE_" }, value = {
MODE_NONE,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
index 50d33a7..bc48235 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
@@ -24,7 +24,6 @@
import android.provider.Settings;
import android.util.MathUtils;
-import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.MainResources;
import com.android.systemui.doze.AlwaysOnDisplayPolicy;
@@ -188,12 +187,7 @@
return;
}
mControlScreenOffAnimation = controlScreenOffAnimation;
- getPowerManager().setDozeAfterScreenOff(!controlScreenOffAnimation);
- }
-
- @VisibleForTesting
- protected PowerManager getPowerManager() {
- return mPowerManager;
+ mPowerManager.setDozeAfterScreenOff(!controlScreenOffAnimation);
}
private boolean getBoolean(String propName, int resId) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java
index fe3c04e..1ecc489 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java
@@ -29,10 +29,12 @@
import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
import javax.inject.Inject;
+import javax.inject.Singleton;
/**
* Controller which handles all the doze animations of the scrims.
*/
+@Singleton
public class DozeScrimController implements StateListener {
private static final String TAG = "DozeScrimController";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
new file mode 100644
index 0000000..2854355
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
@@ -0,0 +1,465 @@
+/*
+ * 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.systemui.statusbar.phone;
+
+import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE;
+import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_WAKING;
+
+import android.annotation.NonNull;
+import android.os.Bundle;
+import android.os.PowerManager;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.systemui.assist.AssistManager;
+import com.android.systemui.doze.DozeEvent;
+import com.android.systemui.doze.DozeHost;
+import com.android.systemui.doze.DozeLog;
+import com.android.systemui.doze.DozeReceiver;
+import com.android.systemui.keyguard.KeyguardViewMediator;
+import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.statusbar.PulseExpansionHandler;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.SysuiStatusBarStateController;
+import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.policy.BatteryController;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController;
+
+import java.util.ArrayList;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import dagger.Lazy;
+
+/**
+ * Implementation of DozeHost for SystemUI.
+ */
+@Singleton
+public final class DozeServiceHost implements DozeHost {
+ private static final String TAG = "DozeServiceHost";
+ private final ArrayList<Callback> mCallbacks = new ArrayList<>();
+ private final DozeLog mDozeLog;
+ private final PowerManager mPowerManager;
+ private boolean mAnimateWakeup;
+ private boolean mAnimateScreenOff;
+ private boolean mIgnoreTouchWhilePulsing;
+ private Runnable mPendingScreenOffCallback;
+ @VisibleForTesting
+ boolean mWakeLockScreenPerformsAuth = SystemProperties.getBoolean(
+ "persist.sysui.wake_performs_auth", true);
+ private boolean mDozingRequested;
+ private boolean mDozing;
+ private boolean mPulsing;
+ private WakefulnessLifecycle mWakefulnessLifecycle;
+ private final SysuiStatusBarStateController mStatusBarStateController;
+ private final DeviceProvisionedController mDeviceProvisionedController;
+ private final HeadsUpManagerPhone mHeadsUpManagerPhone;
+ private final BatteryController mBatteryController;
+ private final ScrimController mScrimController;
+ private final Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy;
+ private BiometricUnlockController mBiometricUnlockController;
+ private final KeyguardViewMediator mKeyguardViewMediator;
+ private final AssistManager mAssistManager;
+ private final DozeScrimController mDozeScrimController;
+ private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+ private final VisualStabilityManager mVisualStabilityManager;
+ private final PulseExpansionHandler mPulseExpansionHandler;
+ private final StatusBarWindowController mStatusBarWindowController;
+ private final NotificationWakeUpCoordinator mNotificationWakeUpCoordinator;
+ private NotificationIconAreaController mNotificationIconAreaController;
+ private StatusBarWindowViewController mStatusBarWindowViewController;
+ private StatusBarWindowView mStatusBarWindow;
+ private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+ private NotificationPanelView mNotificationPanel;
+ private View mAmbientIndicationContainer;
+ private StatusBar mStatusBar;
+
+ @Inject
+ public DozeServiceHost(DozeLog dozeLog, PowerManager powerManager,
+ WakefulnessLifecycle wakefulnessLifecycle,
+ SysuiStatusBarStateController statusBarStateController,
+ DeviceProvisionedController deviceProvisionedController,
+ HeadsUpManagerPhone headsUpManagerPhone, BatteryController batteryController,
+ ScrimController scrimController,
+ Lazy<BiometricUnlockController> biometricUnlockControllerLazy,
+ KeyguardViewMediator keyguardViewMediator,
+ AssistManager assistManager,
+ DozeScrimController dozeScrimController, KeyguardUpdateMonitor keyguardUpdateMonitor,
+ VisualStabilityManager visualStabilityManager,
+ PulseExpansionHandler pulseExpansionHandler,
+ StatusBarWindowController statusBarWindowController,
+ NotificationWakeUpCoordinator notificationWakeUpCoordinator) {
+ super();
+ mDozeLog = dozeLog;
+ mPowerManager = powerManager;
+ mWakefulnessLifecycle = wakefulnessLifecycle;
+ mStatusBarStateController = statusBarStateController;
+ mDeviceProvisionedController = deviceProvisionedController;
+ mHeadsUpManagerPhone = headsUpManagerPhone;
+ mBatteryController = batteryController;
+ mScrimController = scrimController;
+ mBiometricUnlockControllerLazy = biometricUnlockControllerLazy;
+ mKeyguardViewMediator = keyguardViewMediator;
+ mAssistManager = assistManager;
+ mDozeScrimController = dozeScrimController;
+ mKeyguardUpdateMonitor = keyguardUpdateMonitor;
+ mVisualStabilityManager = visualStabilityManager;
+ mPulseExpansionHandler = pulseExpansionHandler;
+ mStatusBarWindowController = statusBarWindowController;
+ mNotificationWakeUpCoordinator = notificationWakeUpCoordinator;
+ }
+
+ // TODO: we should try to not pass status bar in here if we can avoid it.
+
+ /**
+ * Initialize instance with objects only available later during execution.
+ */
+ public void initialize(StatusBar statusBar,
+ NotificationIconAreaController notificationIconAreaController,
+ StatusBarWindowViewController statusBarWindowViewController,
+ StatusBarWindowView statusBarWindow,
+ StatusBarKeyguardViewManager statusBarKeyguardViewManager,
+ NotificationPanelView notificationPanel, View ambientIndicationContainer) {
+ mStatusBar = statusBar;
+ mNotificationIconAreaController = notificationIconAreaController;
+ mStatusBarWindowViewController = statusBarWindowViewController;
+ mStatusBarWindow = statusBarWindow;
+ mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
+ mNotificationPanel = notificationPanel;
+ mAmbientIndicationContainer = ambientIndicationContainer;
+ mBiometricUnlockController = mBiometricUnlockControllerLazy.get();
+ }
+
+ @Override
+ public String toString() {
+ return "PSB.DozeServiceHost[mCallbacks=" + mCallbacks.size() + "]";
+ }
+
+ void firePowerSaveChanged(boolean active) {
+ for (Callback callback : mCallbacks) {
+ callback.onPowerSaveChanged(active);
+ }
+ }
+
+ void fireNotificationPulse(NotificationEntry entry) {
+ Runnable pulseSuppressedListener = () -> {
+ entry.setPulseSuppressed(true);
+ mNotificationIconAreaController.updateAodNotificationIcons();
+ };
+ for (Callback callback : mCallbacks) {
+ callback.onNotificationAlerted(pulseSuppressedListener);
+ }
+ }
+
+ boolean getDozingRequested() {
+ return mDozingRequested;
+ }
+
+ boolean isPulsing() {
+ return mPulsing;
+ }
+
+
+ @Override
+ public void addCallback(@NonNull Callback callback) {
+ mCallbacks.add(callback);
+ }
+
+ @Override
+ public void removeCallback(@NonNull Callback callback) {
+ mCallbacks.remove(callback);
+ }
+
+ @Override
+ public void startDozing() {
+ if (!mDozingRequested) {
+ mDozingRequested = true;
+ mDozeLog.traceDozing(mDozing);
+ updateDozing();
+ mStatusBar.updateIsKeyguard();
+ }
+ }
+
+ void updateDozing() {
+ // When in wake-and-unlock while pulsing, keep dozing state until fully unlocked.
+ boolean
+ dozing =
+ mDozingRequested && mStatusBarStateController.getState() == StatusBarState.KEYGUARD
+ || mBiometricUnlockController.getMode()
+ == BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING;
+ // When in wake-and-unlock we may not have received a change to StatusBarState
+ // but we still should not be dozing, manually set to false.
+ if (mBiometricUnlockController.getMode()
+ == BiometricUnlockController.MODE_WAKE_AND_UNLOCK) {
+ dozing = false;
+ }
+
+
+ mStatusBarStateController.setIsDozing(dozing);
+ }
+
+ @Override
+ public void pulseWhileDozing(@NonNull PulseCallback callback, int reason) {
+ if (reason == DozeEvent.PULSE_REASON_SENSOR_LONG_PRESS) {
+ mPowerManager.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE,
+ "com.android.systemui:LONG_PRESS");
+ mAssistManager.startAssist(new Bundle());
+ return;
+ }
+
+ if (reason == DozeEvent.PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN) {
+ mScrimController.setWakeLockScreenSensorActive(true);
+ }
+
+ if (reason == DozeEvent.PULSE_REASON_DOCKING && mStatusBarWindow != null) {
+ mStatusBarWindowViewController.suppressWakeUpGesture(true);
+ }
+
+ boolean passiveAuthInterrupt = reason == DozeEvent.PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN
+ && mWakeLockScreenPerformsAuth;
+ // Set the state to pulsing, so ScrimController will know what to do once we ask it to
+ // execute the transition. The pulse callback will then be invoked when the scrims
+ // are black, indicating that StatusBar is ready to present the rest of the UI.
+ mPulsing = true;
+ mDozeScrimController.pulse(new PulseCallback() {
+ @Override
+ public void onPulseStarted() {
+ callback.onPulseStarted();
+ mStatusBar.updateNotificationPanelTouchState();
+ setPulsing(true);
+ }
+
+ @Override
+ public void onPulseFinished() {
+ mPulsing = false;
+ callback.onPulseFinished();
+ mStatusBar.updateNotificationPanelTouchState();
+ mScrimController.setWakeLockScreenSensorActive(false);
+ if (mStatusBarWindow != null) {
+ mStatusBarWindowViewController.suppressWakeUpGesture(false);
+ }
+ setPulsing(false);
+ }
+
+ private void setPulsing(boolean pulsing) {
+ mStatusBarStateController.setPulsing(pulsing);
+ mStatusBarKeyguardViewManager.setPulsing(pulsing);
+ mKeyguardViewMediator.setPulsing(pulsing);
+ mNotificationPanel.setPulsing(pulsing);
+ mVisualStabilityManager.setPulsing(pulsing);
+ mStatusBarWindowViewController.setPulsing(pulsing);
+ mIgnoreTouchWhilePulsing = false;
+ if (mKeyguardUpdateMonitor != null && passiveAuthInterrupt) {
+ mKeyguardUpdateMonitor.onAuthInterruptDetected(pulsing /* active */);
+ }
+ mStatusBar.updateScrimController();
+ mPulseExpansionHandler.setPulsing(pulsing);
+ mNotificationWakeUpCoordinator.setPulsing(pulsing);
+ }
+ }, reason);
+ // DozeScrimController is in pulse state, now let's ask ScrimController to start
+ // pulsing and draw the black frame, if necessary.
+ mStatusBar.updateScrimController();
+ }
+
+ @Override
+ public void stopDozing() {
+ if (mDozingRequested) {
+ mDozingRequested = false;
+ mDozeLog.traceDozing(mDozing);
+ updateDozing();
+ }
+ }
+
+ @Override
+ public void onIgnoreTouchWhilePulsing(boolean ignore) {
+ if (ignore != mIgnoreTouchWhilePulsing) {
+ mDozeLog.tracePulseTouchDisabledByProx(ignore);
+ }
+ mIgnoreTouchWhilePulsing = ignore;
+ if (mDozing && ignore) {
+ mStatusBarWindowViewController.cancelCurrentTouch();
+ }
+ }
+
+ @Override
+ public void dozeTimeTick() {
+ mNotificationPanel.dozeTimeTick();
+ if (mAmbientIndicationContainer instanceof DozeReceiver) {
+ ((DozeReceiver) mAmbientIndicationContainer).dozeTimeTick();
+ }
+ }
+
+ @Override
+ public boolean isPowerSaveActive() {
+ return mBatteryController.isAodPowerSave();
+ }
+
+ @Override
+ public boolean isPulsingBlocked() {
+ return mBiometricUnlockController.getMode()
+ == BiometricUnlockController.MODE_WAKE_AND_UNLOCK;
+ }
+
+ @Override
+ public boolean isProvisioned() {
+ return mDeviceProvisionedController.isDeviceProvisioned()
+ && mDeviceProvisionedController.isCurrentUserSetup();
+ }
+
+ @Override
+ public boolean isBlockingDoze() {
+ if (mBiometricUnlockController.hasPendingAuthentication()) {
+ Log.i(StatusBar.TAG, "Blocking AOD because fingerprint has authenticated");
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void extendPulse(int reason) {
+ if (reason == DozeEvent.PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN) {
+ mScrimController.setWakeLockScreenSensorActive(true);
+ }
+ if (mDozeScrimController.isPulsing() && mHeadsUpManagerPhone.hasNotifications()) {
+ mHeadsUpManagerPhone.extendHeadsUp();
+ } else {
+ mDozeScrimController.extendPulse();
+ }
+ }
+
+ @Override
+ public void stopPulsing() {
+ if (mDozeScrimController.isPulsing()) {
+ mDozeScrimController.pulseOutNow();
+ }
+ }
+
+ @Override
+ public void setAnimateWakeup(boolean animateWakeup) {
+ if (mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_AWAKE
+ || mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_WAKING) {
+ // Too late to change the wakeup animation.
+ return;
+ }
+ mAnimateWakeup = animateWakeup;
+ }
+
+ @Override
+ public void setAnimateScreenOff(boolean animateScreenOff) {
+ mAnimateScreenOff = animateScreenOff;
+ }
+
+ @Override
+ public void onSlpiTap(float screenX, float screenY) {
+ if (screenX > 0 && screenY > 0 && mAmbientIndicationContainer != null
+ && mAmbientIndicationContainer.getVisibility() == View.VISIBLE) {
+ int[] locationOnScreen = new int[2];
+ mAmbientIndicationContainer.getLocationOnScreen(locationOnScreen);
+ float viewX = screenX - locationOnScreen[0];
+ float viewY = screenY - locationOnScreen[1];
+ if (0 <= viewX && viewX <= mAmbientIndicationContainer.getWidth()
+ && 0 <= viewY && viewY <= mAmbientIndicationContainer.getHeight()) {
+
+ // Dispatch a tap
+ long now = SystemClock.elapsedRealtime();
+ MotionEvent ev = MotionEvent.obtain(
+ now, now, MotionEvent.ACTION_DOWN, screenX, screenY, 0);
+ mAmbientIndicationContainer.dispatchTouchEvent(ev);
+ ev.recycle();
+ ev = MotionEvent.obtain(
+ now, now, MotionEvent.ACTION_UP, screenX, screenY, 0);
+ mAmbientIndicationContainer.dispatchTouchEvent(ev);
+ ev.recycle();
+ }
+ }
+ }
+
+ @Override
+ public void setDozeScreenBrightness(int value) {
+ mStatusBarWindowController.setDozeScreenBrightness(value);
+ }
+
+ @Override
+ public void setAodDimmingScrim(float scrimOpacity) {
+ mScrimController.setAodFrontScrimAlpha(scrimOpacity);
+ }
+
+
+
+ @Override
+ public void prepareForGentleSleep(Runnable onDisplayOffCallback) {
+ if (mPendingScreenOffCallback != null) {
+ Log.w(TAG, "Overlapping onDisplayOffCallback. Ignoring previous one.");
+ }
+ mPendingScreenOffCallback = onDisplayOffCallback;
+ mStatusBar.updateScrimController();
+ }
+
+ @Override
+ public void cancelGentleSleep() {
+ mPendingScreenOffCallback = null;
+ if (mScrimController.getState() == ScrimState.OFF) {
+ mStatusBar.updateScrimController();
+ }
+ }
+
+ /**
+ * When the dozing host is waiting for scrims to fade out to change the display state.
+ */
+ boolean hasPendingScreenOffCallback() {
+ return mPendingScreenOffCallback != null;
+ }
+
+ /**
+ * Executes an nullifies the pending display state callback.
+ *
+ * @see #hasPendingScreenOffCallback()
+ * @see #prepareForGentleSleep(Runnable)
+ */
+ void executePendingScreenOffCallback() {
+ if (mPendingScreenOffCallback == null) {
+ return;
+ }
+ mPendingScreenOffCallback.run();
+ mPendingScreenOffCallback = null;
+ }
+
+ boolean shouldAnimateWakeup() {
+ return mAnimateWakeup;
+ }
+
+ boolean shouldAnimateScreenOff() {
+ return mAnimateScreenOff;
+ }
+
+ public void setDozing(boolean dozing) {
+ mDozing = dozing;
+ }
+
+ boolean getIgnoreTouchWhilePulsing() {
+ return mIgnoreTouchWhilePulsing;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index 6064fbe..35039a0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -60,11 +60,13 @@
import java.util.function.Consumer;
import javax.inject.Inject;
+import javax.inject.Singleton;
/**
* Controls both the scrim behind the notifications and in front of the notifications (when a
* security method gets shown).
*/
+@Singleton
public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnColorsChangedListener,
Dumpable {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index 2e0fbfa..971843e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -50,7 +50,6 @@
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
-import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityOptions;
@@ -157,10 +156,8 @@
import com.android.systemui.charging.WirelessChargingAnimation;
import com.android.systemui.classifier.FalsingLog;
import com.android.systemui.colorextraction.SysuiColorExtractor;
-import com.android.systemui.doze.DozeEvent;
import com.android.systemui.doze.DozeHost;
import com.android.systemui.doze.DozeLog;
-import com.android.systemui.doze.DozeReceiver;
import com.android.systemui.fragments.ExtensionFragmentListener;
import com.android.systemui.fragments.FragmentHostManager;
import com.android.systemui.keyguard.KeyguardSliceProvider;
@@ -347,7 +344,7 @@
/**
* The {@link StatusBarState} of the status bar.
*/
- protected int mState;
+ protected int mState; // TODO: remove this. Just use StatusBarStateController
protected boolean mBouncerShowing;
private PhoneStatusBarPolicy mIconPolicy;
@@ -373,7 +370,7 @@
protected StatusBarWindowController mStatusBarWindowController;
private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
@VisibleForTesting
- DozeServiceHost mDozeServiceHost = new DozeServiceHost();
+ DozeServiceHost mDozeServiceHost;
private boolean mWakeUpComingFromTouch;
private PointF mWakeUpTouchLocation;
@@ -493,7 +490,6 @@
private final UiOffloadThread mUiOffloadThread;
protected boolean mDozing;
- private boolean mDozingRequested;
private final NotificationMediaManager mMediaManager;
private final NotificationLockscreenUserManager mLockscreenUserManager;
@@ -612,7 +608,6 @@
private ActivityLaunchAnimator mActivityLaunchAnimator;
protected StatusBarNotificationPresenter mPresenter;
private NotificationActivityStarter mNotificationActivityStarter;
- private boolean mPulsing;
private final BubbleController mBubbleController;
private final BubbleController.BubbleExpandListener mBubbleExpandListener;
@@ -692,7 +687,10 @@
DozeParameters dozeParameters,
ScrimController scrimController,
Lazy<LockscreenWallpaper> lockscreenWallpaperLazy,
- Lazy<BiometricUnlockController> biometricUnlockControllerLazy) {
+ Lazy<BiometricUnlockController> biometricUnlockControllerLazy,
+ DozeServiceHost dozeServiceHost,
+ PowerManager powerManager,
+ DozeScrimController dozeScrimController) {
super(context);
mFeatureFlags = featureFlags;
mLightBarController = lightBarController;
@@ -749,9 +747,12 @@
mStatusBarWindowController = statusBarWindowController;
mStatusBarWindowViewControllerBuilder = statusBarWindowViewControllerBuilder;
mNotifLog = notifLog;
+ mDozeServiceHost = dozeServiceHost;
+ mPowerManager = powerManager;
mDozeParameters = dozeParameters;
mScrimController = scrimController;
mLockscreenWallpaperLazy = lockscreenWallpaperLazy;
+ mDozeScrimController = dozeScrimController;
mBiometricUnlockControllerLazy = biometricUnlockControllerLazy;
mBubbleExpandListener =
@@ -804,7 +805,6 @@
mAccessibilityManager = (AccessibilityManager)
mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
- mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
mKeyguardUpdateMonitor.setKeyguardBypassController(mKeyguardBypassController);
mBarService = IStatusBarService.Stub.asInterface(
ServiceManager.getService(Context.STATUS_BAR_SERVICE));
@@ -904,6 +904,9 @@
startKeyguard();
mKeyguardUpdateMonitor.registerCallback(mUpdateCallback);
+ mDozeServiceHost.initialize(this, mNotificationIconAreaController,
+ mStatusBarWindowViewController, mStatusBarWindow, mStatusBarKeyguardViewManager,
+ mNotificationPanel, mAmbientIndicationContainer);
putComponent(DozeHost.class, mDozeServiceHost);
mScreenPinningRequest = new ScreenPinningRequest(mContext);
@@ -1068,7 +1071,6 @@
mNotificationPanel.initDependencies(this, mGroupManager, mNotificationShelf,
mHeadsUpManager, mNotificationIconAreaController, mScrimController);
- mDozeScrimController = new DozeScrimController(mDozeParameters, mDozeLog);
BackDropView backdrop = mStatusBarWindow.findViewById(R.id.backdrop);
mMediaManager.setup(backdrop, backdrop.findViewById(R.id.backdrop_front),
@@ -1729,7 +1731,7 @@
if (isDozing() && isHeadsUp) {
entry.setPulseSuppressed(false);
mDozeServiceHost.fireNotificationPulse(entry);
- if (mPulsing) {
+ if (mDozeServiceHost.isPulsing()) {
mDozeScrimController.cancelPendingPulseTimeout();
}
}
@@ -1761,7 +1763,7 @@
}
public boolean isPulsing() {
- return mPulsing;
+ return mDozeServiceHost.isPulsing();
}
public boolean hideStatusBarIconsWhenExpanded() {
@@ -2824,7 +2826,7 @@
if (mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_ASLEEP
&& mKeyguardStateController.canDismissLockScreen()
&& !mStatusBarStateController.leaveOpenOnKeyguardHide()
- && isPulsing()) {
+ && mDozeServiceHost.isPulsing()) {
// Reuse the biometric wake-and-unlock transition if we dismiss keyguard from a pulse.
// TODO: Factor this transition out of BiometricUnlockController.
mBiometricUnlockController.startWakeAndUnlock(
@@ -3155,7 +3157,7 @@
return mState == StatusBarState.FULLSCREEN_USER_SWITCHER;
}
- private boolean updateIsKeyguard() {
+ boolean updateIsKeyguard() {
boolean wakeAndUnlocking = mBiometricUnlockController.getMode()
== BiometricUnlockController.MODE_WAKE_AND_UNLOCK;
@@ -3163,8 +3165,8 @@
// there's no surface we can show to the user. Note that the device goes fully interactive
// late in the transition, so we also allow the device to start dozing once the screen has
// turned off fully.
- boolean keyguardForDozing = mDozingRequested &&
- (!mDeviceInteractive || isGoingToSleep() && (isScreenFullyOff() || mIsKeyguard));
+ boolean keyguardForDozing = mDozeServiceHost.getDozingRequested()
+ && (!mDeviceInteractive || isGoingToSleep() && (isScreenFullyOff() || mIsKeyguard));
boolean shouldBeKeyguard = (mStatusBarStateController.isKeyguardRequested()
|| keyguardForDozing) && !wakeAndUnlocking;
if (keyguardForDozing) {
@@ -3580,7 +3582,7 @@
public void onStateChanged(int newState) {
mState = newState;
updateReportRejectedTouchVisibility();
- updateDozing();
+ mDozeServiceHost.updateDozing();
updateTheme();
mNavigationBarController.touchAutoDim(mDisplayId);
Trace.beginSection("StatusBar#updateKeyguardState");
@@ -3618,9 +3620,11 @@
public void onDozingChanged(boolean isDozing) {
Trace.beginSection("StatusBar#updateDozing");
mDozing = isDozing;
+ mDozeServiceHost.setDozing(mDozing);
// Collapse the notification panel if open
- boolean dozingAnimated = mDozingRequested && mDozeParameters.shouldControlScreenOff();
+ boolean dozingAnimated = mDozeServiceHost.getDozingRequested()
+ && mDozeParameters.shouldControlScreenOff();
mNotificationPanel.resetViews(dozingAnimated);
updateQsExpansionEnabled();
@@ -3628,26 +3632,12 @@
mEntryManager.updateNotifications("onDozingChanged");
updateDozingState();
+ mDozeServiceHost.updateDozing();
updateScrimController();
updateReportRejectedTouchVisibility();
Trace.endSection();
}
- private void updateDozing() {
- // When in wake-and-unlock while pulsing, keep dozing state until fully unlocked.
- boolean dozing = mDozingRequested && mState == StatusBarState.KEYGUARD
- || mBiometricUnlockController.getMode()
- == BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING;
- // When in wake-and-unlock we may not have received a change to mState
- // but we still should not be dozing, manually set to false.
- if (mBiometricUnlockController.getMode() ==
- BiometricUnlockController.MODE_WAKE_AND_UNLOCK) {
- dozing = false;
- }
-
- mStatusBarStateController.setIsDozing(dozing);
- }
-
private void updateKeyguardState() {
mKeyguardStateController.notifyKeyguardState(mStatusBarKeyguardViewManager.isShowing(),
mStatusBarKeyguardViewManager.isOccluded());
@@ -3872,10 +3862,11 @@
* collapse the panel after we expanded it, and thus we would end up with a blank
* Keyguard.
*/
- private void updateNotificationPanelTouchState() {
+ void updateNotificationPanelTouchState() {
boolean goingToSleepWithoutAnimation = isGoingToSleep()
&& !mDozeParameters.shouldControlScreenOff();
- boolean disabled = (!mDeviceInteractive && !mPulsing) || goingToSleepWithoutAnimation;
+ boolean disabled = (!mDeviceInteractive && !mDozeServiceHost.isPulsing())
+ || goingToSleepWithoutAnimation;
mNotificationPanel.setTouchAndAnimationDisabled(disabled);
mNotificationIconAreaController.setAnimationsEnabled(!disabled);
}
@@ -4025,7 +4016,7 @@
}
public void notifyBiometricAuthModeChanged() {
- updateDozing();
+ mDozeServiceHost.updateDozing();
updateScrimController();
mStatusBarWindowViewController.onBiometricAuthModeChanged(
mBiometricUnlockController.isWakeAndUnlock(),
@@ -4061,7 +4052,7 @@
mScrimController.transitionTo(ScrimState.UNLOCKED, mUnlockScrimCallback);
} else if (mBrightnessMirrorVisible) {
mScrimController.transitionTo(ScrimState.BRIGHTNESS_MIRROR);
- } else if (isPulsing()) {
+ } else if (mDozeServiceHost.isPulsing()) {
mScrimController.transitionTo(ScrimState.PULSING,
mDozeScrimController.getScrimCallback());
} else if (mDozeServiceHost.hasPendingScreenOffCallback()) {
@@ -4091,295 +4082,8 @@
return mStatusBarKeyguardViewManager.isShowing();
}
- @VisibleForTesting
- final class DozeServiceHost implements DozeHost {
- private final ArrayList<Callback> mCallbacks = new ArrayList<>();
- private boolean mAnimateWakeup;
- private boolean mAnimateScreenOff;
- private boolean mIgnoreTouchWhilePulsing;
- private Runnable mPendingScreenOffCallback;
- @VisibleForTesting
- boolean mWakeLockScreenPerformsAuth = SystemProperties.getBoolean(
- "persist.sysui.wake_performs_auth", true);
-
- @Override
- public String toString() {
- return "PSB.DozeServiceHost[mCallbacks=" + mCallbacks.size() + "]";
- }
-
- public void firePowerSaveChanged(boolean active) {
- for (Callback callback : mCallbacks) {
- callback.onPowerSaveChanged(active);
- }
- }
-
- public void fireNotificationPulse(NotificationEntry entry) {
- Runnable pulseSupressedListener = () -> {
- entry.setPulseSuppressed(true);
- mNotificationIconAreaController.updateAodNotificationIcons();
- };
- for (Callback callback : mCallbacks) {
- callback.onNotificationAlerted(pulseSupressedListener);
- }
- }
-
- @Override
- public void addCallback(@NonNull Callback callback) {
- mCallbacks.add(callback);
- }
-
- @Override
- public void removeCallback(@NonNull Callback callback) {
- mCallbacks.remove(callback);
- }
-
- @Override
- public void startDozing() {
- if (!mDozingRequested) {
- mDozingRequested = true;
- mDozeLog.traceDozing(mDozing);
- updateDozing();
- updateIsKeyguard();
- }
- }
-
- @Override
- public void pulseWhileDozing(@NonNull PulseCallback callback, int reason) {
- if (reason == DozeEvent.PULSE_REASON_SENSOR_LONG_PRESS) {
- mPowerManager.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE,
- "com.android.systemui:LONG_PRESS");
- startAssist(new Bundle());
- return;
- }
-
- if (reason == DozeEvent.PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN) {
- mScrimController.setWakeLockScreenSensorActive(true);
- }
-
- if (reason == DozeEvent.PULSE_REASON_DOCKING && mStatusBarWindow != null) {
- mStatusBarWindowViewController.suppressWakeUpGesture(true);
- }
-
- boolean passiveAuthInterrupt = reason == DozeEvent.PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN
- && mWakeLockScreenPerformsAuth;
- // Set the state to pulsing, so ScrimController will know what to do once we ask it to
- // execute the transition. The pulse callback will then be invoked when the scrims
- // are black, indicating that StatusBar is ready to present the rest of the UI.
- mPulsing = true;
- mDozeScrimController.pulse(new PulseCallback() {
- @Override
- public void onPulseStarted() {
- callback.onPulseStarted();
- updateNotificationPanelTouchState();
- setPulsing(true);
- }
-
- @Override
- public void onPulseFinished() {
- mPulsing = false;
- callback.onPulseFinished();
- updateNotificationPanelTouchState();
- mScrimController.setWakeLockScreenSensorActive(false);
- if (mStatusBarWindow != null) {
- mStatusBarWindowViewController.suppressWakeUpGesture(false);
- }
- setPulsing(false);
- }
-
- private void setPulsing(boolean pulsing) {
- mStatusBarStateController.setPulsing(pulsing);
- mStatusBarKeyguardViewManager.setPulsing(pulsing);
- mKeyguardViewMediator.setPulsing(pulsing);
- mNotificationPanel.setPulsing(pulsing);
- mVisualStabilityManager.setPulsing(pulsing);
- mStatusBarWindowViewController.setPulsing(pulsing);
- mIgnoreTouchWhilePulsing = false;
- if (mKeyguardUpdateMonitor != null && passiveAuthInterrupt) {
- mKeyguardUpdateMonitor.onAuthInterruptDetected(pulsing /* active */);
- }
- updateScrimController();
- mPulseExpansionHandler.setPulsing(pulsing);
- mWakeUpCoordinator.setPulsing(pulsing);
- }
- }, reason);
- // DozeScrimController is in pulse state, now let's ask ScrimController to start
- // pulsing and draw the black frame, if necessary.
- updateScrimController();
- }
-
- @Override
- public void stopDozing() {
- if (mDozingRequested) {
- mDozingRequested = false;
- mDozeLog.traceDozing(mDozing);
- updateDozing();
- }
- }
-
- @Override
- public void onIgnoreTouchWhilePulsing(boolean ignore) {
- if (ignore != mIgnoreTouchWhilePulsing) {
- mDozeLog.tracePulseTouchDisabledByProx(ignore);
- }
- mIgnoreTouchWhilePulsing = ignore;
- if (isDozing() && ignore) {
- mStatusBarWindowViewController.cancelCurrentTouch();
- }
- }
-
- @Override
- public void dozeTimeTick() {
- mNotificationPanel.dozeTimeTick();
- if (mAmbientIndicationContainer instanceof DozeReceiver) {
- ((DozeReceiver) mAmbientIndicationContainer).dozeTimeTick();
- }
- }
-
- @Override
- public boolean isPowerSaveActive() {
- return mBatteryController.isAodPowerSave();
- }
-
- @Override
- public boolean isPulsingBlocked() {
- return mBiometricUnlockController.getMode()
- == BiometricUnlockController.MODE_WAKE_AND_UNLOCK;
- }
-
- @Override
- public boolean isProvisioned() {
- return mDeviceProvisionedController.isDeviceProvisioned()
- && mDeviceProvisionedController.isCurrentUserSetup();
- }
-
- @Override
- public boolean isBlockingDoze() {
- if (mBiometricUnlockController.hasPendingAuthentication()) {
- Log.i(TAG, "Blocking AOD because fingerprint has authenticated");
- return true;
- }
- return false;
- }
-
- @Override
- public void extendPulse(int reason) {
- if (reason == DozeEvent.PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN) {
- mScrimController.setWakeLockScreenSensorActive(true);
- }
- if (mDozeScrimController.isPulsing() && mHeadsUpManager.hasNotifications()) {
- mHeadsUpManager.extendHeadsUp();
- } else {
- mDozeScrimController.extendPulse();
- }
- }
-
- @Override
- public void stopPulsing() {
- if (mDozeScrimController.isPulsing()) {
- mDozeScrimController.pulseOutNow();
- }
- }
-
- @Override
- public void setAnimateWakeup(boolean animateWakeup) {
- if (mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_AWAKE
- || mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_WAKING) {
- // Too late to change the wakeup animation.
- return;
- }
- mAnimateWakeup = animateWakeup;
- }
-
- @Override
- public void setAnimateScreenOff(boolean animateScreenOff) {
- mAnimateScreenOff = animateScreenOff;
- }
-
- @Override
- public void onSlpiTap(float screenX, float screenY) {
- if (screenX > 0 && screenY > 0 && mAmbientIndicationContainer != null
- && mAmbientIndicationContainer.getVisibility() == View.VISIBLE) {
- mAmbientIndicationContainer.getLocationOnScreen(mTmpInt2);
- float viewX = screenX - mTmpInt2[0];
- float viewY = screenY - mTmpInt2[1];
- if (0 <= viewX && viewX <= mAmbientIndicationContainer.getWidth()
- && 0 <= viewY && viewY <= mAmbientIndicationContainer.getHeight()) {
- dispatchTap(mAmbientIndicationContainer, viewX, viewY);
- }
- }
- }
-
- @Override
- public void setDozeScreenBrightness(int value) {
- mStatusBarWindowController.setDozeScreenBrightness(value);
- }
-
- @Override
- public void setAodDimmingScrim(float scrimOpacity) {
- mScrimController.setAodFrontScrimAlpha(scrimOpacity);
- }
-
- @Override
- public void prepareForGentleSleep(Runnable onDisplayOffCallback) {
- if (mPendingScreenOffCallback != null) {
- Log.w(TAG, "Overlapping onDisplayOffCallback. Ignoring previous one.");
- }
- mPendingScreenOffCallback = onDisplayOffCallback;
- updateScrimController();
- }
-
- @Override
- public void cancelGentleSleep() {
- mPendingScreenOffCallback = null;
- if (mScrimController.getState() == ScrimState.OFF) {
- updateScrimController();
- }
- }
-
- /**
- * When the dozing host is waiting for scrims to fade out to change the display state.
- */
- boolean hasPendingScreenOffCallback() {
- return mPendingScreenOffCallback != null;
- }
-
- /**
- * Executes an nullifies the pending display state callback.
- *
- * @see #hasPendingScreenOffCallback()
- * @see #prepareForGentleSleep(Runnable)
- */
- void executePendingScreenOffCallback() {
- if (mPendingScreenOffCallback == null) {
- return;
- }
- mPendingScreenOffCallback.run();
- mPendingScreenOffCallback = null;
- }
-
- private void dispatchTap(View view, float x, float y) {
- long now = SystemClock.elapsedRealtime();
- dispatchTouchEvent(view, x, y, now, MotionEvent.ACTION_DOWN);
- dispatchTouchEvent(view, x, y, now, MotionEvent.ACTION_UP);
- }
-
- private void dispatchTouchEvent(View view, float x, float y, long now, int action) {
- MotionEvent ev = MotionEvent.obtain(now, now, action, x, y, 0 /* meta */);
- view.dispatchTouchEvent(ev);
- ev.recycle();
- }
-
- private boolean shouldAnimateWakeup() {
- return mAnimateWakeup;
- }
-
- public boolean shouldAnimateScreenOff() {
- return mAnimateScreenOff;
- }
- }
-
public boolean shouldIgnoreTouch() {
- return isDozing() && mDozeServiceHost.mIgnoreTouchWhilePulsing;
+ return isDozing() && mDozeServiceHost.getIgnoreTouchWhilePulsing();
}
// Begin Extra BaseStatusBar methods.
@@ -4406,7 +4110,7 @@
private boolean mVisibleToUser;
protected DevicePolicyManager mDevicePolicyManager;
- protected PowerManager mPowerManager;
+ private final PowerManager mPowerManager;
protected StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
protected KeyguardManager mKeyguardManager;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
new file mode 100644
index 0000000..b05172c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
@@ -0,0 +1,224 @@
+/*
+ * 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.systemui.statusbar.phone;
+
+import static org.junit.Assert.assertFalse;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.os.PowerManager;
+import android.testing.AndroidTestingRunner;
+import android.view.View;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.assist.AssistManager;
+import com.android.systemui.doze.DozeEvent;
+import com.android.systemui.doze.DozeHost;
+import com.android.systemui.doze.DozeLog;
+import com.android.systemui.keyguard.KeyguardViewMediator;
+import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.statusbar.PulseExpansionHandler;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.StatusBarStateControllerImpl;
+import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
+import com.android.systemui.statusbar.policy.BatteryController;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+
+import dagger.Lazy;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class DozeServiceHostTest extends SysuiTestCase {
+
+ private DozeServiceHost mDozeServiceHost;
+
+ @Mock private HeadsUpManagerPhone mHeadsUpManager;
+ @Mock private ScrimController mScrimController;
+ @Mock private DozeScrimController mDozeScrimController;
+ @Mock private Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy;
+ @Mock private VisualStabilityManager mVisualStabilityManager;
+ @Mock private KeyguardViewMediator mKeyguardViewMediator;
+ @Mock private StatusBarStateControllerImpl mStatusBarStateController;
+ @Mock private BatteryController mBatteryController;
+ @Mock private DeviceProvisionedController mDeviceProvisionedController;
+ @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+ @Mock private AssistManager mAssistManager;
+ @Mock private DozeLog mDozeLog;
+ @Mock private PulseExpansionHandler mPulseExpansionHandler;
+ @Mock private NotificationWakeUpCoordinator mNotificationWakeUpCoordinator;
+ @Mock private StatusBarWindowController mStatusBarWindowController;
+ @Mock private PowerManager mPowerManager;
+ @Mock private WakefulnessLifecycle mWakefullnessLifecycle;
+ @Mock private StatusBar mStatusBar;
+ @Mock private NotificationIconAreaController mNotificationIconAreaController;
+ @Mock private StatusBarWindowViewController mStatusBarWindowViewController;
+ @Mock private StatusBarWindowView mStatusBarWindow;
+ @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+ @Mock private NotificationPanelView mNotificationPanel;
+ @Mock private View mAmbientIndicationContainer;
+ @Mock private BiometricUnlockController mBiometricUnlockController;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ when(mBiometricUnlockControllerLazy.get()).thenReturn(mBiometricUnlockController);
+ mDozeServiceHost = new DozeServiceHost(mDozeLog, mPowerManager, mWakefullnessLifecycle,
+ mStatusBarStateController, mDeviceProvisionedController, mHeadsUpManager,
+ mBatteryController, mScrimController, mBiometricUnlockControllerLazy,
+ mKeyguardViewMediator, mAssistManager, mDozeScrimController, mKeyguardUpdateMonitor,
+ mVisualStabilityManager, mPulseExpansionHandler, mStatusBarWindowController,
+ mNotificationWakeUpCoordinator);
+
+ mDozeServiceHost.initialize(mStatusBar, mNotificationIconAreaController,
+ mStatusBarWindowViewController, mStatusBarWindow, mStatusBarKeyguardViewManager,
+ mNotificationPanel, mAmbientIndicationContainer);
+ }
+
+ @Test
+ public void testStartStopDozing() {
+ when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
+ when(mStatusBarStateController.isKeyguardRequested()).thenReturn(true);
+
+ assertFalse(mDozeServiceHost.getDozingRequested());
+
+ mDozeServiceHost.startDozing();
+ verify(mStatusBarStateController).setIsDozing(eq(true));
+ verify(mStatusBar).updateIsKeyguard();
+
+ mDozeServiceHost.stopDozing();
+ verify(mStatusBarStateController).setIsDozing(eq(false));
+ }
+
+
+ @Test
+ public void testPulseWhileDozing_updatesScrimController() {
+ mStatusBar.setBarStateForTest(StatusBarState.KEYGUARD);
+ mStatusBar.showKeyguardImpl();
+
+ // Keep track of callback to be able to stop the pulse
+// DozeHost.PulseCallback[] pulseCallback = new DozeHost.PulseCallback[1];
+// doAnswer(invocation -> {
+// pulseCallback[0] = invocation.getArgument(0);
+// return null;
+// }).when(mDozeScrimController).pulse(any(), anyInt());
+
+ // Starting a pulse should change the scrim controller to the pulsing state
+ mDozeServiceHost.pulseWhileDozing(new DozeHost.PulseCallback() {
+ @Override
+ public void onPulseStarted() {
+ }
+
+ @Override
+ public void onPulseFinished() {
+ }
+ }, DozeEvent.PULSE_REASON_NOTIFICATION);
+
+ ArgumentCaptor<DozeHost.PulseCallback> pulseCallbackArgumentCaptor =
+ ArgumentCaptor.forClass(DozeHost.PulseCallback.class);
+
+ verify(mDozeScrimController).pulse(
+ pulseCallbackArgumentCaptor.capture(), eq(DozeEvent.PULSE_REASON_NOTIFICATION));
+ verify(mStatusBar).updateScrimController();
+ reset(mStatusBar);
+
+ pulseCallbackArgumentCaptor.getValue().onPulseFinished();
+ assertFalse(mDozeScrimController.isPulsing());
+ verify(mStatusBar).updateScrimController();
+ }
+
+
+ @Test
+ public void testPulseWhileDozingWithDockingReason_suppressWakeUpGesture() {
+ // Keep track of callback to be able to stop the pulse
+ final DozeHost.PulseCallback[] pulseCallback = new DozeHost.PulseCallback[1];
+ doAnswer(invocation -> {
+ pulseCallback[0] = invocation.getArgument(0);
+ return null;
+ }).when(mDozeScrimController).pulse(any(), anyInt());
+
+ // Starting a pulse while docking should suppress wakeup gesture
+ mDozeServiceHost.pulseWhileDozing(mock(DozeHost.PulseCallback.class),
+ DozeEvent.PULSE_REASON_DOCKING);
+ verify(mStatusBarWindowViewController).suppressWakeUpGesture(eq(true));
+
+ // Ending a pulse should restore wakeup gesture
+ pulseCallback[0].onPulseFinished();
+ verify(mStatusBarWindowViewController).suppressWakeUpGesture(eq(false));
+ }
+
+ @Test
+ public void testPulseWhileDozing_notifyAuthInterrupt() {
+ HashSet<Integer> reasonsWantingAuth = new HashSet<>(
+ Collections.singletonList(DozeEvent.PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN));
+ HashSet<Integer> reasonsSkippingAuth = new HashSet<>(
+ Arrays.asList(DozeEvent.PULSE_REASON_INTENT,
+ DozeEvent.PULSE_REASON_NOTIFICATION,
+ DozeEvent.PULSE_REASON_SENSOR_SIGMOTION,
+ DozeEvent.REASON_SENSOR_PICKUP,
+ DozeEvent.REASON_SENSOR_DOUBLE_TAP,
+ DozeEvent.PULSE_REASON_SENSOR_LONG_PRESS,
+ DozeEvent.PULSE_REASON_DOCKING,
+ DozeEvent.REASON_SENSOR_WAKE_UP,
+ DozeEvent.REASON_SENSOR_TAP));
+ HashSet<Integer> reasonsThatDontPulse = new HashSet<>(
+ Arrays.asList(DozeEvent.REASON_SENSOR_PICKUP,
+ DozeEvent.REASON_SENSOR_DOUBLE_TAP,
+ DozeEvent.REASON_SENSOR_TAP));
+
+ doAnswer(invocation -> {
+ DozeHost.PulseCallback callback = invocation.getArgument(0);
+ callback.onPulseStarted();
+ return null;
+ }).when(mDozeScrimController).pulse(any(), anyInt());
+
+ mDozeServiceHost.mWakeLockScreenPerformsAuth = true;
+ for (int i = 0; i < DozeEvent.TOTAL_REASONS; i++) {
+ reset(mKeyguardUpdateMonitor);
+ mDozeServiceHost.pulseWhileDozing(mock(DozeHost.PulseCallback.class), i);
+ if (reasonsWantingAuth.contains(i)) {
+ verify(mKeyguardUpdateMonitor).onAuthInterruptDetected(eq(true));
+ } else if (reasonsSkippingAuth.contains(i) || reasonsThatDontPulse.contains(i)) {
+ verify(mKeyguardUpdateMonitor, never()).onAuthInterruptDetected(eq(true));
+ } else {
+ throw new AssertionError("Reason " + i + " isn't specified as wanting or skipping"
+ + " passive auth. Please consider how this pulse reason should behave.");
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
index f5e92e4..66c01ca 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
@@ -85,8 +85,6 @@
import com.android.systemui.bubbles.BubbleController;
import com.android.systemui.classifier.FalsingManagerFake;
import com.android.systemui.colorextraction.SysuiColorExtractor;
-import com.android.systemui.doze.DozeEvent;
-import com.android.systemui.doze.DozeHost;
import com.android.systemui.doze.DozeLog;
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.keyguard.ScreenLifecycle;
@@ -145,9 +143,6 @@
import java.io.ByteArrayOutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashSet;
import dagger.Lazy;
@@ -231,6 +226,7 @@
@Mock private DozeParameters mDozeParameters;
@Mock private Lazy<LockscreenWallpaper> mLockscreenWallpaperLazy;
@Mock private LockscreenWallpaper mLockscreenWallpaper;
+ @Mock private DozeServiceHost mDozeServiceHost;
@Mock private LinearLayout mLockIconContainer;
@Mock private ViewMediatorCallback mKeyguardVieMediatorCallback;
@@ -365,7 +361,10 @@
mDozeParameters,
mScrimController,
mLockscreenWallpaperLazy,
- mBiometricUnlockControllerLazy);
+ mBiometricUnlockControllerLazy,
+ mDozeServiceHost,
+ mPowerManager,
+ mDozeScrimController);
when(mStatusBarWindowView.findViewById(R.id.lock_icon_container)).thenReturn(
mLockIconContainer);
@@ -388,7 +387,6 @@
mStatusBar.mNotificationIconAreaController = mNotificationIconAreaController;
mStatusBar.mPresenter = mNotificationPresenter;
mStatusBar.mKeyguardIndicationController = mKeyguardIndicationController;
- mStatusBar.mPowerManager = mPowerManager;
mStatusBar.mBarService = mBarService;
mStatusBar.mStackScroller = mStackScroller;
mStatusBar.mStatusBarWindowViewController = mStatusBarWindowViewController;
@@ -757,83 +755,18 @@
mStatusBar.setBarStateForTest(StatusBarState.KEYGUARD);
mStatusBar.showKeyguardImpl();
- // Keep track of callback to be able to stop the pulse
- DozeHost.PulseCallback[] pulseCallback = new DozeHost.PulseCallback[1];
- doAnswer(invocation -> {
- pulseCallback[0] = invocation.getArgument(0);
- return null;
- }).when(mDozeScrimController).pulse(any(), anyInt());
-
// Starting a pulse should change the scrim controller to the pulsing state
- mStatusBar.mDozeServiceHost.pulseWhileDozing(mock(DozeHost.PulseCallback.class),
- DozeEvent.PULSE_REASON_NOTIFICATION);
+ when(mDozeServiceHost.isPulsing()).thenReturn(true);
+ mStatusBar.updateScrimController();
verify(mScrimController).transitionTo(eq(ScrimState.PULSING), any());
// Ending a pulse should take it back to keyguard state
- pulseCallback[0].onPulseFinished();
+ when(mDozeServiceHost.isPulsing()).thenReturn(false);
+ mStatusBar.updateScrimController();
verify(mScrimController).transitionTo(eq(ScrimState.KEYGUARD));
}
@Test
- public void testPulseWhileDozing_notifyAuthInterrupt() {
- HashSet<Integer> reasonsWantingAuth = new HashSet<>(
- Collections.singletonList(DozeEvent.PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN));
- HashSet<Integer> reasonsSkippingAuth = new HashSet<>(
- Arrays.asList(DozeEvent.PULSE_REASON_INTENT,
- DozeEvent.PULSE_REASON_NOTIFICATION,
- DozeEvent.PULSE_REASON_SENSOR_SIGMOTION,
- DozeEvent.REASON_SENSOR_PICKUP,
- DozeEvent.REASON_SENSOR_DOUBLE_TAP,
- DozeEvent.PULSE_REASON_SENSOR_LONG_PRESS,
- DozeEvent.PULSE_REASON_DOCKING,
- DozeEvent.REASON_SENSOR_WAKE_UP,
- DozeEvent.REASON_SENSOR_TAP));
- HashSet<Integer> reasonsThatDontPulse = new HashSet<>(
- Arrays.asList(DozeEvent.REASON_SENSOR_PICKUP,
- DozeEvent.REASON_SENSOR_DOUBLE_TAP,
- DozeEvent.REASON_SENSOR_TAP));
-
- doAnswer(invocation -> {
- DozeHost.PulseCallback callback = invocation.getArgument(0);
- callback.onPulseStarted();
- return null;
- }).when(mDozeScrimController).pulse(any(), anyInt());
-
- mStatusBar.mDozeServiceHost.mWakeLockScreenPerformsAuth = true;
- for (int i = 0; i < DozeEvent.TOTAL_REASONS; i++) {
- reset(mKeyguardUpdateMonitor);
- mStatusBar.mDozeServiceHost.pulseWhileDozing(mock(DozeHost.PulseCallback.class), i);
- if (reasonsWantingAuth.contains(i)) {
- verify(mKeyguardUpdateMonitor).onAuthInterruptDetected(eq(true));
- } else if (reasonsSkippingAuth.contains(i) || reasonsThatDontPulse.contains(i)) {
- verify(mKeyguardUpdateMonitor, never()).onAuthInterruptDetected(eq(true));
- } else {
- throw new AssertionError("Reason " + i + " isn't specified as wanting or skipping"
- + " passive auth. Please consider how this pulse reason should behave.");
- }
- }
- }
-
- @Test
- public void testPulseWhileDozingWithDockingReason_suppressWakeUpGesture() {
- // Keep track of callback to be able to stop the pulse
- final DozeHost.PulseCallback[] pulseCallback = new DozeHost.PulseCallback[1];
- doAnswer(invocation -> {
- pulseCallback[0] = invocation.getArgument(0);
- return null;
- }).when(mDozeScrimController).pulse(any(), anyInt());
-
- // Starting a pulse while docking should suppress wakeup gesture
- mStatusBar.mDozeServiceHost.pulseWhileDozing(mock(DozeHost.PulseCallback.class),
- DozeEvent.PULSE_REASON_DOCKING);
- verify(mStatusBarWindowViewController).suppressWakeUpGesture(eq(true));
-
- // Ending a pulse should restore wakeup gesture
- pulseCallback[0].onPulseFinished();
- verify(mStatusBarWindowViewController).suppressWakeUpGesture(eq(false));
- }
-
- @Test
public void testSetState_changesIsFullScreenUserSwitcherState() {
mStatusBar.setBarStateForTest(StatusBarState.KEYGUARD);
assertFalse(mStatusBar.isFullScreenUserSwitcherState());
@@ -859,27 +792,17 @@
}
@Test
- public void testStartStopDozing() {
- mStatusBar.setBarStateForTest(StatusBarState.KEYGUARD);
- when(mStatusBarStateController.isKeyguardRequested()).thenReturn(true);
-
- mStatusBar.mDozeServiceHost.startDozing();
- verify(mStatusBarStateController).setIsDozing(eq(true));
-
- mStatusBar.mDozeServiceHost.stopDozing();
- verify(mStatusBarStateController).setIsDozing(eq(false));
- }
-
- @Test
public void testOnStartedWakingUp_isNotDozing() {
mStatusBar.setBarStateForTest(StatusBarState.KEYGUARD);
when(mStatusBarStateController.isKeyguardRequested()).thenReturn(true);
- mStatusBar.mDozeServiceHost.startDozing();
- verify(mStatusBarStateController).setIsDozing(eq(true));
+ when(mDozeServiceHost.getDozingRequested()).thenReturn(true);
+ mStatusBar.updateIsKeyguard();
+ // TODO: mNotificationPanelView.expand(false) gets called twice. Should be once.
+ verify(mNotificationPanelView, times(2)).expand(eq(false));
clearInvocations(mNotificationPanelView);
mStatusBar.mWakefulnessObserver.onStartedWakingUp();
- verify(mStatusBarStateController).setIsDozing(eq(false));
+ verify(mDozeServiceHost).stopDozing();
verify(mNotificationPanelView).expand(eq(false));
}
@@ -887,7 +810,8 @@
public void testOnStartedWakingUp_doesNotDismissBouncer_whenPulsing() {
mStatusBar.setBarStateForTest(StatusBarState.KEYGUARD);
when(mStatusBarStateController.isKeyguardRequested()).thenReturn(true);
- mStatusBar.mDozeServiceHost.startDozing();
+ when(mDozeServiceHost.getDozingRequested()).thenReturn(true);
+ mStatusBar.updateIsKeyguard();
clearInvocations(mNotificationPanelView);
mStatusBar.setBouncerShowing(true);
diff --git a/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java b/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java
new file mode 100644
index 0000000..99b1ef4
--- /dev/null
+++ b/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java
@@ -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);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/notification/NotificationHistoryFilter.java b/services/core/java/com/android/server/notification/NotificationHistoryFilter.java
new file mode 100644
index 0000000..c3b2e73
--- /dev/null
+++ b/services/core/java/com/android/server/notification/NotificationHistoryFilter.java
@@ -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;
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/notification/NotificationHistoryProtoHelper.java b/services/core/java/com/android/server/notification/NotificationHistoryProtoHelper.java
new file mode 100644
index 0000000..2831d37
--- /dev/null
+++ b/services/core/java/com/android/server/notification/NotificationHistoryProtoHelper.java
@@ -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();
+ }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java
new file mode 100644
index 0000000..bcff2f8
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java
@@ -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();
+ }
+
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryFilterTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryFilterTest.java
new file mode 100644
index 0000000..10bfcf1
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryFilterTest.java
@@ -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();
+ }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryProtoHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryProtoHelperTest.java
new file mode 100644
index 0000000..458117d
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryProtoHelperTest.java
@@ -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);
+ }
+}
diff --git a/tests/testables/src/android/testing/TestableSettingsProvider.java b/tests/testables/src/android/testing/TestableSettingsProvider.java
index b158476..fd92c65 100644
--- a/tests/testables/src/android/testing/TestableSettingsProvider.java
+++ b/tests/testables/src/android/testing/TestableSettingsProvider.java
@@ -14,6 +14,8 @@
package android.testing;
+import static org.junit.Assert.assertEquals;
+
import android.content.ContentProviderClient;
import android.content.Context;
import android.os.Bundle;
@@ -25,8 +27,6 @@
import java.util.HashMap;
-import static org.junit.Assert.*;
-
/**
* Allows calls to android.provider.Settings to be tested easier.
*
@@ -71,7 +71,7 @@
public Bundle call(String method, String arg, Bundle extras) {
// Methods are "GET_system", "GET_global", "PUT_secure", etc.
- final int userId = extras.getInt(Settings.CALL_METHOD_USER_KEY, 0);
+ final int userId = extras.getInt(Settings.CALL_METHOD_USER_KEY, UserHandle.myUserId());
final String[] commands = method.split("_", 2);
final String op = commands[0];
final String table = commands[1];