Merge "Move logic from QSCarrierGroup into Controller."
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
index 3aef5d1..3fdc571 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
@@ -50,12 +50,11 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.provider.Settings;
-import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.KeyValueListParser;
 import android.util.Log;
 import android.util.Slog;
-import android.util.SparseArray;
+import android.util.SparseArrayMap;
 import android.util.SparseBooleanArray;
 import android.util.SparseSetArray;
 import android.util.proto.ProtoOutputStream;
@@ -114,92 +113,6 @@
     private static final String ALARM_TAG_QUOTA_CHECK = "*job.quota_check*";
 
     /**
-     * A sparse array of ArrayMaps, which is suitable for holding (userId, packageName)->object
-     * associations.
-     */
-    private static class UserPackageMap<T> {
-        private final SparseArray<ArrayMap<String, T>> mData = new SparseArray<>();
-
-        public void add(int userId, @NonNull String packageName, @Nullable T obj) {
-            ArrayMap<String, T> data = mData.get(userId);
-            if (data == null) {
-                data = new ArrayMap<String, T>();
-                mData.put(userId, data);
-            }
-            data.put(packageName, obj);
-        }
-
-        public void clear() {
-            for (int i = 0; i < mData.size(); ++i) {
-                mData.valueAt(i).clear();
-            }
-        }
-
-        /** Removes all the data for the user, if there was any. */
-        public void delete(int userId) {
-            mData.delete(userId);
-        }
-
-        /** Removes the data for the user and package, if there was any. */
-        public void delete(int userId, @NonNull String packageName) {
-            ArrayMap<String, T> data = mData.get(userId);
-            if (data != null) {
-                data.remove(packageName);
-            }
-        }
-
-        @Nullable
-        public T get(int userId, @NonNull String packageName) {
-            ArrayMap<String, T> data = mData.get(userId);
-            if (data != null) {
-                return data.get(packageName);
-            }
-            return null;
-        }
-
-        /** @see SparseArray#indexOfKey */
-        public int indexOfKey(int userId) {
-            return mData.indexOfKey(userId);
-        }
-
-        /** Returns the userId at the given index. */
-        public int keyAt(int index) {
-            return mData.keyAt(index);
-        }
-
-        /** Returns the package name at the given index. */
-        @NonNull
-        public String keyAt(int userIndex, int packageIndex) {
-            return mData.valueAt(userIndex).keyAt(packageIndex);
-        }
-
-        /** Returns the size of the outer (userId) array. */
-        public int numUsers() {
-            return mData.size();
-        }
-
-        public int numPackagesForUser(int userId) {
-            ArrayMap<String, T> data = mData.get(userId);
-            return data == null ? 0 : data.size();
-        }
-
-        /** Returns the value T at the given user and index. */
-        @Nullable
-        public T valueAt(int userIndex, int packageIndex) {
-            return mData.valueAt(userIndex).valueAt(packageIndex);
-        }
-
-        public void forEach(Consumer<T> consumer) {
-            for (int i = numUsers() - 1; i >= 0; --i) {
-                ArrayMap<String, T> data = mData.valueAt(i);
-                for (int j = data.size() - 1; j >= 0; --j) {
-                    consumer.accept(data.valueAt(j));
-                }
-            }
-        }
-    }
-
-    /**
      * Standardize the output of userId-packageName combo.
      */
     private static String string(int userId, String packageName) {
@@ -378,22 +291,22 @@
     }
 
     /** List of all tracked jobs keyed by source package-userId combo. */
-    private final UserPackageMap<ArraySet<JobStatus>> mTrackedJobs = new UserPackageMap<>();
+    private final SparseArrayMap<ArraySet<JobStatus>> mTrackedJobs = new SparseArrayMap<>();
 
     /** Timer for each package-userId combo. */
-    private final UserPackageMap<Timer> mPkgTimers = new UserPackageMap<>();
+    private final SparseArrayMap<Timer> mPkgTimers = new SparseArrayMap<>();
 
     /** List of all timing sessions for a package-userId combo, in chronological order. */
-    private final UserPackageMap<List<TimingSession>> mTimingSessions = new UserPackageMap<>();
+    private final SparseArrayMap<List<TimingSession>> mTimingSessions = new SparseArrayMap<>();
 
     /**
      * List of alarm listeners for each package that listen for when each package comes back within
      * quota.
      */
-    private final UserPackageMap<QcAlarmListener> mInQuotaAlarmListeners = new UserPackageMap<>();
+    private final SparseArrayMap<QcAlarmListener> mInQuotaAlarmListeners = new SparseArrayMap<>();
 
     /** Cached calculation results for each app, with the standby buckets as the array indices. */
-    private final UserPackageMap<ExecutionStats[]> mExecutionStatsCache = new UserPackageMap<>();
+    private final SparseArrayMap<ExecutionStats[]> mExecutionStatsCache = new SparseArrayMap<>();
 
     /** List of UIDs currently in the foreground. */
     private final SparseBooleanArray mForegroundUids = new SparseBooleanArray();
