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