@@ -1206,9 +1119,9 @@
 
     private void maybeUpdateAllConstraintsLocked() {
         boolean changed = false;
-        for (int u = 0; u < mTrackedJobs.numUsers(); ++u) {
+        for (int u = 0; u < mTrackedJobs.numMaps(); ++u) {
             final int userId = mTrackedJobs.keyAt(u);
-            for (int p = 0; p < mTrackedJobs.numPackagesForUser(userId); ++p) {
+            for (int p = 0; p < mTrackedJobs.numElementsForKey(userId); ++p) {
                 final String packageName = mTrackedJobs.keyAt(u, p);
                 changed |= maybeUpdateConstraintForPkgLocked(userId, packageName);
             }
@@ -1268,7 +1181,7 @@
     }
 
     private class UidConstraintUpdater implements Consumer<JobStatus> {
-        private final UserPackageMap<Integer> mToScheduleStartAlarms = new UserPackageMap<>();
+        private final SparseArrayMap<Integer> mToScheduleStartAlarms = new SparseArrayMap<>();
         public boolean wasJobChanged;
 
         @Override
@@ -1290,9 +1203,9 @@
         }
 
         void postProcess() {
-            for (int u = 0; u < mToScheduleStartAlarms.numUsers(); ++u) {
+            for (int u = 0; u < mToScheduleStartAlarms.numMaps(); ++u) {
                 final int userId = mToScheduleStartAlarms.keyAt(u);
-                for (int p = 0; p < mToScheduleStartAlarms.numPackagesForUser(userId); ++p) {
+                for (int p = 0; p < mToScheduleStartAlarms.numElementsForKey(userId); ++p) {
                     final String packageName = mToScheduleStartAlarms.keyAt(u, p);
                     final int standbyBucket = mToScheduleStartAlarms.get(userId, packageName);
                     maybeScheduleStartAlarmLocked(userId, packageName, standbyBucket);
@@ -2547,9 +2460,9 @@
         });
 
         pw.println();
-        for (int u = 0; u < mPkgTimers.numUsers(); ++u) {
+        for (int u = 0; u < mPkgTimers.numMaps(); ++u) {
             final int userId = mPkgTimers.keyAt(u);
-            for (int p = 0; p < mPkgTimers.numPackagesForUser(userId); ++p) {
+            for (int p = 0; p < mPkgTimers.numElementsForKey(userId); ++p) {
                 final String pkgName = mPkgTimers.keyAt(u, p);
                 mPkgTimers.valueAt(u, p).dump(pw, predicate);
                 pw.println();
@@ -2571,9 +2484,9 @@
 
         pw.println("Cached execution stats:");
         pw.increaseIndent();
-        for (int u = 0; u < mExecutionStatsCache.numUsers(); ++u) {
+        for (int u = 0; u < mExecutionStatsCache.numMaps(); ++u) {
             final int userId = mExecutionStatsCache.keyAt(u);
-            for (int p = 0; p < mExecutionStatsCache.numPackagesForUser(userId); ++p) {
+            for (int p = 0; p < mExecutionStatsCache.numElementsForKey(userId); ++p) {
                 final String pkgName = mExecutionStatsCache.keyAt(u, p);
                 ExecutionStats[] stats = mExecutionStatsCache.valueAt(u, p);
 
@@ -2595,9 +2508,9 @@
         pw.println();
         pw.println("In quota alarms:");
         pw.increaseIndent();
-        for (int u = 0; u < mInQuotaAlarmListeners.numUsers(); ++u) {
+        for (int u = 0; u < mInQuotaAlarmListeners.numMaps(); ++u) {
             final int userId = mInQuotaAlarmListeners.keyAt(u);
-            for (int p = 0; p < mInQuotaAlarmListeners.numPackagesForUser(userId); ++p) {
+            for (int p = 0; p < mInQuotaAlarmListeners.numElementsForKey(userId); ++p) {
                 final String pkgName = mInQuotaAlarmListeners.keyAt(u, p);
                 QcAlarmListener alarmListener = mInQuotaAlarmListeners.valueAt(u, p);
 
@@ -2667,9 +2580,9 @@
             }
         });
 
-        for (int u = 0; u < mPkgTimers.numUsers(); ++u) {
+        for (int u = 0; u < mPkgTimers.numMaps(); ++u) {
             final int userId = mPkgTimers.keyAt(u);
-            for (int p = 0; p < mPkgTimers.numPackagesForUser(userId); ++p) {
+            for (int p = 0; p < mPkgTimers.numElementsForKey(userId); ++p) {
                 final String pkgName = mPkgTimers.keyAt(u, p);
                 final long psToken = proto.start(
                         StateControllerProto.QuotaController.PACKAGE_STATS);
diff --git a/api/current.txt b/api/current.txt
index 8c27430..85cc06c 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -48769,6 +48769,7 @@
     method public void append(int, E);
     method public void clear();
     method public android.util.SparseArray<E> clone();
+    method public boolean contains(int);
     method public void delete(int);
     method public E get(int);
     method public E get(int, E);
diff --git a/api/test-current.txt b/api/test-current.txt
index c6b02b9..19e1212 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -4245,6 +4245,25 @@
     field public static final String SETTINGS_WIFITRACKER2 = "settings_wifitracker2";
   }
 
+  public class SparseArrayMap<T> {
+    ctor public SparseArrayMap();
+    method public void add(int, @NonNull String, @Nullable T);
+    method public void clear();
+    method public boolean contains(int, @NonNull String);
+    method public void delete(int);
+    method @Nullable public T delete(int, @NonNull String);
+    method public void forEach(@NonNull java.util.function.Consumer<T>);
+    method @Nullable public T get(int, @NonNull String);
+    method @Nullable public T getOrDefault(int, @NonNull String, T);
+    method public int indexOfKey(int);
+    method public int indexOfKey(int, @NonNull String);
+    method public int keyAt(int);
+    method @NonNull public String keyAt(int, int);
+    method public int numElementsForKey(int);
+    method public int numMaps();
+    method @Nullable public T valueAt(int, int);
+  }
+
   public class TimeUtils {
     method public static String formatDuration(long);
   }
diff --git a/cmds/statsd/src/StatsLogProcessor.cpp b/cmds/statsd/src/StatsLogProcessor.cpp
index 91cadc9..98d41c2 100644
--- a/cmds/statsd/src/StatsLogProcessor.cpp
+++ b/cmds/statsd/src/StatsLogProcessor.cpp
@@ -328,11 +328,6 @@
                                mAnomalyAlarmMonitor, mPeriodicAlarmMonitor);
     if (newMetricsManager->isConfigValid()) {
         mUidMap->OnConfigUpdated(key);
-        if (newMetricsManager->shouldAddUidMapListener()) {
-            // We have to add listener after the MetricsManager is constructed because it's
-            // not safe to create wp or sp from this pointer inside its constructor.
-            mUidMap->addListener(newMetricsManager.get());
-        }
         newMetricsManager->refreshTtl(timestampNs);
         mMetricsManagers[key] = newMetricsManager;
         VLOG("StatsdConfig valid");
@@ -753,6 +748,32 @@
     }
 }
 
+void StatsLogProcessor::notifyAppUpgrade(const int64_t& eventTimeNs, const string& apk,
+                                         const int uid, const int64_t version) {
+    std::lock_guard<std::mutex> lock(mMetricsMutex);
+    ALOGW("Received app upgrade");
+    for (auto it : mMetricsManagers) {
+        it.second->notifyAppUpgrade(eventTimeNs, apk, uid, version);
+    }
+}
+
+void StatsLogProcessor::notifyAppRemoved(const int64_t& eventTimeNs, const string& apk,
+                                         const int uid) {
+    std::lock_guard<std::mutex> lock(mMetricsMutex);
+    ALOGW("Received app removed");
+    for (auto it : mMetricsManagers) {
+        it.second->notifyAppRemoved(eventTimeNs, apk, uid);
+    }
+}
+
+void StatsLogProcessor::onUidMapReceived(const int64_t& eventTimeNs) {
+    std::lock_guard<std::mutex> lock(mMetricsMutex);
+    ALOGW("Received uid map");
+    for (auto it : mMetricsManagers) {
+        it.second->onUidMapReceived(eventTimeNs);
+    }
+}
+
 void StatsLogProcessor::noteOnDiskData(const ConfigKey& key) {
     std::lock_guard<std::mutex> lock(mMetricsMutex);
     mOnDiskDataConfigs.insert(key);
diff --git a/cmds/statsd/src/StatsLogProcessor.h b/cmds/statsd/src/StatsLogProcessor.h
index b41771d..68a51ef 100644
--- a/cmds/statsd/src/StatsLogProcessor.h
+++ b/cmds/statsd/src/StatsLogProcessor.h
@@ -32,7 +32,7 @@
 namespace statsd {
 
 
-class StatsLogProcessor : public ConfigListener {
+class StatsLogProcessor : public ConfigListener, public virtual PackageInfoListener {
 public:
     StatsLogProcessor(const sp<UidMap>& uidMap, const sp<StatsPullerManager>& pullerManager,
                       const sp<AlarmMonitor>& anomalyAlarmMonitor,
@@ -91,6 +91,16 @@
     /* Sets the active status/ttl for all configs and metrics to the status in ActiveConfigList. */
     void SetConfigsActiveState(const ActiveConfigList& activeConfigList, int64_t currentTimeNs);
 
+    /* Notify all MetricsManagers of app upgrades */
+    void notifyAppUpgrade(const int64_t& eventTimeNs, const string& apk, const int uid,
+                          const int64_t version) override;
+
+    /* Notify all MetricsManagers of app removals */
+    void notifyAppRemoved(const int64_t& eventTimeNs, const string& apk, const int uid) override;
+
+    /* Notify all MetricsManagers of uid map snapshots received */
+    void onUidMapReceived(const int64_t& eventTimeNs) override;
+
     // Reset all configs.
     void resetConfigs();
 
diff --git a/cmds/statsd/src/StatsService.cpp b/cmds/statsd/src/StatsService.cpp
index f072c9c..2c325ba6 100644
--- a/cmds/statsd/src/StatsService.cpp
+++ b/cmds/statsd/src/StatsService.cpp
@@ -200,6 +200,7 @@
                 }
             });
 
+    mUidMap->setListener(mProcessor);
     mConfigManager->AddListener(mProcessor);
 
     init_system_properties();
diff --git a/cmds/statsd/src/metrics/MetricProducer.h b/cmds/statsd/src/metrics/MetricProducer.h
index d7cbcc8..a513db6 100644
--- a/cmds/statsd/src/metrics/MetricProducer.h
+++ b/cmds/statsd/src/metrics/MetricProducer.h
@@ -87,7 +87,7 @@
 // writing the report to dropbox. MetricProducers should respond to package changes as required in
 // PackageInfoListener, but if none of the metrics are slicing by package name, then the update can
 // be a no-op.
-class MetricProducer : public virtual PackageInfoListener, public virtual StateListener {
+class MetricProducer : public virtual android::RefBase, public virtual StateListener {
 public:
     MetricProducer(const int64_t& metricId, const ConfigKey& key, const int64_t timeBaseNs,
                    const int conditionIndex, const sp<ConditionWizard>& wizard,
@@ -109,8 +109,8 @@
      * the flush again when the end timestamp is forced to be now, and then after flushing, update
      * the start timestamp to be now.
      */
-    void notifyAppUpgrade(const int64_t& eventTimeNs, const string& apk, const int uid,
-                          const int64_t version) override {
+    virtual void notifyAppUpgrade(const int64_t& eventTimeNs, const string& apk, const int uid,
+                          const int64_t version) {
         std::lock_guard<std::mutex> lock(mMutex);
 
         if (eventTimeNs > getCurrentBucketEndTimeNs()) {
@@ -123,16 +123,11 @@
         // is a partial bucket and can merge it with the previous bucket.
     };
 
-    void notifyAppRemoved(const int64_t& eventTimeNs, const string& apk, const int uid) override{
+    void notifyAppRemoved(const int64_t& eventTimeNs, const string& apk, const int uid) {
         // Force buckets to split on removal also.
         notifyAppUpgrade(eventTimeNs, apk, uid, 0);
     };
 
-    void onUidMapReceived(const int64_t& eventTimeNs) override{
-            // Purposefully don't flush partial buckets on a new snapshot.
-            // This occurs if a new user is added/removed or statsd crashes.
-    };
-
     // Consume the parsed stats log entry that already matched the "what" of the metric.
     void onMatchedLogEvent(const size_t matcherIndex, const LogEvent& event) {
         std::lock_guard<std::mutex> lock(mMutex);
diff --git a/cmds/statsd/src/metrics/MetricsManager.cpp b/cmds/statsd/src/metrics/MetricsManager.cpp
index 7bae4b9..464cec3 100644
--- a/cmds/statsd/src/metrics/MetricsManager.cpp
+++ b/cmds/statsd/src/metrics/MetricsManager.cpp
@@ -182,6 +182,10 @@
 
 void MetricsManager::notifyAppUpgrade(const int64_t& eventTimeNs, const string& apk, const int uid,
                                       const int64_t version) {
+    // Inform all metric producers.
+    for (auto it : mAllMetricProducers) {
+        it->notifyAppUpgrade(eventTimeNs, apk, uid, version);
+    }
     // check if we care this package
     if (std::find(mAllowedPkg.begin(), mAllowedPkg.end(), apk) == mAllowedPkg.end()) {
         return;
@@ -193,6 +197,10 @@
 
 void MetricsManager::notifyAppRemoved(const int64_t& eventTimeNs, const string& apk,
                                       const int uid) {
+    // Inform all metric producers.
+    for (auto it : mAllMetricProducers) {
+        it->notifyAppRemoved(eventTimeNs, apk, uid);
+    }
     // check if we care this package
     if (std::find(mAllowedPkg.begin(), mAllowedPkg.end(), apk) == mAllowedPkg.end()) {
         return;
@@ -203,6 +211,9 @@
 }
 
 void MetricsManager::onUidMapReceived(const int64_t& eventTimeNs) {
+    // Purposefully don't inform metric producers on a new snapshot
+    // because we don't need to flush partial buckets.
+    // This occurs if a new user is added/removed or statsd crashes.
     if (mAllowedPkg.size() == 0) {
         return;
     }
diff --git a/cmds/statsd/src/metrics/MetricsManager.h b/cmds/statsd/src/metrics/MetricsManager.h
index 286610a..1fda696 100644
--- a/cmds/statsd/src/metrics/MetricsManager.h
+++ b/cmds/statsd/src/metrics/MetricsManager.h
@@ -35,7 +35,7 @@
 namespace statsd {
 
 // A MetricsManager is responsible for managing metrics from one single config source.
-class MetricsManager : public PackageInfoListener {
+class MetricsManager : public virtual android::RefBase {
 public:
     MetricsManager(const ConfigKey& configKey, const StatsdConfig& config, const int64_t timeBaseNs,
                    const int64_t currentTimeNs, const sp<UidMap>& uidMap,
@@ -63,15 +63,11 @@
         unordered_set<sp<const InternalAlarm>, SpHash<InternalAlarm>>& alarmSet);
 
     void notifyAppUpgrade(const int64_t& eventTimeNs, const string& apk, const int uid,
-                          const int64_t version) override;
+                          const int64_t version);
 
-    void notifyAppRemoved(const int64_t& eventTimeNs, const string& apk, const int uid) override;
+    void notifyAppRemoved(const int64_t& eventTimeNs, const string& apk, const int uid);
 
-    void onUidMapReceived(const int64_t& eventTimeNs) override;
-
-    bool shouldAddUidMapListener() const {
-        return !mAllowedPkg.empty();
-    }
+    void onUidMapReceived(const int64_t& eventTimeNs);
 
     bool shouldWriteToDisk() const {
         return mNoReportMetricIds.size() != mAllMetricProducers.size();
diff --git a/cmds/statsd/src/metrics/metrics_manager_util.cpp b/cmds/statsd/src/metrics/metrics_manager_util.cpp
index 6e76717..9131802 100644
--- a/cmds/statsd/src/metrics/metrics_manager_util.cpp
+++ b/cmds/statsd/src/metrics/metrics_manager_util.cpp
@@ -396,7 +396,7 @@
 }
 
 bool initMetrics(const ConfigKey& key, const StatsdConfig& config, const int64_t timeBaseTimeNs,
-                 const int64_t currentTimeNs, UidMap& uidMap,
+                 const int64_t currentTimeNs,
                  const sp<StatsPullerManager>& pullerManager,
                  const unordered_map<int64_t, int>& logTrackerMap,
                  const unordered_map<int64_t, int>& conditionTrackerMap,
@@ -788,8 +788,6 @@
         noReportMetricIds.insert(no_report_metric);
     }
     for (const auto& it : allMetricProducers) {
-        uidMap.addListener(it);
-
         // Register metrics to StateTrackers
         for (int atomId : it->getSlicedStateAtoms()) {
             if (!StateManager::getInstance().registerListener(atomId, it)) {
@@ -939,7 +937,7 @@
         ALOGE("initStates failed");
         return false;
     }
-    if (!initMetrics(key, config, timeBaseNs, currentTimeNs, uidMap, pullerManager, logTrackerMap,
+    if (!initMetrics(key, config, timeBaseNs, currentTimeNs, pullerManager, logTrackerMap,
                      conditionTrackerMap, allAtomMatchers, stateAtomIdMap, allStateGroupMaps,
                      allConditionTrackers, allMetricProducers,
                      conditionToMetricMap, trackerToMetricMap, metricProducerMap,
diff --git a/cmds/statsd/src/packages/UidMap.cpp b/cmds/statsd/src/packages/UidMap.cpp
index d4b57dd..7e63bbf 100644
--- a/cmds/statsd/src/packages/UidMap.cpp
+++ b/cmds/statsd/src/packages/UidMap.cpp
@@ -119,7 +119,7 @@
 void UidMap::updateMap(const int64_t& timestamp, const vector<int32_t>& uid,
                        const vector<int64_t>& versionCode, const vector<String16>& versionString,
                        const vector<String16>& packageName, const vector<String16>& installer) {
-    vector<wp<PackageInfoListener>> broadcastList;
+    wp<PackageInfoListener> broadcast = NULL;
     {
         lock_guard<mutex> lock(mMutex);  // Exclusively lock for updates.
 
@@ -150,25 +150,22 @@
 
         ensureBytesUsedBelowLimit();
         StatsdStats::getInstance().setCurrentUidMapMemory(mBytesUsed);
-        getListenerListCopyLocked(&broadcastList);
+        broadcast = mSubscriber;
     }
     // To avoid invoking callback while holding the internal lock. we get a copy of the listener
-    // list and invoke the callback. It's still possible that after we copy the list, a
-    // listener removes itself before we call it. It's then the listener's job to handle it (expect
-    // the callback to be called after listener is removed, and the listener should properly
-    // ignore it).
-    for (const auto& weakPtr : broadcastList) {
-        auto strongPtr = weakPtr.promote();
-        if (strongPtr != NULL) {
-            strongPtr->onUidMapReceived(timestamp);
-        }
+    // and invoke the callback. It's still possible that after we copy the listener, it removes
+    // itself before we call it. It's then the listener's job to handle it (expect the callback to
+    // be called after listener is removed, and the listener should properly ignore it).
+    auto strongPtr = broadcast.promote();
+    if (strongPtr != NULL) {
+        strongPtr->onUidMapReceived(timestamp);
     }
 }
 
 void UidMap::updateApp(const int64_t& timestamp, const String16& app_16, const int32_t& uid,
                        const int64_t& versionCode, const String16& versionString,
                        const String16& installer) {
-    vector<wp<PackageInfoListener>> broadcastList;
+    wp<PackageInfoListener> broadcast = NULL;
     string appName = string(String8(app_16).string());
     {
         lock_guard<mutex> lock(mMutex);
@@ -195,7 +192,7 @@
             // for the first time, then we don't notify the listeners.
             // It's also OK to split again if we're forming a partial bucket after re-installing an
             // app after deletion.
-            getListenerListCopyLocked(&broadcastList);
+            broadcast = mSubscriber;
         }
         mChanges.emplace_back(false, timestamp, appName, uid, versionCode, newVersionString,
                               prevVersion, prevVersionString);
@@ -205,11 +202,9 @@
         StatsdStats::getInstance().setUidMapChanges(mChanges.size());
     }
 
-    for (const auto& weakPtr : broadcastList) {
-        auto strongPtr = weakPtr.promote();
-        if (strongPtr != NULL) {
-            strongPtr->notifyAppUpgrade(timestamp, appName, uid, versionCode);
-        }
+    auto strongPtr = broadcast.promote();
+    if (strongPtr != NULL) {
+        strongPtr->notifyAppUpgrade(timestamp, appName, uid, versionCode);
     }
 }
 
@@ -230,21 +225,8 @@
     }
 }
 
-void UidMap::getListenerListCopyLocked(vector<wp<PackageInfoListener>>* output) {
-    for (auto weakIt = mSubscribers.begin(); weakIt != mSubscribers.end();) {
-        auto strongPtr = weakIt->promote();
-        if (strongPtr != NULL) {
-            output->push_back(*weakIt);
-            weakIt++;
-        } else {
-            weakIt = mSubscribers.erase(weakIt);
-            VLOG("The UidMap listener is gone, remove it now");
-        }
-    }
-}
-
 void UidMap::removeApp(const int64_t& timestamp, const String16& app_16, const int32_t& uid) {
-    vector<wp<PackageInfoListener>> broadcastList;
+    wp<PackageInfoListener> broadcast = NULL;
     string app = string(String8(app_16).string());
     {
         lock_guard<mutex> lock(mMutex);
@@ -271,25 +253,18 @@
         ensureBytesUsedBelowLimit();
         StatsdStats::getInstance().setCurrentUidMapMemory(mBytesUsed);
         StatsdStats::getInstance().setUidMapChanges(mChanges.size());
-        getListenerListCopyLocked(&broadcastList);
+        broadcast = mSubscriber;
     }
 
-    for (const auto& weakPtr : broadcastList) {
-        auto strongPtr = weakPtr.promote();
-        if (strongPtr != NULL) {
-            strongPtr->notifyAppRemoved(timestamp, app, uid);
-        }
+    auto strongPtr = broadcast.promote();
+    if (strongPtr != NULL) {
+        strongPtr->notifyAppRemoved(timestamp, app, uid);
     }
 }
 
-void UidMap::addListener(wp<PackageInfoListener> producer) {
+void UidMap::setListener(wp<PackageInfoListener> listener) {
     lock_guard<mutex> lock(mMutex);  // Lock for updates
-    mSubscribers.insert(producer);
-}
-
-void UidMap::removeListener(wp<PackageInfoListener> producer) {
-    lock_guard<mutex> lock(mMutex);  // Lock for updates
-    mSubscribers.erase(producer);
+    mSubscriber = listener;
 }
 
 void UidMap::assignIsolatedUid(int isolatedUid, int parentUid) {
diff --git a/cmds/statsd/src/packages/UidMap.h b/cmds/statsd/src/packages/UidMap.h
index a7c5fb2..2d3f6ee 100644
--- a/cmds/statsd/src/packages/UidMap.h
+++ b/cmds/statsd/src/packages/UidMap.h
@@ -118,12 +118,10 @@
     // adb shell cmd stats print-uid-map
     void printUidMap(int outFd) const;
 
-    // Commands for indicating to the map that a producer should be notified if an app is updated.
-    // This allows the metric producer to distinguish when the same uid or app represents a
-    // different version of an app.
-    void addListener(wp<PackageInfoListener> producer);
-    // Remove the listener from the set of metric producers that subscribe to updates.
-    void removeListener(wp<PackageInfoListener> producer);
+    // Command for indicating to the map that StatsLogProcessor should be notified if an app is
+    // updated. This allows metric producers and managers to distinguish when the same uid or app
+    // represents a different version of an app.
+    void setListener(wp<PackageInfoListener> listener);
 
     // Informs uid map that a config is added/updated. Used for keeping mConfigKeys up to date.
     void OnConfigUpdated(const ConfigKey& key);
@@ -167,8 +165,6 @@
     std::set<string> getAppNamesFromUidLocked(const int32_t& uid, bool returnNormalized) const;
     string normalizeAppName(const string& appName) const;
 
-    void getListenerListCopyLocked(std::vector<wp<PackageInfoListener>>* output);
-
     void writeUidMapSnapshotLocked(int64_t timestamp, bool includeVersionStrings,
                                    bool includeInstaller, const std::set<int32_t>& interestingUids,
                                    std::set<string>* str_set, ProtoOutputStream* proto);
@@ -195,8 +191,8 @@
     // Store which uid and apps represent deleted ones.
     std::list<std::pair<int, string>> mDeletedApps;
 
-    // Metric producers that should be notified if there's an upgrade in any app.
-    set<wp<PackageInfoListener>> mSubscribers;
+    // Notify StatsLogProcessor if there's an upgrade/removal in any app.
+    wp<PackageInfoListener> mSubscriber;
 
     // Mapping of config keys we're aware of to the epoch time they last received an update. This
     // lets us know it's safe to delete events older than the oldest update. The value is nanosec.
diff --git a/cmds/statsd/tests/e2e/PartialBucket_e2e_test.cpp b/cmds/statsd/tests/e2e/PartialBucket_e2e_test.cpp
index 309d251..0bc3ebb 100644
--- a/cmds/statsd/tests/e2e/PartialBucket_e2e_test.cpp
+++ b/cmds/statsd/tests/e2e/PartialBucket_e2e_test.cpp
@@ -162,7 +162,10 @@
 
     ConfigMetricsReport report = GetReports(service.mProcessor, start + 4);
     backfillStartEndTimestamp(&report);
-    EXPECT_EQ(1, report.metrics_size());
+
+    ASSERT_EQ(1, report.metrics_size());
+    ASSERT_EQ(1, report.metrics(0).count_metrics().data_size());
+    ASSERT_EQ(1, report.metrics(0).count_metrics().data(0).bucket_info_size());
     EXPECT_TRUE(report.metrics(0).count_metrics().data(0).bucket_info(0).
                     has_start_bucket_elapsed_nanos());
     EXPECT_TRUE(report.metrics(0).count_metrics().data(0).bucket_info(0).
@@ -186,7 +189,10 @@
 
     ConfigMetricsReport report = GetReports(service.mProcessor, start + 4);
     backfillStartEndTimestamp(&report);
-    EXPECT_EQ(1, report.metrics_size());
+
+    ASSERT_EQ(1, report.metrics_size());
+    ASSERT_EQ(1, report.metrics(0).count_metrics().data_size());
+    ASSERT_EQ(1, report.metrics(0).count_metrics().data(0).bucket_info_size());
     EXPECT_TRUE(report.metrics(0).count_metrics().data(0).bucket_info(0).
                     has_start_bucket_elapsed_nanos());
     EXPECT_TRUE(report.metrics(0).count_metrics().data(0).bucket_info(0).
@@ -228,8 +234,9 @@
     ConfigMetricsReport report =
             GetReports(service.mProcessor, 5 * 60 * NS_PER_SEC + start + 100 * NS_PER_SEC, true);
     backfillStartEndTimestamp(&report);
-    EXPECT_EQ(1, report.metrics_size());
-    EXPECT_EQ(1, report.metrics(0).value_metrics().skipped_size());
+
+    ASSERT_EQ(1, report.metrics_size());
+    ASSERT_EQ(1, report.metrics(0).value_metrics().skipped_size());
     EXPECT_TRUE(report.metrics(0).value_metrics().skipped(0).has_start_bucket_elapsed_nanos());
     // Can't test the start time since it will be based on the actual time when the pulling occurs.
     EXPECT_EQ(MillisToNano(NanoToMillis(endSkipped)),
@@ -270,8 +277,8 @@
     ConfigMetricsReport report =
             GetReports(service.mProcessor, 5 * 60 * NS_PER_SEC + start + 100 * NS_PER_SEC, true);
     backfillStartEndTimestamp(&report);
-    EXPECT_EQ(1, report.metrics_size());
-    EXPECT_EQ(1, report.metrics(0).gauge_metrics().skipped_size());
+    ASSERT_EQ(1, report.metrics_size());
+    ASSERT_EQ(1, report.metrics(0).gauge_metrics().skipped_size());
     // Can't test the start time since it will be based on the actual time when the pulling occurs.
     EXPECT_TRUE(report.metrics(0).gauge_metrics().skipped(0).has_start_bucket_elapsed_nanos());
     EXPECT_EQ(MillisToNano(NanoToMillis(endSkipped)),
diff --git a/core/java/android/app/NotificationHistory.java b/core/java/android/app/NotificationHistory.java
index c35246b..8ba39a8 100644
--- a/core/java/android/app/NotificationHistory.java
+++ b/core/java/android/app/NotificationHistory.java
@@ -311,6 +311,14 @@
         mHistoryCount++;
     }
 
+    public void addNotificationsToWrite(@NonNull NotificationHistory notificationHistory) {
+        for (HistoricalNotification hn : notificationHistory.getNotificationsToWrite()) {
+            // TODO: consider merging by date
+            addNotificationToWrite(hn);
+        }
+        poolStringsFromNotifications();
+    }
+
     /**
      * Removes a package's historical notifications and regenerates the string pool
      */
diff --git a/core/java/android/service/contentsuggestions/ContentSuggestionsService.java b/core/java/android/service/contentsuggestions/ContentSuggestionsService.java
index 4bcd39f..306b483 100644
--- a/core/java/android/service/contentsuggestions/ContentSuggestionsService.java
+++ b/core/java/android/service/contentsuggestions/ContentSuggestionsService.java
@@ -64,6 +64,10 @@
         @Override
         public void provideContextImage(int taskId, GraphicBuffer contextImage,
                 int colorSpaceId, Bundle imageContextRequestExtras) {
+            if (imageContextRequestExtras.containsKey(ContentSuggestionsManager.EXTRA_BITMAP)
+                    && contextImage != null) {
+                throw new IllegalArgumentException("Two bitmaps provided; expected one.");
+            }
 
             Bitmap wrappedBuffer = null;
             if (imageContextRequestExtras.containsKey(ContentSuggestionsManager.EXTRA_BITMAP)) {
diff --git a/core/java/android/util/SparseArray.java b/core/java/android/util/SparseArray.java
index 0a15db2..484894f 100644
--- a/core/java/android/util/SparseArray.java
+++ b/core/java/android/util/SparseArray.java
@@ -104,6 +104,14 @@
     }
 
     /**
+     * Returns true if the key exists in the array. This is equivalent to
+     * {@link #indexOfKey(int)} >= 0.
+     */
+    public boolean contains(int key) {
+        return indexOfKey(key) >= 0;
+    }
+
+    /**
      * Gets the Object mapped from the specified key, or <code>null</code>
      * if no such mapping has been made.
      */
diff --git a/core/java/android/util/SparseArrayMap.java b/core/java/android/util/SparseArrayMap.java
new file mode 100644
index 0000000..3ec6b81
--- /dev/null
+++ b/core/java/android/util/SparseArrayMap.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 android.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+
+import java.util.function.Consumer;
+
+/**
+ * A sparse array of ArrayMaps, which is suitable for holding (userId, packageName)->object
+ * associations.
+ *
+ * @param <T> Any class
+ * @hide
+ */
+@TestApi
+public class SparseArrayMap<T> {
+    private final SparseArray<ArrayMap<String, T>> mData = new SparseArray<>();
+
+    /** Add an entry associating obj with the int-String pair. */
+    public void add(int key, @NonNull String mapKey, @Nullable T obj) {
+        ArrayMap<String, T> data = mData.get(key);
+        if (data == null) {
+            data = new ArrayMap<>();
+            mData.put(key, data);
+        }
+        data.put(mapKey, obj);
+    }
+
+    /** Remove all entries from the map. */
+    public void clear() {
+        for (int i = 0; i < mData.size(); ++i) {
+            mData.valueAt(i).clear();
+        }
+    }
+
+    /** Return true if the structure contains an explicit entry for the int-String pair. */
+    public boolean contains(int key, @NonNull String mapKey) {
+        return mData.contains(key) && mData.get(key).containsKey(mapKey);
+    }
+
+    /** Removes all the data for the key, if there was any. */
+    public void delete(int key) {
+        mData.delete(key);
+    }
+
+    /**
+     * Removes the data for the key and mapKey, if there was any.
+     *
+     * @return Returns the value that was stored under the keys, or null if there was none.
+     */
+    @Nullable
+    public T delete(int key, @NonNull String mapKey) {
+        ArrayMap<String, T> data = mData.get(key);
+        if (data != null) {
+            return data.remove(mapKey);
+        }
+        return null;
+    }
+
+    /**
+     * Get the value associated with the int-String pair.
+     */
+    @Nullable
+    public T get(int key, @NonNull String mapKey) {
+        ArrayMap<String, T> data = mData.get(key);
+        if (data != null) {
+            return data.get(mapKey);
+        }
+        return null;
+    }
+
+    /**
+     * Returns the value to which the specified key and mapKey are mapped, or defaultValue if this
+     * map contains no mapping for them.
+     */
+    @Nullable
+    public T getOrDefault(int key, @NonNull String mapKey, T defaultValue) {
+        if (mData.contains(key)) {
+            ArrayMap<String, T> data = mData.get(key);
+            if (data != null && data.containsKey(mapKey)) {
+                return data.get(mapKey);
+            }
+        }
+        return defaultValue;
+    }
+
+    /** @see SparseArray#indexOfKey */
+    public int indexOfKey(int key) {
+        return mData.indexOfKey(key);
+    }
+
+    /**
+     * Returns the index of the mapKey.
+     *
+     * @see SparseArray#indexOfKey
+     */
+    public int indexOfKey(int key, @NonNull String mapKey) {
+        ArrayMap<String, T> data = mData.get(key);
+        if (data != null) {
+            return data.indexOfKey(mapKey);
+        }
+        return -1;
+    }
+
+    /** Returns the key at the given index. */
+    public int keyAt(int index) {
+        return mData.keyAt(index);
+    }
+
+    /** Returns the map's key at the given mapIndex for the given keyIndex. */
+    @NonNull
+    public String keyAt(int keyIndex, int mapIndex) {
+        return mData.valueAt(keyIndex).keyAt(mapIndex);
+    }
+
+    /** Returns the size of the outer array. */
+    public int numMaps() {
+        return mData.size();
+    }
+
+    /** Returns the number of elements in the map of the given key. */
+    public int numElementsForKey(int key) {
+        ArrayMap<String, T> data = mData.get(key);
+        return data == null ? 0 : data.size();
+    }
+
+    /** Returns the value T at the given key and map index. */
+    @Nullable
+    public T valueAt(int keyIndex, int mapIndex) {
+        return mData.valueAt(keyIndex).valueAt(mapIndex);
+    }
+
+    /** Iterate through all int-String pairs and operate on all of the values. */
+    public void forEach(@NonNull Consumer<T> consumer) {
+        for (int i = numMaps() - 1; i >= 0; --i) {
+            ArrayMap<String, T> data = mData.valueAt(i);
+            for (int j = data.size() - 1; j >= 0; --j) {
+                consumer.accept(data.valueAt(j));
+            }
+        }
+    }
+}
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index ba29382..db76b71 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -10353,6 +10353,9 @@
      * the user. It is a supplement to the boolean states (for example, checked/unchecked) and
      * it is used for customized state description (for example, "wifi, connected, three bars").
      * State description changes frequently while content description should change less often.
+     * State description should be localized. For android widgets which have default state
+     * descriptions, app developers can call this method to override the state descriptions.
+     * Setting state description to null restores the default behavior.
      *
      * @param stateDescription The state description.
      * @see #getStateDescription()
diff --git a/core/proto/android/app/settings_enums.proto b/core/proto/android/app/settings_enums.proto
index 9e0c35a..1008a0d 100644
--- a/core/proto/android/app/settings_enums.proto
+++ b/core/proto/android/app/settings_enums.proto
@@ -2441,4 +2441,9 @@
     // CATEGORY: SETTINGS
     // OS: R
     NOTIFICATION_ACCESS_DETAIL = 1804;
+
+    // OPEN: Settings > Developer Options > Platform Compat
+    // CATEGORY: SETTINGS
+    // OS: R
+    SETTINGS_PLATFORM_COMPAT_DASHBOARD = 1805;
 }
diff --git a/core/tests/coretests/src/android/app/NotificationHistoryTest.java b/core/tests/coretests/src/android/app/NotificationHistoryTest.java
index 08595bb..f9a6a5c 100644
--- a/core/tests/coretests/src/android/app/NotificationHistoryTest.java
+++ b/core/tests/coretests/src/android/app/NotificationHistoryTest.java
@@ -114,6 +114,33 @@
     }
 
     @Test
+    public void testAddNotificationsToWrite() {
+        NotificationHistory history = new NotificationHistory();
+        HistoricalNotification n = getHistoricalNotification(0);
+        HistoricalNotification n2 = getHistoricalNotification(1);
+        history.addNotificationToWrite(n2);
+        history.addNotificationToWrite(n);
+
+        NotificationHistory secondHistory = new NotificationHistory();
+        HistoricalNotification n3 = getHistoricalNotification(2);
+        HistoricalNotification n4 = getHistoricalNotification(3);
+        secondHistory.addNotificationToWrite(n4);
+        secondHistory.addNotificationToWrite(n3);
+
+        history.addNotificationsToWrite(secondHistory);
+
+        assertThat(history.getNotificationsToWrite().size()).isEqualTo(4);
+        assertThat(history.getNotificationsToWrite().get(0)).isSameAs(n2);
+        assertThat(history.getNotificationsToWrite().get(1)).isSameAs(n);
+        assertThat(history.getNotificationsToWrite().get(2)).isSameAs(n4);
+        assertThat(history.getNotificationsToWrite().get(3)).isSameAs(n3);
+        assertThat(history.getHistoryCount()).isEqualTo(4);
+
+        assertThat(history.getPooledStringsToWrite()).asList().contains(n2.getChannelName());
+        assertThat(history.getPooledStringsToWrite()).asList().contains(n4.getPackage());
+    }
+
+    @Test
     public void testPoolStringsFromNotifications() {
         NotificationHistory history = new NotificationHistory();
 
diff --git a/packages/PackageInstaller/AndroidManifest.xml b/packages/PackageInstaller/AndroidManifest.xml
index 1c0e409..7d07fda 100644
--- a/packages/PackageInstaller/AndroidManifest.xml
+++ b/packages/PackageInstaller/AndroidManifest.xml
@@ -118,6 +118,7 @@
         </receiver>
 
         <activity android:name=".UninstallUninstalling"
+            android:theme="@style/Theme.AlertDialogActivity.NoActionBar"
             android:excludeFromRecents="true"
             android:exported="false" />
 
diff --git a/packages/PackageInstaller/res/values/themes.xml b/packages/PackageInstaller/res/values/themes.xml
index b11d28b..eecf9a1 100644
--- a/packages/PackageInstaller/res/values/themes.xml
+++ b/packages/PackageInstaller/res/values/themes.xml
@@ -17,7 +17,8 @@
 
 <resources>
 
-    <style name="Theme.AlertDialogActivity.NoAnimation">
+    <style name="Theme.AlertDialogActivity.NoAnimation"
+           parent="@style/Theme.AlertDialogActivity.NoActionBar">
         <item name="android:windowAnimationStyle">@null</item>
     </style>
 
diff --git a/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java b/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java
index 15a5c27..c1a23c8 100644
--- a/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java
+++ b/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java
@@ -27,6 +27,7 @@
 import com.android.systemui.dagger.qualifiers.MainHandler;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.util.Assert;
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
@@ -121,6 +122,8 @@
      * @param active whether the appOpCode is active or not
      */
     void onAppOpChanged(int appOpCode, int uid, String packageName, boolean active) {
+        Assert.isMainThread();
+
         int userId = UserHandle.getUserId(uid);
         // Record active app ops
         synchronized (mMutex) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java
index 02a3766..0990e22 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java
@@ -18,6 +18,7 @@
 
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertNull;
+import static junit.framework.TestCase.fail;
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -34,12 +35,14 @@
 import android.app.NotificationManager;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.Looper;
 import android.os.UserHandle;
 import android.service.notification.StatusBarNotification;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
 import android.widget.RemoteViews;
 
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.messages.nano.SystemMessageProto;
 import com.android.systemui.appops.AppOpsController;
@@ -58,7 +61,8 @@
 import org.mockito.MockitoAnnotations;
 
 @SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
 public class ForegroundServiceControllerTest extends SysuiTestCase {
     private ForegroundServiceController mFsc;
     private ForegroundServiceNotificationListener mListener;
@@ -69,6 +73,9 @@
 
     @Before
     public void setUp() throws Exception {
+        // assume the TestLooper is the main looper for these tests
+        com.android.systemui.util.Assert.sMainLooper = TestableLooper.get(this).getLooper();
+
         MockitoAnnotations.initMocks(this);
         mFsc = new ForegroundServiceController(mEntryManager, mAppOpsController, mMainHandler);
         mListener = new ForegroundServiceNotificationListener(
@@ -81,6 +88,26 @@
     }
 
     @Test
+    public void testAppOpsChangedCalledFromBgThread() {
+        try {
+            // WHEN onAppOpChanged is called from a different thread than the MainLooper
+            com.android.systemui.util.Assert.sMainLooper = Looper.getMainLooper();
+            NotificationEntry entry = createFgEntry();
+            mFsc.onAppOpChanged(
+                    AppOpsManager.OP_CAMERA,
+                    entry.getSbn().getUid(),
+                    entry.getSbn().getPackageName(),
+                    true);
+
+            // This test is run on the TestableLooper, which is not the MainLooper, so
+            // we expect an exception to be thrown
+            fail("onAppOpChanged shouldn't be allowed to be called from a bg thread.");
+        } catch (IllegalStateException e) {
+            // THEN expect an exception
+        }
+    }
+
+    @Test
     public void testAppOps_appOpChangedBeforeNotificationExists() {
         // GIVEN app op exists, but notification doesn't exist in NEM yet
         NotificationEntry entry = createFgEntry();
diff --git a/packages/overlays/IconShapeHeartOverlay/Android.mk b/packages/overlays/IconShapeHeartOverlay/Android.mk
new file mode 100644
index 0000000..20fe71f
--- /dev/null
+++ b/packages/overlays/IconShapeHeartOverlay/Android.mk
@@ -0,0 +1,29 @@
+#
+#  Copyright 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.
+#
+
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_RRO_THEME := IconShapeHeart
+
+LOCAL_PRODUCT_MODULE := true
+
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+
+LOCAL_PACKAGE_NAME := IconShapeHeartOverlay
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_RRO_PACKAGE)
diff --git a/packages/overlays/IconShapeHeartOverlay/AndroidManifest.xml b/packages/overlays/IconShapeHeartOverlay/AndroidManifest.xml
new file mode 100644
index 0000000..8fb19df
--- /dev/null
+++ b/packages/overlays/IconShapeHeartOverlay/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<!--
+/**
+ * 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.
+ */
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.theme.icon.heart"
+    android:versionCode="1"
+    android:versionName="1.0">
+<overlay
+        android:targetPackage="android"
+        android:targetName="IconShapeCustomization"
+        android:category="android.theme.customization.adaptive_icon_shape"
+        android:priority="1"/>
+
+    <application android:label="@string/icon_shape_heart_overlay" android:hasCode="false"/>
+</manifest>
diff --git a/packages/overlays/IconShapeHeartOverlay/res/values/config.xml b/packages/overlays/IconShapeHeartOverlay/res/values/config.xml
new file mode 100644
index 0000000..f9929f5
--- /dev/null
+++ b/packages/overlays/IconShapeHeartOverlay/res/values/config.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 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.
+*/
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- Specifies the path that is used by AdaptiveIconDrawable class to crop launcher icons. -->
+    <string name="config_icon_mask" translatable="false">"M50,20 C45,0 30,0 25,0 20,0 0,5 0,34 0,72 40,97 50,100 60,97 100,72 100,34 100,5 80,0 75,0 70,0 55,0 50,20 Z"</string>
+    <!-- Flag indicating whether round icons should be parsed from the application manifest. -->
+    <bool name="config_useRoundIcon">false</bool>
+    <!-- Corner radius of system dialogs -->
+    <dimen name="config_dialogCornerRadius">8dp</dimen>
+    <!-- Corner radius for bottom sheet system dialogs -->
+    <dimen name="config_bottomDialogCornerRadius">16dp</dimen>
+
+</resources>
+
diff --git a/packages/overlays/IconShapeHeartOverlay/res/values/strings.xml b/packages/overlays/IconShapeHeartOverlay/res/values/strings.xml
new file mode 100644
index 0000000..92c33fa
--- /dev/null
+++ b/packages/overlays/IconShapeHeartOverlay/res/values/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * 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.
+ */
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- Heart icon overlay -->
+    <string name="icon_shape_heart_overlay" translatable="false">Heart</string>
+
+</resources>
diff --git a/services/core/java/com/android/server/BinderCallsStatsService.java b/services/core/java/com/android/server/BinderCallsStatsService.java
index e510259..f2ce444 100644
--- a/services/core/java/com/android/server/BinderCallsStatsService.java
+++ b/services/core/java/com/android/server/BinderCallsStatsService.java
@@ -19,6 +19,7 @@
 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
 
+import android.app.ActivityThread;
 import android.content.Context;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
@@ -40,6 +41,7 @@
 import com.android.internal.os.BinderCallsStats;
 import com.android.internal.os.BinderInternal;
 import com.android.internal.os.CachedDeviceState;
+import com.android.internal.util.DumpUtils;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -49,6 +51,7 @@
 public class BinderCallsStatsService extends Binder {
 
     private static final String TAG = "BinderCallsStatsService";
+    private static final String SERVICE_NAME = "binder_calls_stats";
 
     private static final String PERSIST_SYS_BINDER_CALLS_DETAILED_TRACKING
             = "persist.sys.binder_calls_detailed_tracking";
@@ -246,7 +249,7 @@
             mService = new BinderCallsStatsService(
                     mBinderCallsStats, mWorkSourceProvider);
             publishLocalService(Internal.class, new Internal(mBinderCallsStats));
-            publishBinderService("binder_calls_stats", mService);
+            publishBinderService(SERVICE_NAME, mService);
             boolean detailedTrackingEnabled = SystemProperties.getBoolean(
                     PERSIST_SYS_BINDER_CALLS_DETAILED_TRACKING, false);
 
@@ -293,6 +296,11 @@
 
     @Override
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        if (!DumpUtils.checkDumpAndUsageStatsPermission(ActivityThread.currentApplication(),
+                SERVICE_NAME, pw)) {
+            return;
+        }
+
         boolean verbose = false;
         if (args != null) {
             for (final String arg : args) {
diff --git a/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java b/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java
index f05b2bf..378ca4a 100644
--- a/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java
+++ b/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java
@@ -75,7 +75,7 @@
     private final Context mContext;
     private final AlarmManager mAlarmManager;
     private final Object mLock = new Object();
-    private Handler mFileWriteHandler;
+    private final Handler mFileWriteHandler;
     @VisibleForTesting
     // List of files holding history information, sorted newest to oldest
     final LinkedList<AtomicFile> mHistoryFiles;
@@ -90,11 +90,12 @@
     @VisibleForTesting
     NotificationHistory mBuffer;
 
-    public NotificationHistoryDatabase(Context context, File dir,
+    public NotificationHistoryDatabase(Context context, Handler fileWriteHandler, File dir,
             FileAttrProvider fileAttrProvider) {
         mContext = context;
         mAlarmManager = context.getSystemService(AlarmManager.class);
         mCurrentVersion = DEFAULT_CURRENT_VERSION;
+        mFileWriteHandler = fileWriteHandler;
         mVersionFile = new File(dir, "version");
         mHistoryDir = new File(dir, "history");
         mHistoryFiles = new LinkedList<>();
@@ -107,10 +108,8 @@
         mContext.registerReceiver(mFileCleaupReceiver, deletionFilter);
     }
 
-    public void init(Handler fileWriteHandler) {
+    public void init() {
         synchronized (mLock) {
-            mFileWriteHandler = fileWriteHandler;
-
             try {
                 mHistoryDir.mkdir();
                 mVersionFile.createNewFile();
@@ -160,13 +159,13 @@
         }
     }
 
-    void forceWriteToDisk() {
+    public void forceWriteToDisk() {
         if (!mFileWriteHandler.hasCallbacks(mWriteBufferRunnable)) {
             mFileWriteHandler.post(mWriteBufferRunnable);
         }
     }
 
-    void onPackageRemoved(String packageName) {
+    public void onPackageRemoved(String packageName) {
         RemovePackageRunnable rpr = new RemovePackageRunnable(packageName);
         mFileWriteHandler.post(rpr);
     }
@@ -227,7 +226,7 @@
     /**
      * Remove any files that are too old and schedule jobs to clean up the rest
      */
-    public void prune(final int retentionDays, final long currentTimeMillis) {
+    void prune(final int retentionDays, final long currentTimeMillis) {
         synchronized (mLock) {
             GregorianCalendar retentionBoundary = new GregorianCalendar();
             retentionBoundary.setTimeInMillis(currentTimeMillis);
@@ -252,7 +251,7 @@
         }
     }
 
-    void scheduleDeletion(File file, long deletionTime) {
+    private void scheduleDeletion(File file, long deletionTime) {
         if (DEBUG) {
             Slog.d(TAG, "Scheduling deletion for " + file.getName() + " at " + deletionTime);
         }
diff --git a/services/core/java/com/android/server/notification/NotificationHistoryDatabaseFactory.java b/services/core/java/com/android/server/notification/NotificationHistoryDatabaseFactory.java
new file mode 100644
index 0000000..b4940a5
--- /dev/null
+++ b/services/core/java/com/android/server/notification/NotificationHistoryDatabaseFactory.java
@@ -0,0 +1,41 @@
+/*
+ * 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.content.Context;
+import android.os.Handler;
+
+import java.io.File;
+
+public class NotificationHistoryDatabaseFactory {
+
+    private static NotificationHistoryDatabase sTestingNotificationHistoryDb;
+
+    public static void setTestingNotificationHistoryDatabase(NotificationHistoryDatabase db) {
+        sTestingNotificationHistoryDb = db;
+    }
+
+    public static NotificationHistoryDatabase create(@NonNull Context context,
+            @NonNull Handler handler, @NonNull File rootDir,
+            @NonNull NotificationHistoryDatabase.FileAttrProvider fileAttrProvider) {
+        if(sTestingNotificationHistoryDb != null) {
+            return sTestingNotificationHistoryDb;
+        }
+        return new NotificationHistoryDatabase(context, handler, rootDir, fileAttrProvider);
+    }
+}
diff --git a/services/core/java/com/android/server/notification/NotificationHistoryManager.java b/services/core/java/com/android/server/notification/NotificationHistoryManager.java
new file mode 100644
index 0000000..a350a6b
--- /dev/null
+++ b/services/core/java/com/android/server/notification/NotificationHistoryManager.java
@@ -0,0 +1,245 @@
+/*
+ * 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.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.NotificationHistory;
+import android.app.NotificationHistory.HistoricalNotification;
+import android.content.Context;
+import android.os.Environment;
+import android.os.UserManager;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.IoThread;
+import com.android.server.notification.NotificationHistoryDatabase.NotificationHistoryFileAttrProvider;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Keeps track of per-user notification histories.
+ */
+public class NotificationHistoryManager {
+    private static final String TAG = "NotificationHistory";
+    private static final boolean DEBUG = NotificationManagerService.DBG;
+
+    @VisibleForTesting
+    static final String DIRECTORY_PER_USER = "notification_history";
+
+    private final Context mContext;
+    private final UserManager mUserManager;
+    private final Object mLock = new Object();
+    @GuardedBy("mLock")
+    private final SparseArray<NotificationHistoryDatabase> mUserState = new SparseArray<>();
+    @GuardedBy("mLock")
+    private final SparseBooleanArray mUserUnlockedStates = new SparseBooleanArray();
+    // TODO: does this need to be persisted across reboots?
+    @GuardedBy("mLock")
+    private final SparseArray<List<String>> mUserPendingPackageRemovals = new SparseArray<>();
+
+    public NotificationHistoryManager(Context context) {
+        mContext = context;
+        mUserManager = context.getSystemService(UserManager.class);
+    }
+
+    public void onUserUnlocked(@UserIdInt int userId) {
+        synchronized (mLock) {
+            mUserUnlockedStates.put(userId, true);
+            final NotificationHistoryDatabase userHistory =
+                    getUserHistoryAndInitializeIfNeededLocked(userId);
+            if (userHistory == null) {
+                Slog.i(TAG, "Attempted to unlock stopped or removed user " + userId);
+                return;
+            }
+
+            // remove any packages that were deleted while the user was locked
+            final List<String> pendingPackageRemovals = mUserPendingPackageRemovals.get(userId);
+            if (pendingPackageRemovals != null) {
+                for (int i = 0; i < pendingPackageRemovals.size(); i++) {
+                    userHistory.onPackageRemoved(pendingPackageRemovals.get(i));
+                }
+                mUserPendingPackageRemovals.put(userId, null);
+            }
+        }
+    }
+
+    public void onUserStopped(@UserIdInt int userId) {
+        synchronized (mLock) {
+            mUserUnlockedStates.put(userId, false);
+            mUserState.put(userId, null); // release the service (mainly for GC)
+        }
+    }
+
+    void onUserRemoved(@UserIdInt int userId) {
+        synchronized (mLock) {
+            // Actual data deletion is handled by other parts of the system (the entire directory is
+            // removed) - we just need clean up our internal state for GC
+            mUserPendingPackageRemovals.put(userId, null);
+            onUserStopped(userId);
+        }
+    }
+
+    void onPackageRemoved(int userId, String packageName) {
+        synchronized (mLock) {
+            if (!mUserUnlockedStates.get(userId, false)) {
+                List<String> userPendingRemovals =
+                        mUserPendingPackageRemovals.get(userId, new ArrayList<>());
+                userPendingRemovals.add(packageName);
+                mUserPendingPackageRemovals.put(userId, userPendingRemovals);
+                return;
+            }
+            final NotificationHistoryDatabase userHistory = mUserState.get(userId);
+            if (userHistory == null) {
+                return;
+            }
+
+            userHistory.onPackageRemoved(packageName);
+        }
+    }
+
+    void triggerWriteToDisk() {
+        synchronized (mLock) {
+            final int userCount = mUserState.size();
+            for (int i = 0; i < userCount; i++) {
+                final int userId = mUserState.keyAt(i);
+                if (!mUserUnlockedStates.get(userId)) {
+                    continue;
+                }
+                NotificationHistoryDatabase userHistory = mUserState.get(userId);
+                if (userHistory != null) {
+                    userHistory.forceWriteToDisk();
+                }
+            }
+        }
+    }
+
+    public void addNotification(@NonNull final HistoricalNotification notification) {
+        synchronized (mLock) {
+            final NotificationHistoryDatabase userHistory =
+                    getUserHistoryAndInitializeIfNeededLocked(notification.getUserId());
+            if (userHistory == null) {
+                Slog.w(TAG, "Attempted to add notif for locked/gone user "
+                        + notification.getUserId());
+                return;
+            }
+            userHistory.addNotification(notification);
+        }
+    }
+
+    public @NonNull NotificationHistory readNotificationHistory(@UserIdInt int[] userIds) {
+        synchronized (mLock) {
+            NotificationHistory mergedHistory = new NotificationHistory();
+            if (userIds == null) {
+                return mergedHistory;
+            }
+            for (int userId : userIds) {
+                final NotificationHistoryDatabase userHistory =
+                        getUserHistoryAndInitializeIfNeededLocked(userId);
+                if (userHistory == null) {
+                    Slog.i(TAG, "Attempted to read history for locked/gone user " +userId);
+                    continue;
+                }
+                mergedHistory.addNotificationsToWrite(userHistory.readNotificationHistory());
+            }
+            return mergedHistory;
+        }
+    }
+
+    public @NonNull android.app.NotificationHistory readFilteredNotificationHistory(
+            @UserIdInt int userId, String packageName, String channelId, int maxNotifications) {
+        synchronized (mLock) {
+            final NotificationHistoryDatabase userHistory =
+                    getUserHistoryAndInitializeIfNeededLocked(userId);
+            if (userHistory == null) {
+                Slog.i(TAG, "Attempted to read history for locked/gone user " +userId);
+                return new android.app.NotificationHistory();
+            }
+
+            return userHistory.readNotificationHistory(packageName, channelId, maxNotifications);
+        }
+    }
+
+    @GuardedBy("mLock")
+    private @Nullable NotificationHistoryDatabase getUserHistoryAndInitializeIfNeededLocked(
+            int userId) {
+        NotificationHistoryDatabase userHistory = mUserState.get(userId);
+        if (userHistory == null) {
+            final File historyDir = new File(Environment.getDataSystemCeDirectory(userId),
+                    DIRECTORY_PER_USER);
+            userHistory = NotificationHistoryDatabaseFactory.create(mContext, IoThread.getHandler(),
+                    historyDir, new NotificationHistoryFileAttrProvider());
+            if (mUserUnlockedStates.get(userId)) {
+                try {
+                    userHistory.init();
+                } catch (Exception e) {
+                    if (mUserManager.isUserUnlocked(userId)) {
+                        throw e; // rethrow exception - user is unlocked
+                    } else {
+                        Slog.w(TAG, "Attempted to initialize service for "
+                                + "stopped or removed user " + userId);
+                        return null;
+                    }
+                }
+            } else {
+                // locked! data unavailable
+                Slog.w(TAG, "Attempted to initialize service for "
+                        + "stopped or removed user " + userId);
+                return null;
+            }
+            mUserState.put(userId, userHistory);
+        }
+        return userHistory;
+    }
+
+    @VisibleForTesting
+    boolean isUserUnlocked(@UserIdInt int userId) {
+        synchronized (mLock) {
+            return mUserUnlockedStates.get(userId);
+        }
+    }
+
+    @VisibleForTesting
+    boolean doesHistoryExistForUser(@UserIdInt int userId) {
+        synchronized (mLock) {
+            return mUserState.get(userId) != null;
+        }
+    }
+
+    @VisibleForTesting
+    void replaceNotificationHistoryDatabase(@UserIdInt int userId,
+            NotificationHistoryDatabase replacement) {
+        synchronized (mLock) {
+            if (mUserState.get(userId) != null) {
+                mUserState.put(userId, replacement);
+            }
+        }
+    }
+
+    @VisibleForTesting
+    List<String> getPendingPackageRemovalsForUser(@UserIdInt int userId) {
+        synchronized (mLock) {
+            return mUserPendingPackageRemovals.get(userId);
+        }
+    }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java
index 608625f..a00afec 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java
@@ -45,11 +45,6 @@
 import org.mockito.MockitoAnnotations;
 
 import java.io.File;
-import java.nio.file.FileSystem;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.attribute.BasicFileAttributes;
-import java.nio.file.attribute.FileTime;
 import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.GregorianCalendar;
@@ -109,8 +104,9 @@
         mFileAttrProvider = new TestFileAttrProvider();
         mRootDir = new File(mContext.getFilesDir(), "NotificationHistoryDatabaseTest");
 
-        mDataBase = new NotificationHistoryDatabase(mContext, mRootDir, mFileAttrProvider);
-        mDataBase.init(mFileWriteHandler);
+        mDataBase = new NotificationHistoryDatabase(
+                mContext, mFileWriteHandler, mRootDir, mFileAttrProvider);
+        mDataBase.init();
     }
 
     @Test
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryManagerTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryManagerTest.java
new file mode 100644
index 0000000..aa3c465
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryManagerTest.java
@@ -0,0 +1,315 @@
+/*
+ * 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 android.os.UserHandle.USER_ALL;
+import static android.os.UserHandle.USER_SYSTEM;
+
+import static com.google.common.truth.Truth.assertThat;
+
+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;
+import android.app.NotificationHistory.HistoricalNotification;
+import android.content.Context;
+import android.graphics.drawable.Icon;
+import android.os.UserManager;
+
+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.util.ArrayList;
+
+@RunWith(AndroidJUnit4.class)
+public class NotificationHistoryManagerTest extends UiServiceTestCase {
+
+    @Mock
+    Context mContext;
+    @Mock
+    UserManager mUserManager;
+    @Mock
+    NotificationHistoryDatabase mDb;
+
+    NotificationHistoryManager mHistoryManager;
+
+    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 = 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);
+        when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager);
+        when(mContext.getUser()).thenReturn(getContext().getUser());
+        when(mContext.getPackageName()).thenReturn(getContext().getPackageName());
+
+        NotificationHistoryDatabaseFactory.setTestingNotificationHistoryDatabase(mDb);
+
+        mHistoryManager = new NotificationHistoryManager(mContext);
+    }
+
+    @Test
+    public void testOnUserUnlocked() {
+        assertThat(mHistoryManager.doesHistoryExistForUser(USER_SYSTEM)).isFalse();
+        assertThat(mHistoryManager.isUserUnlocked(USER_SYSTEM)).isFalse();
+        mHistoryManager.onUserUnlocked(USER_SYSTEM);
+        assertThat(mHistoryManager.doesHistoryExistForUser(USER_SYSTEM)).isTrue();
+        assertThat(mHistoryManager.isUserUnlocked(USER_SYSTEM)).isTrue();
+        verify(mDb, times(1)).init();
+    }
+
+    @Test
+    public void testOnUserUnlocked_cleansUpRemovedPackages() {
+        String pkg = "pkg";
+        mHistoryManager.onPackageRemoved(USER_SYSTEM, pkg);
+        assertThat(mHistoryManager.doesHistoryExistForUser(USER_SYSTEM)).isFalse();
+
+        mHistoryManager.onUserUnlocked(USER_SYSTEM);
+        assertThat(mHistoryManager.doesHistoryExistForUser(USER_SYSTEM)).isTrue();
+        assertThat(mHistoryManager.isUserUnlocked(USER_SYSTEM)).isTrue();
+
+        verify(mDb, times(1)).onPackageRemoved(pkg);
+    }
+
+    @Test
+    public void testOnUserStopped_userExists() {
+        mHistoryManager.onUserUnlocked(USER_SYSTEM);
+        mHistoryManager.onUserStopped(USER_SYSTEM);
+
+        assertThat(mHistoryManager.doesHistoryExistForUser(USER_SYSTEM)).isFalse();
+        assertThat(mHistoryManager.isUserUnlocked(USER_SYSTEM)).isFalse();
+    }
+
+    @Test
+    public void testOnUserStopped_userDoesNotExist() {
+        mHistoryManager.onUserStopped(USER_SYSTEM);
+        // no crash
+        assertThat(mHistoryManager.doesHistoryExistForUser(USER_SYSTEM)).isFalse();
+        assertThat(mHistoryManager.isUserUnlocked(USER_SYSTEM)).isFalse();
+    }
+
+    @Test
+    public void testOnUserRemoved_userExists() {
+        mHistoryManager.onUserUnlocked(USER_SYSTEM);
+        mHistoryManager.onUserRemoved(USER_SYSTEM);
+
+        assertThat(mHistoryManager.doesHistoryExistForUser(USER_SYSTEM)).isFalse();
+        assertThat(mHistoryManager.isUserUnlocked(USER_SYSTEM)).isFalse();
+    }
+
+    @Test
+    public void testOnUserRemoved_userDoesNotExist() {
+        mHistoryManager.onUserRemoved(USER_SYSTEM);
+        // no crash
+        assertThat(mHistoryManager.doesHistoryExistForUser(USER_SYSTEM)).isFalse();
+        assertThat(mHistoryManager.isUserUnlocked(USER_SYSTEM)).isFalse();
+    }
+
+    @Test
+    public void testOnUserRemoved_cleanupPendingPackages() {
+        mHistoryManager.onUserUnlocked(USER_SYSTEM);
+        mHistoryManager.onUserStopped(USER_SYSTEM);
+        String pkg = "pkg";
+        mHistoryManager.onPackageRemoved(USER_SYSTEM, pkg);
+        mHistoryManager.onUserRemoved(USER_SYSTEM);
+
+        assertThat(mHistoryManager.getPendingPackageRemovalsForUser(USER_SYSTEM)).isNull();
+    }
+
+    @Test
+    public void testOnPackageRemoved_userUnlocked() {
+        String pkg = "pkg";
+        NotificationHistoryDatabase userHistory = mock(NotificationHistoryDatabase.class);
+
+        mHistoryManager.onUserUnlocked(USER_SYSTEM);
+        mHistoryManager.replaceNotificationHistoryDatabase(USER_SYSTEM, userHistory);
+
+        mHistoryManager.onPackageRemoved(USER_SYSTEM, pkg);
+
+        verify(userHistory, times(1)).onPackageRemoved(pkg);
+    }
+
+    @Test
+    public void testOnPackageRemoved_userLocked() {
+        String pkg = "pkg";
+        mHistoryManager.onPackageRemoved(USER_SYSTEM, pkg);
+
+        assertThat(mHistoryManager.getPendingPackageRemovalsForUser(USER_SYSTEM)).contains(pkg);
+    }
+
+    @Test
+    public void testOnPackageRemoved_multiUser() {
+        String pkg = "pkg";
+        NotificationHistoryDatabase userHistorySystem = mock(NotificationHistoryDatabase.class);
+        NotificationHistoryDatabase userHistoryAll = mock(NotificationHistoryDatabase.class);
+
+        mHistoryManager.onUserUnlocked(USER_SYSTEM);
+        mHistoryManager.replaceNotificationHistoryDatabase(USER_SYSTEM, userHistorySystem);
+
+        mHistoryManager.onUserUnlocked(USER_ALL);
+        mHistoryManager.replaceNotificationHistoryDatabase(USER_ALL, userHistoryAll);
+
+        mHistoryManager.onPackageRemoved(USER_SYSTEM, pkg);
+
+        verify(userHistorySystem, times(1)).onPackageRemoved(pkg);
+        verify(userHistoryAll, never()).onPackageRemoved(pkg);
+    }
+
+    @Test
+    public void testTriggerWriteToDisk() {
+        NotificationHistoryDatabase userHistorySystem = mock(NotificationHistoryDatabase.class);
+        NotificationHistoryDatabase userHistoryAll = mock(NotificationHistoryDatabase.class);
+
+        mHistoryManager.onUserUnlocked(USER_SYSTEM);
+        mHistoryManager.replaceNotificationHistoryDatabase(USER_SYSTEM, userHistorySystem);
+
+        mHistoryManager.onUserUnlocked(USER_ALL);
+        mHistoryManager.replaceNotificationHistoryDatabase(USER_ALL, userHistoryAll);
+
+        mHistoryManager.triggerWriteToDisk();
+
+        verify(userHistorySystem, times(1)).forceWriteToDisk();
+        verify(userHistoryAll, times(1)).forceWriteToDisk();
+    }
+
+    @Test
+    public void testTriggerWriteToDisk_onlyUnlockedUsers() {
+        NotificationHistoryDatabase userHistorySystem = mock(NotificationHistoryDatabase.class);
+        NotificationHistoryDatabase userHistoryAll = mock(NotificationHistoryDatabase.class);
+
+        mHistoryManager.onUserUnlocked(USER_SYSTEM);
+        mHistoryManager.replaceNotificationHistoryDatabase(USER_SYSTEM, userHistorySystem);
+
+        mHistoryManager.onUserUnlocked(USER_ALL);
+        mHistoryManager.replaceNotificationHistoryDatabase(USER_ALL, userHistoryAll);
+        mHistoryManager.onUserStopped(USER_ALL);
+
+        mHistoryManager.triggerWriteToDisk();
+
+        verify(userHistorySystem, times(1)).forceWriteToDisk();
+        verify(userHistoryAll, never()).forceWriteToDisk();
+    }
+
+    @Test
+    public void testAddNotification_userLocked_noCrash() {
+        HistoricalNotification hn = getHistoricalNotification("pkg", 1);
+
+        mHistoryManager.addNotification(hn);
+    }
+
+    @Test
+    public void testAddNotification() {
+        HistoricalNotification hnSystem = getHistoricalNotification("pkg", USER_SYSTEM);
+        HistoricalNotification hnAll = getHistoricalNotification("pkg", USER_ALL);
+
+        NotificationHistoryDatabase userHistorySystem = mock(NotificationHistoryDatabase.class);
+        NotificationHistoryDatabase userHistoryAll = mock(NotificationHistoryDatabase.class);
+
+        mHistoryManager.onUserUnlocked(USER_SYSTEM);
+        mHistoryManager.replaceNotificationHistoryDatabase(USER_SYSTEM, userHistorySystem);
+
+        mHistoryManager.onUserUnlocked(USER_ALL);
+        mHistoryManager.replaceNotificationHistoryDatabase(USER_ALL, userHistoryAll);
+
+        mHistoryManager.addNotification(hnSystem);
+        mHistoryManager.addNotification(hnAll);
+
+        verify(userHistorySystem, times(1)).addNotification(hnSystem);
+        verify(userHistoryAll, times(1)).addNotification(hnAll);
+    }
+
+    @Test
+    public void testReadNotificationHistory() {
+        HistoricalNotification hnSystem = getHistoricalNotification("pkg", USER_SYSTEM);
+        HistoricalNotification hnAll = getHistoricalNotification("pkg", USER_ALL);
+
+        NotificationHistoryDatabase userHistorySystem = mock(NotificationHistoryDatabase.class);
+        NotificationHistoryDatabase userHistoryAll = mock(NotificationHistoryDatabase.class);
+
+        mHistoryManager.onUserUnlocked(USER_SYSTEM);
+        mHistoryManager.replaceNotificationHistoryDatabase(USER_SYSTEM, userHistorySystem);
+        NotificationHistory nhSystem = mock(NotificationHistory.class);
+        ArrayList<HistoricalNotification> nhSystemList = new ArrayList<>();
+        nhSystemList.add(hnSystem);
+        when(nhSystem.getNotificationsToWrite()).thenReturn(nhSystemList);
+        when(userHistorySystem.readNotificationHistory()).thenReturn(nhSystem);
+
+        mHistoryManager.onUserUnlocked(USER_ALL);
+        mHistoryManager.replaceNotificationHistoryDatabase(USER_ALL, userHistoryAll);
+        NotificationHistory nhAll = mock(NotificationHistory.class);
+        ArrayList<HistoricalNotification> nhAllList = new ArrayList<>();
+        nhAllList.add(hnAll);
+        when(nhAll.getNotificationsToWrite()).thenReturn(nhAllList);
+        when(userHistoryAll.readNotificationHistory()).thenReturn(nhAll);
+
+        // ensure read history returns both historical notifs
+        NotificationHistory nh = mHistoryManager.readNotificationHistory(
+                new int[] {USER_SYSTEM, USER_ALL});
+        assertThat(nh.getNotificationsToWrite()).contains(hnSystem);
+        assertThat(nh.getNotificationsToWrite()).contains(hnAll);
+    }
+
+    @Test
+    public void readFilteredNotificationHistory_userUnlocked() {
+        NotificationHistory nh =
+                mHistoryManager.readFilteredNotificationHistory(USER_SYSTEM, "", "", 1000);
+        assertThat(nh.getNotificationsToWrite()).isEmpty();
+    }
+
+    @Test
+    public void readFilteredNotificationHistory() {
+        mHistoryManager.onUserUnlocked(USER_SYSTEM);
+
+        mHistoryManager.readFilteredNotificationHistory(USER_SYSTEM, "pkg", "chn", 1000);
+        verify(mDb, times(1)).readNotificationHistory("pkg", "chn", 1000);
+    }
+}