Merge "Create standby bucket changed atom." into rvc-dev
diff --git a/apex/extservices/Android.bp b/apex/extservices/Android.bp
index c89f694..021246c 100644
--- a/apex/extservices/Android.bp
+++ b/apex/extservices/Android.bp
@@ -20,6 +20,7 @@
 
 apex_defaults {
     name: "com.android.extservices-defaults",
+    updatable: true,
     key: "com.android.extservices.key",
     certificate: ":com.android.extservices.certificate",
     apps: ["ExtServices"],
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index b86aba6..8c08e75 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -3034,7 +3034,7 @@
     }
 
     void resetExecutionQuota(@NonNull String pkgName, int userId) {
-        mQuotaController.clearAppStats(pkgName, userId);
+        mQuotaController.clearAppStats(userId, pkgName);
     }
 
     void resetScheduleQuota() {
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 4393a95..7256371 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
@@ -54,12 +54,14 @@
 import android.util.ArraySet;
 import android.util.KeyValueListParser;
 import android.util.Log;
+import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArrayMap;
 import android.util.SparseBooleanArray;
 import android.util.SparseSetArray;
 import android.util.proto.ProtoOutputStream;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.IndentingPrintWriter;
@@ -74,6 +76,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
+import java.util.PriorityQueue;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 
@@ -301,10 +304,10 @@
     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.
+     * Listener to track and manage when each package comes back within quota.
      */
-    private final SparseArrayMap<QcAlarmListener> mInQuotaAlarmListeners = new SparseArrayMap<>();
+    @GuardedBy("mLock")
+    private final InQuotaAlarmListener mInQuotaAlarmListener = new InQuotaAlarmListener();
 
     /** Cached calculation results for each app, with the standby buckets as the array indices. */
     private final SparseArrayMap<ExecutionStats[]> mExecutionStatsCache = new SparseArrayMap<>();
@@ -579,7 +582,7 @@
             Slog.wtf(TAG, "Told app removed but given null package name.");
             return;
         }
-        clearAppStats(packageName, UserHandle.getUserId(uid));
+        clearAppStats(UserHandle.getUserId(uid), packageName);
         mForegroundUids.delete(uid);
         mUidToPackageCache.remove(uid);
     }
@@ -589,13 +592,13 @@
         mTrackedJobs.delete(userId);
         mPkgTimers.delete(userId);
         mTimingSessions.delete(userId);
-        mInQuotaAlarmListeners.delete(userId);
+        mInQuotaAlarmListener.removeAlarmsLocked(userId);
         mExecutionStatsCache.delete(userId);
         mUidToPackageCache.clear();
     }
 
     /** Drop all historical stats and stop tracking any active sessions for the specified app. */
-    public void clearAppStats(@NonNull String packageName, int userId) {
+    public void clearAppStats(int userId, @NonNull String packageName) {
         mTrackedJobs.delete(userId, packageName);
         Timer timer = mPkgTimers.get(userId, packageName);
         if (timer != null) {
@@ -606,11 +609,7 @@
             mPkgTimers.delete(userId, packageName);
         }
         mTimingSessions.delete(userId, packageName);
-        QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName);
-        if (alarmListener != null) {
-            mAlarmManager.cancel(alarmListener);
-            mInQuotaAlarmListeners.delete(userId, packageName);
-        }
+        mInQuotaAlarmListener.removeAlarmLocked(userId, packageName);
         mExecutionStatsCache.delete(userId, packageName);
     }
 
@@ -1208,12 +1207,7 @@
             // exempted.
             maybeScheduleStartAlarmLocked(userId, packageName, realStandbyBucket);
         } else {
-            QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName);
-            if (alarmListener != null && alarmListener.isWaiting()) {
-                mAlarmManager.cancel(alarmListener);
-                // Set the trigger time to 0 so that the alarm doesn't think it's still waiting.
-                alarmListener.setTriggerTime(0);
-            }
+            mInQuotaAlarmListener.removeAlarmLocked(userId, packageName);
         }
         return changed;
     }
@@ -1229,12 +1223,7 @@
             final String packageName = jobStatus.getSourcePackageName();
             final int realStandbyBucket = jobStatus.getStandbyBucket();
             if (isWithinQuotaLocked(userId, packageName, realStandbyBucket)) {
-                QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName);
-                if (alarmListener != null && alarmListener.isWaiting()) {
-                    mAlarmManager.cancel(alarmListener);
-                    // Set the trigger time to 0 so that the alarm doesn't think it's still waiting.
-                    alarmListener.setTriggerTime(0);
-                }
+                mInQuotaAlarmListener.removeAlarmLocked(userId, packageName);
             } else {
                 mToScheduleStartAlarms.add(userId, packageName, realStandbyBucket);
             }
@@ -1285,7 +1274,6 @@
         final boolean isUnderTimingSessionCountQuota = isUnderSessionCountQuotaLocked(stats,
                 standbyBucket);
 
-        QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName);
         if (stats.executionTimeInWindowMs < mAllowedTimePerPeriodMs
                 && stats.executionTimeInMaxPeriodMs < mMaxExecutionTimeMs
                 && isUnderJobCountQuota
@@ -1297,21 +1285,11 @@
                         + getRemainingExecutionTimeLocked(userId, packageName, standbyBucket)
                         + "ms in its quota.");
             }
-            if (alarmListener != null) {
-                // Cancel any pending alarm.
-                mAlarmManager.cancel(alarmListener);
-                // Set the trigger time to 0 so that the alarm doesn't think it's still waiting.
-                alarmListener.setTriggerTime(0);
-            }
+            mInQuotaAlarmListener.removeAlarmLocked(userId, packageName);
             mHandler.obtainMessage(MSG_CHECK_PACKAGE, userId, 0, packageName).sendToTarget();
             return;
         }
 
-        if (alarmListener == null) {
-            alarmListener = new QcAlarmListener(userId, packageName);
-            mInQuotaAlarmListeners.add(userId, packageName, alarmListener);
-        }
-
         // The time this app will have quota again.
         long inQuotaTimeElapsed = stats.inQuotaTimeElapsed;
         if (!isUnderJobCountQuota && stats.bgJobCountInWindow < stats.jobCountLimit) {
@@ -1325,27 +1303,7 @@
             inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed,
                     stats.sessionRateLimitExpirationTimeElapsed);
         }
-        // Only schedule the alarm if:
-        // 1. There isn't one currently scheduled
-        // 2. The new alarm is significantly earlier than the previous alarm (which could be the
-        // case if the package moves into a higher standby bucket). If it's earlier but not
-        // significantly so, then we essentially delay the job a few extra minutes.
-        // 3. The alarm is after the current alarm by more than the quota buffer.
-        // TODO: this might be overengineering. Simplify if proven safe.
-        if (!alarmListener.isWaiting()
-                || inQuotaTimeElapsed < alarmListener.getTriggerTimeElapsed() - 3 * MINUTE_IN_MILLIS
-                || alarmListener.getTriggerTimeElapsed() < inQuotaTimeElapsed) {
-            if (DEBUG) {
-                Slog.d(TAG, "Scheduling start alarm for " + pkgString);
-            }
-            // If the next time this app will have quota is at least 3 minutes before the
-            // alarm is supposed to go off, reschedule the alarm.
-            mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, inQuotaTimeElapsed,
-                    ALARM_TAG_QUOTA_CHECK, alarmListener, mHandler);
-            alarmListener.setTriggerTime(inQuotaTimeElapsed);
-        } else if (DEBUG) {
-            Slog.d(TAG, "No need to schedule start alarm for " + pkgString);
-        }
+        mInQuotaAlarmListener.addAlarmLocked(userId, packageName, inQuotaTimeElapsed);
     }
 
     private boolean setConstraintSatisfied(@NonNull JobStatus jobStatus, boolean isWithinQuota) {
@@ -1875,32 +1833,161 @@
         }
     }
 
-    private class QcAlarmListener implements AlarmManager.OnAlarmListener {
-        private final int mUserId;
-        private final String mPackageName;
-        private volatile long mTriggerTimeElapsed;
-
-        QcAlarmListener(int userId, String packageName) {
-            mUserId = userId;
-            mPackageName = packageName;
+    static class AlarmQueue extends PriorityQueue<Pair<Package, Long>> {
+        AlarmQueue() {
+            super(1, (o1, o2) -> (int) (o1.second - o2.second));
         }
 
-        boolean isWaiting() {
-            return mTriggerTimeElapsed > 0;
+        /**
+         * Remove any instances of the Package from the queue.
+         *
+         * @return true if an instance was removed, false otherwise.
+         */
+        boolean remove(@NonNull Package pkg) {
+            boolean removed = false;
+            Pair[] alarms = toArray(new Pair[size()]);
+            for (int i = alarms.length - 1; i >= 0; --i) {
+                if (pkg.equals(alarms[i].first)) {
+                    remove(alarms[i]);
+                    removed = true;
+                }
+            }
+            return removed;
+        }
+    }
+
+    /** Track when UPTCs are expected to come back into quota. */
+    private class InQuotaAlarmListener implements AlarmManager.OnAlarmListener {
+        @GuardedBy("mLock")
+        private final AlarmQueue mAlarmQueue = new AlarmQueue();
+        /** The next time the alarm is set to go off, in the elapsed realtime timebase. */
+        @GuardedBy("mLock")
+        private long mTriggerTimeElapsed = 0;
+
+        @GuardedBy("mLock")
+        void addAlarmLocked(int userId, @NonNull String pkgName, long inQuotaTimeElapsed) {
+            final Package pkg = new Package(userId, pkgName);
+            mAlarmQueue.remove(pkg);
+            mAlarmQueue.offer(new Pair<>(pkg, inQuotaTimeElapsed));
+            setNextAlarmLocked();
         }
 
-        void setTriggerTime(long timeElapsed) {
-            mTriggerTimeElapsed = timeElapsed;
+        @GuardedBy("mLock")
+        void removeAlarmLocked(@NonNull Package pkg) {
+            if (mAlarmQueue.remove(pkg)) {
+                setNextAlarmLocked();
+            }
         }
 
-        long getTriggerTimeElapsed() {
-            return mTriggerTimeElapsed;
+        @GuardedBy("mLock")
+        void removeAlarmLocked(int userId, @NonNull String packageName) {
+            removeAlarmLocked(new Package(userId, packageName));
+        }
+
+        @GuardedBy("mLock")
+        void removeAlarmsLocked(int userId) {
+            boolean removed = false;
+            Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]);
+            for (int i = alarms.length - 1; i >= 0; --i) {
+                final Package pkg = (Package) alarms[i].first;
+                if (userId == pkg.userId) {
+                    mAlarmQueue.remove(alarms[i]);
+                    removed = true;
+                }
+            }
+            if (removed) {
+                setNextAlarmLocked();
+            }
+        }
+
+        @GuardedBy("mLock")
+        private void setNextAlarmLocked() {
+            if (mAlarmQueue.size() > 0) {
+                final long nextTriggerTimeElapsed = mAlarmQueue.peek().second;
+                // Only schedule the alarm if one of the following is true:
+                // 1. There isn't one currently scheduled
+                // 2. The new alarm is significantly earlier than the previous alarm. If it's
+                // earlier but not significantly so, then we essentially delay the job a few extra
+                // minutes.
+                // 3. The alarm is after the current alarm.
+                if (mTriggerTimeElapsed == 0
+                        || nextTriggerTimeElapsed < mTriggerTimeElapsed - 3 * MINUTE_IN_MILLIS
+                        || mTriggerTimeElapsed < nextTriggerTimeElapsed) {
+                    if (DEBUG) {
+                        Slog.d(TAG, "Scheduling start alarm at " + nextTriggerTimeElapsed);
+                    }
+                    mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, nextTriggerTimeElapsed,
+                            ALARM_TAG_QUOTA_CHECK, this, mHandler);
+                    mTriggerTimeElapsed = nextTriggerTimeElapsed;
+                }
+            } else {
+                mAlarmManager.cancel(this);
+                mTriggerTimeElapsed = 0;
+            }
         }
 
         @Override
         public void onAlarm() {
-            mHandler.obtainMessage(MSG_CHECK_PACKAGE, mUserId, 0, mPackageName).sendToTarget();
-            mTriggerTimeElapsed = 0;
+            synchronized (mLock) {
+                while (mAlarmQueue.size() > 0) {
+                    final Pair<Package, Long> alarm = mAlarmQueue.peek();
+                    if (alarm.second <= sElapsedRealtimeClock.millis()) {
+                        mHandler.obtainMessage(MSG_CHECK_PACKAGE, alarm.first.userId, 0,
+                                alarm.first.packageName).sendToTarget();
+                        mAlarmQueue.remove(alarm);
+                    } else {
+                        break;
+                    }
+                }
+                setNextAlarmLocked();
+            }
+        }
+
+        @GuardedBy("mLock")
+        void dumpLocked(IndentingPrintWriter pw) {
+            pw.println("In quota alarms:");
+            pw.increaseIndent();
+
+            if (mAlarmQueue.size() == 0) {
+                pw.println("NOT WAITING");
+            } else {
+                Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]);
+                for (int i = 0; i < alarms.length; ++i) {
+                    final Package pkg = (Package) alarms[i].first;
+                    pw.print(pkg);
+                    pw.print(": ");
+                    pw.print(alarms[i].second);
+                    pw.println();
+                }
+            }
+
+            pw.decreaseIndent();
+        }
+
+        @GuardedBy("mLock")
+        void dumpLocked(ProtoOutputStream proto, long fieldId) {
+            final long token = proto.start(fieldId);
+
+            proto.write(
+                    StateControllerProto.QuotaController.InQuotaAlarmListener.TRIGGER_TIME_ELAPSED,
+                    mTriggerTimeElapsed);
+
+            Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]);
+            for (int i = 0; i < alarms.length; ++i) {
+                final long aToken = proto.start(
+                        StateControllerProto.QuotaController.InQuotaAlarmListener.ALARMS);
+
+                final Package pkg = (Package) alarms[i].first;
+                pkg.dumpDebug(proto,
+                        StateControllerProto.QuotaController.InQuotaAlarmListener.Alarm.PKG);
+                proto.write(
+                        StateControllerProto.QuotaController.InQuotaAlarmListener.Alarm.IN_QUOTA_TIME_ELAPSED,
+                        (Long) alarms[i].second);
+
+                proto.end(aToken);
+            }
+
+            proto.end(token);
         }
     }
 
@@ -2618,23 +2705,7 @@
         pw.decreaseIndent();
 
         pw.println();
-        pw.println("In quota alarms:");
-        pw.increaseIndent();
-        for (int u = 0; u < mInQuotaAlarmListeners.numMaps(); ++u) {
-            final int userId = mInQuotaAlarmListeners.keyAt(u);
-            for (int p = 0; p < mInQuotaAlarmListeners.numElementsForKey(userId); ++p) {
-                final String pkgName = mInQuotaAlarmListeners.keyAt(u, p);
-                QcAlarmListener alarmListener = mInQuotaAlarmListeners.valueAt(u, p);
-
-                pw.print(string(userId, pkgName));
-                pw.print(": ");
-                if (alarmListener.isWaiting()) {
-                    pw.println(alarmListener.getTriggerTimeElapsed());
-                } else {
-                    pw.println("NOT WAITING");
-                }
-            }
-        }
+        mInQuotaAlarmListener.dumpLocked(pw);
         pw.decreaseIndent();
     }
 
@@ -2768,22 +2839,13 @@
                     }
                 }
 
-                QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, pkgName);
-                if (alarmListener != null) {
-                    final long alToken = proto.start(
-                            StateControllerProto.QuotaController.PackageStats.IN_QUOTA_ALARM_LISTENER);
-                    proto.write(StateControllerProto.QuotaController.AlarmListener.IS_WAITING,
-                            alarmListener.isWaiting());
-                    proto.write(
-                            StateControllerProto.QuotaController.AlarmListener.TRIGGER_TIME_ELAPSED,
-                            alarmListener.getTriggerTimeElapsed());
-                    proto.end(alToken);
-                }
-
                 proto.end(psToken);
             }
         }
 
+        mInQuotaAlarmListener.dumpLocked(proto,
+                StateControllerProto.QuotaController.IN_QUOTA_ALARM_LISTENER);
+
         proto.end(mToken);
         proto.end(token);
     }
diff --git a/apex/jobscheduler/service/java/com/android/server/usage/TEST_MAPPING b/apex/jobscheduler/service/java/com/android/server/usage/TEST_MAPPING
index cf70878..ba7572a 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/TEST_MAPPING
+++ b/apex/jobscheduler/service/java/com/android/server/usage/TEST_MAPPING
@@ -1,6 +1,13 @@
 {
   "presubmit": [
     {
+      "name": "CtsUsageStatsTestCases",
+      "options": [
+        {"include-filter": "android.app.usage.cts.UsageStatsTest"},
+        {"exclude-annotation": "androidx.test.filters.FlakyTest"}
+      ]
+    },
+    {
       "name": "FrameworksServicesTests",
       "options": [
         {"include-filter": "com.android.server.usage"},
@@ -10,6 +17,9 @@
   ],
   "postsubmit": [
     {
+      "name": "CtsUsageStatsTestCases"
+    },
+    {
       "name": "FrameworksServicesTests",
       "options": [
         {"include-filter": "com.android.server.usage"}
diff --git a/apex/permission/Android.bp b/apex/permission/Android.bp
index d746ea6..0171b0d 100644
--- a/apex/permission/Android.bp
+++ b/apex/permission/Android.bp
@@ -20,6 +20,7 @@
 
 apex_defaults {
     name: "com.android.permission-defaults",
+    updatable: true,
     key: "com.android.permission.key",
     certificate: ":com.android.permission.certificate",
     java_libs: [
diff --git a/apex/sdkextensions/Android.bp b/apex/sdkextensions/Android.bp
index 25765af..322d5e1 100644
--- a/apex/sdkextensions/Android.bp
+++ b/apex/sdkextensions/Android.bp
@@ -26,6 +26,7 @@
 
 apex_defaults {
     name: "com.android.sdkext-defaults",
+    updatable: true,
     java_libs: [ "framework-sdkextensions" ],
     prebuilts: [
         "derive_sdk.rc",
diff --git a/apex/statsd/Android.bp b/apex/statsd/Android.bp
index 4c96263..2df3eea 100644
--- a/apex/statsd/Android.bp
+++ b/apex/statsd/Android.bp
@@ -32,6 +32,7 @@
     compile_multilib: "both",
     prebuilts: ["com.android.os.statsd.init.rc"],
     name: "com.android.os.statsd-defaults",
+    updatable: true,
     key: "com.android.os.statsd.key",
     certificate: ":com.android.os.statsd.certificate",
 }
diff --git a/apex/statsd/framework/Android.bp b/apex/statsd/framework/Android.bp
index 0f52f15..1bd770a 100644
--- a/apex/statsd/framework/Android.bp
+++ b/apex/statsd/framework/Android.bp
@@ -69,6 +69,8 @@
         "android.util",
     ],
 
+    plugins: ["java_api_finder"],
+
     hostdex: true, // for hiddenapi check
     visibility: [
         "//frameworks/base/apex/statsd:__subpackages__",
diff --git a/apex/statsd/service/Android.bp b/apex/statsd/service/Android.bp
index 0f325e3..ff56bb5 100644
--- a/apex/statsd/service/Android.bp
+++ b/apex/statsd/service/Android.bp
@@ -31,6 +31,8 @@
         "android_module_lib_stubs_current",
     ],
 
+    plugins: ["java_api_finder"],
+
     apex_available: [
         "com.android.os.statsd",
         "test_com.android.os.statsd",
diff --git a/cmds/statsd/src/external/StatsCallbackPuller.cpp b/cmds/statsd/src/external/StatsCallbackPuller.cpp
index 0edf40b..933f48d 100644
--- a/cmds/statsd/src/external/StatsCallbackPuller.cpp
+++ b/cmds/statsd/src/external/StatsCallbackPuller.cpp
@@ -66,10 +66,8 @@
                 {
                     lock_guard<mutex> lk(*cv_mutex);
                     for (const StatsEventParcel& parcel: output) {
-                        uint8_t* buf = reinterpret_cast<uint8_t*>(
-                                const_cast<int8_t*>(parcel.buffer.data()));
-                        shared_ptr<LogEvent> event = make_shared<LogEvent>(
-                                buf, parcel.buffer.size(), /*uid=*/-1, /*pid=*/-1);
+                        shared_ptr<LogEvent> event = make_shared<LogEvent>(/*uid=*/-1, /*pid=*/-1);
+                        event->parseBuffer((uint8_t*)parcel.buffer.data(), parcel.buffer.size());
                         sharedData->push_back(event);
                     }
                     *pullSuccess = success;
diff --git a/cmds/statsd/src/logd/LogEvent.cpp b/cmds/statsd/src/logd/LogEvent.cpp
index 3e46d13..974e203 100644
--- a/cmds/statsd/src/logd/LogEvent.cpp
+++ b/cmds/statsd/src/logd/LogEvent.cpp
@@ -65,18 +65,6 @@
 #define ATTRIBUTION_CHAIN_TYPE 0x09
 #define ERROR_TYPE 0x0F
 
-// Msg is expected to begin at the start of the serialized atom -- it should not
-// include the android_log_header_t or the StatsEventTag.
-LogEvent::LogEvent(uint8_t* msg, uint32_t len, int32_t uid, int32_t pid)
-    : mBuf(msg),
-      mRemainingLen(len),
-      mLogdTimestampNs(time(nullptr)),
-      mLogUid(uid),
-      mLogPid(pid)
-{
-    initNew();
-}
-
 LogEvent::LogEvent(const LogEvent& event) {
     mTagId = event.mTagId;
     mLogUid = event.mLogUid;
@@ -86,6 +74,12 @@
     mValues = event.mValues;
 }
 
+LogEvent::LogEvent(int32_t uid, int32_t pid)
+    : mLogdTimestampNs(time(nullptr)),
+      mLogUid(uid),
+      mLogPid(pid) {
+}
+
 LogEvent::LogEvent(int32_t tagId, int64_t wallClockTimestampNs, int64_t elapsedTimestampNs) {
     mLogdTimestampNs = wallClockTimestampNs;
     mElapsedTimestampNs = elapsedTimestampNs;
@@ -189,9 +183,6 @@
     mValues.push_back(FieldValue(Field(mTagId, getSimpleField(4)), Value(trainInfo.status)));
 }
 
-LogEvent::LogEvent(int32_t tagId, int64_t timestampNs) : LogEvent(tagId, timestampNs, timestampNs) {
-}
-
 LogEvent::LogEvent(int32_t tagId, int64_t timestampNs, int32_t uid) {
     mLogdTimestampNs = timestampNs;
     mTagId = tagId;
@@ -467,7 +458,10 @@
 
 // This parsing logic is tied to the encoding scheme used in StatsEvent.java and
 // stats_event.c
-void LogEvent::initNew() {
+bool LogEvent::parseBuffer(uint8_t* buf, size_t len) {
+    mBuf = buf;
+    mRemainingLen = (uint32_t)len;
+
     int32_t pos[] = {1, 1, 1};
     bool last[] = {false, false, false};
 
@@ -529,6 +523,7 @@
 
     if (mRemainingLen != 0) mValid = false;
     mBuf = nullptr;
+    return mValid;
 }
 
 uint8_t LogEvent::getTypeId(uint8_t typeInfo) {
diff --git a/cmds/statsd/src/logd/LogEvent.h b/cmds/statsd/src/logd/LogEvent.h
index e167e67..3940aa8 100644
--- a/cmds/statsd/src/logd/LogEvent.h
+++ b/cmds/statsd/src/logd/LogEvent.h
@@ -20,7 +20,6 @@
 
 #include <android/util/ProtoOutputStream.h>
 #include <private/android_logger.h>
-#include <stats_event.h>
 
 #include <string>
 #include <vector>
@@ -61,14 +60,32 @@
 };
 
 /**
- * Wrapper for the log_msg structure.
+ * This class decodes the structured, serialized encoding of an atom into a
+ * vector of FieldValues.
  */
 class LogEvent {
 public:
     /**
-     * Read a LogEvent from the socket
+     * \param uid user id of the logging caller
+     * \param pid process id of the logging caller
      */
-    explicit LogEvent(uint8_t* msg, uint32_t len, int32_t uid, int32_t pid);
+    explicit LogEvent(int32_t uid, int32_t pid);
+
+    /**
+     * Parses the atomId, timestamp, and vector of values from a buffer
+     * containing the StatsEvent/AStatsEvent encoding of an atom.
+     *
+     * \param buf a buffer that begins at the start of the serialized atom (it
+     * should not include the android_log_header_t or the StatsEventTag)
+     * \param len size of the buffer
+     *
+     * \return success of the initialization
+     */
+    bool parseBuffer(uint8_t* buf, size_t len);
+
+    // TODO(b/149590301): delete unused functions below once LogEvent uses the
+    // new socket schema within test code. Really we would like the only entry
+    // points into LogEvent to be the above constructor and parseBuffer functions.
 
     /**
      * Constructs a LogEvent with synthetic data for testing. Must call init() before reading.
@@ -76,9 +93,6 @@
     explicit LogEvent(int32_t tagId, int64_t wallClockTimestampNs, int64_t elapsedTimestampNs);
 
     // For testing. The timestamp is used as both elapsed real time and logd timestamp.
-    explicit LogEvent(int32_t tagId, int64_t timestampNs);
-
-    // For testing. The timestamp is used as both elapsed real time and logd timestamp.
     explicit LogEvent(int32_t tagId, int64_t timestampNs, int32_t uid);
 
     /**
@@ -192,10 +206,6 @@
         return &mValues;
     }
 
-    bool isValid() {
-          return mValid;
-    }
-
     inline LogEvent makeCopy() {
         return LogEvent(*this);
     }
@@ -222,12 +232,6 @@
      */
     LogEvent(const LogEvent&);
 
-
-    /**
-     * Parsing function for new encoding scheme.
-     */
-    void initNew();
-
     void parseInt32(int32_t* pos, int32_t depth, bool* last);
     void parseInt64(int32_t* pos, int32_t depth, bool* last);
     void parseString(int32_t* pos, int32_t depth, bool* last);
@@ -238,13 +242,14 @@
     void parseAttributionChain(int32_t* pos, int32_t depth, bool* last);
 
     /**
-     * mBuf is a pointer to the current location in the buffer being parsed.
-     * Because the buffer lives  on the StatsSocketListener stack, this pointer
-     * is only valid during the LogEvent constructor. It will be set to null at
-     * the end of initNew.
+     * The below three variables are only valid during the execution of
+     * parseBuffer. There are no guarantees about the state of these variables
+     * before/after.
+     *
+     * TODO (b/150312423): These shouldn't be member variables. We should pass
+     * them around as parameters.
      */
     uint8_t* mBuf;
-
     uint32_t mRemainingLen; // number of valid bytes left in the buffer being parsed
     bool mValid = true; // stores whether the event we received from the socket is valid
 
diff --git a/cmds/statsd/src/socket/StatsSocketListener.cpp b/cmds/statsd/src/socket/StatsSocketListener.cpp
index 8f0f480..b877cc9 100755
--- a/cmds/statsd/src/socket/StatsSocketListener.cpp
+++ b/cmds/statsd/src/socket/StatsSocketListener.cpp
@@ -126,7 +126,10 @@
     uint32_t pid = cred->pid;
 
     int64_t oldestTimestamp;
-    if (!mQueue->push(std::make_unique<LogEvent>(msg, len, uid, pid), &oldestTimestamp)) {
+    std::unique_ptr<LogEvent> logEvent = std::make_unique<LogEvent>(uid, pid);
+    logEvent->parseBuffer(msg, len);
+
+    if (!mQueue->push(std::move(logEvent), &oldestTimestamp)) {
         StatsdStats::getInstance().noteEventQueueOverflow(oldestTimestamp);
     }
 
diff --git a/cmds/statsd/tests/LogEvent_test.cpp b/cmds/statsd/tests/LogEvent_test.cpp
index 7542faf..7458cbf 100644
--- a/cmds/statsd/tests/LogEvent_test.cpp
+++ b/cmds/statsd/tests/LogEvent_test.cpp
@@ -54,8 +54,9 @@
     size_t size;
     uint8_t* buf = AStatsEvent_getBuffer(event, &size);
 
-    LogEvent logEvent(buf, size, /*uid=*/ 1000, /*pid=*/ 1001);
-    EXPECT_TRUE(logEvent.isValid());
+    LogEvent logEvent(/*uid=*/1000, /*pid=*/1001);
+    EXPECT_TRUE(logEvent.parseBuffer(buf, size));
+
     EXPECT_EQ(100, logEvent.GetTagId());
     EXPECT_EQ(1000, logEvent.GetUid());
     EXPECT_EQ(1001, logEvent.GetPid());
@@ -102,8 +103,9 @@
     size_t size;
     uint8_t* buf = AStatsEvent_getBuffer(event, &size);
 
-    LogEvent logEvent(buf, size, /*uid=*/ 1000, /*pid=*/ 1001);
-    EXPECT_TRUE(logEvent.isValid());
+    LogEvent logEvent(/*uid=*/ 1000, /*pid=*/ 1001);
+    EXPECT_TRUE(logEvent.parseBuffer(buf, size));
+
     EXPECT_EQ(100, logEvent.GetTagId());
     EXPECT_EQ(1000, logEvent.GetUid());
     EXPECT_EQ(1001, logEvent.GetPid());
@@ -137,8 +139,9 @@
     size_t size;
     uint8_t* buf = AStatsEvent_getBuffer(event, &size);
 
-    LogEvent logEvent(buf, size, /*uid=*/ 1000, /*pid=*/ 1001);
-    EXPECT_TRUE(logEvent.isValid());
+    LogEvent logEvent(/*uid=*/ 1000, /*pid=*/ 1001);
+    EXPECT_TRUE(logEvent.parseBuffer(buf, size));
+
     EXPECT_EQ(100, logEvent.GetTagId());
     EXPECT_EQ(1000, logEvent.GetUid());
     EXPECT_EQ(1001, logEvent.GetPid());
@@ -165,8 +168,9 @@
     size_t size;
     uint8_t* buf = AStatsEvent_getBuffer(event, &size);
 
-    LogEvent logEvent(buf, size, /*uid=*/ 1000, /*pid=*/ 1001);
-    EXPECT_TRUE(logEvent.isValid());
+    LogEvent logEvent(/*uid=*/ 1000, /*pid=*/ 1001);
+    EXPECT_TRUE(logEvent.parseBuffer(buf, size));
+
     EXPECT_EQ(100, logEvent.GetTagId());
     EXPECT_EQ(1000, logEvent.GetUid());
     EXPECT_EQ(1001, logEvent.GetPid());
@@ -200,8 +204,9 @@
     size_t size;
     uint8_t* buf = AStatsEvent_getBuffer(event, &size);
 
-    LogEvent logEvent(buf, size, /*uid=*/ 1000, /*pid=*/ 1001);
-    EXPECT_TRUE(logEvent.isValid());
+    LogEvent logEvent(/*uid=*/ 1000, /*pid=*/ 1001);
+    EXPECT_TRUE(logEvent.parseBuffer(buf, size));
+
     EXPECT_EQ(100, logEvent.GetTagId());
     EXPECT_EQ(1000, logEvent.GetUid());
     EXPECT_EQ(1001, logEvent.GetPid());
diff --git a/cmds/statsd/tests/condition/SimpleConditionTracker_test.cpp b/cmds/statsd/tests/condition/SimpleConditionTracker_test.cpp
index 36094b2..8701e17 100644
--- a/cmds/statsd/tests/condition/SimpleConditionTracker_test.cpp
+++ b/cmds/statsd/tests/condition/SimpleConditionTracker_test.cpp
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 #include "src/condition/SimpleConditionTracker.h"
+#include "stats_event.h"
 #include "tests/statsd_test_util.h"
 
 #include <gmock/gmock.h>
@@ -31,6 +32,8 @@
 namespace os {
 namespace statsd {
 
+namespace {
+
 const ConfigKey kConfigKey(0, 12345);
 
 const int ATTRIBUTION_NODE_FIELD_ID = 1;
@@ -57,24 +60,33 @@
     return simplePredicate;
 }
 
-void writeAttributionNodesToEvent(LogEvent* event, const std::vector<int> &uids) {
-    std::vector<AttributionNodeInternal> nodes;
-    for (size_t i = 0; i < uids.size(); ++i) {
-        AttributionNodeInternal node;
-        node.set_uid(uids[i]);
-        nodes.push_back(node);
+void makeWakeLockEvent(LogEvent* logEvent, uint32_t atomId, uint64_t timestamp,
+                       const vector<int>& uids, const string& wl, int acquire) {
+    AStatsEvent* statsEvent = AStatsEvent_obtain();
+    AStatsEvent_setAtomId(statsEvent, atomId);
+    AStatsEvent_overwriteTimestamp(statsEvent, timestamp);
+
+    vector<std::string> tags(uids.size()); // vector of empty strings
+    vector<const char*> cTags(uids.size());
+    for (int i = 0; i < cTags.size(); i++) {
+        cTags[i] = tags[i].c_str();
     }
-    event->write(nodes);  // attribution chain.
+    AStatsEvent_writeAttributionChain(statsEvent, reinterpret_cast<const uint32_t*>(uids.data()),
+                                      cTags.data(), uids.size());
+
+    AStatsEvent_writeString(statsEvent, wl.c_str());
+    AStatsEvent_writeInt32(statsEvent, acquire);
+    AStatsEvent_build(statsEvent);
+
+    size_t size;
+    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
+    logEvent->parseBuffer(buf, size);
+
+    AStatsEvent_release(statsEvent);
 }
 
-// TODO(b/149590301): Update this helper to use new socket schema.
-//void makeWakeLockEvent(
-//        LogEvent* event, const std::vector<int> &uids, const string& wl, int acquire) {
-//    writeAttributionNodesToEvent(event, uids);
-//    event->write(wl);
-//    event->write(acquire);
-//    event->init();
-//}
+} // anonymous namespace
+
 
 std::map<int64_t, HashableDimensionKey> getWakeLockQueryKey(
     const Position position,
@@ -265,138 +277,128 @@
     EXPECT_TRUE(changedCache[0]);
 }
 
-// TODO(b/149590301): Update these tests to use new socket schema.
-//TEST(SimpleConditionTrackerTest, TestSlicedCondition) {
-//    std::vector<sp<ConditionTracker>> allConditions;
-//    for (Position position :
-//            { Position::FIRST, Position::LAST}) {
-//
-//        SimplePredicate simplePredicate = getWakeLockHeldCondition(
-//                true /*nesting*/, true /*default to false*/, true /*output slice by uid*/,
-//                position);
-//        string conditionName = "WL_HELD_BY_UID2";
-//
-//        unordered_map<int64_t, int> trackerNameIndexMap;
-//        trackerNameIndexMap[StringToId("WAKE_LOCK_ACQUIRE")] = 0;
-//        trackerNameIndexMap[StringToId("WAKE_LOCK_RELEASE")] = 1;
-//        trackerNameIndexMap[StringToId("RELEASE_ALL")] = 2;
-//
-//        SimpleConditionTracker conditionTracker(kConfigKey, StringToId(conditionName),
-//                                                0 /*condition tracker index*/, simplePredicate,
-//                                                trackerNameIndexMap);
-//
-//        std::vector<int> uids = {111, 222, 333};
-//
-//        LogEvent event(1 /*tagId*/, 0 /*timestamp*/);
-//        makeWakeLockEvent(&event, uids, "wl1", 1);
-//
-//        // one matched start
-//        vector<MatchingState> matcherState;
-//        matcherState.push_back(MatchingState::kMatched);
-//        matcherState.push_back(MatchingState::kNotMatched);
-//        matcherState.push_back(MatchingState::kNotMatched);
-//        vector<sp<ConditionTracker>> allPredicates;
-//        vector<ConditionState> conditionCache(1, ConditionState::kNotEvaluated);
-//        vector<bool> changedCache(1, false);
-//
-//        conditionTracker.evaluateCondition(event, matcherState, allPredicates, conditionCache,
-//                                           changedCache);
-//
-//        if (position == Position::FIRST ||
-//            position == Position::LAST) {
-//            EXPECT_EQ(1UL, conditionTracker.mSlicedConditionState.size());
-//        } else {
-//            EXPECT_EQ(uids.size(), conditionTracker.mSlicedConditionState.size());
-//        }
-//        EXPECT_TRUE(changedCache[0]);
-//        if (position == Position::FIRST ||
-//            position == Position::LAST) {
-//            EXPECT_EQ(conditionTracker.getChangedToTrueDimensions(allConditions)->size(), 1u);
-//            EXPECT_TRUE(conditionTracker.getChangedToFalseDimensions(allConditions)->empty());
-//        } else {
-//            EXPECT_EQ(conditionTracker.getChangedToTrueDimensions(allConditions)->size(), uids.size());
-//        }
-//
-//        // Now test query
-//        const auto queryKey = getWakeLockQueryKey(position, uids, conditionName);
-//        conditionCache[0] = ConditionState::kNotEvaluated;
-//
-//        conditionTracker.isConditionMet(queryKey, allPredicates,
-//                                        false,
-//                                        conditionCache);
-//        EXPECT_EQ(ConditionState::kTrue, conditionCache[0]);
-//
-//        // another wake lock acquired by this uid
-//        LogEvent event2(1 /*tagId*/, 0 /*timestamp*/);
-//        makeWakeLockEvent(&event2, uids, "wl2", 1);
-//        matcherState.clear();
-//        matcherState.push_back(MatchingState::kMatched);
-//        matcherState.push_back(MatchingState::kNotMatched);
-//        conditionCache[0] = ConditionState::kNotEvaluated;
-//        changedCache[0] = false;
-//        conditionTracker.evaluateCondition(event2, matcherState, allPredicates, conditionCache,
-//                                           changedCache);
-//        EXPECT_FALSE(changedCache[0]);
-//        if (position == Position::FIRST ||
-//            position == Position::LAST) {
-//            EXPECT_EQ(1UL, conditionTracker.mSlicedConditionState.size());
-//        } else {
-//            EXPECT_EQ(uids.size(), conditionTracker.mSlicedConditionState.size());
-//        }
-//        EXPECT_TRUE(conditionTracker.getChangedToTrueDimensions(allConditions)->empty());
-//        EXPECT_TRUE(conditionTracker.getChangedToFalseDimensions(allConditions)->empty());
-//
-//
-//        // wake lock 1 release
-//        LogEvent event3(1 /*tagId*/, 0 /*timestamp*/);
-//        makeWakeLockEvent(&event3, uids, "wl1", 0);  // now release it.
-//        matcherState.clear();
-//        matcherState.push_back(MatchingState::kNotMatched);
-//        matcherState.push_back(MatchingState::kMatched);
-//        conditionCache[0] = ConditionState::kNotEvaluated;
-//        changedCache[0] = false;
-//        conditionTracker.evaluateCondition(event3, matcherState, allPredicates, conditionCache,
-//                                           changedCache);
-//        // nothing changes, because wake lock 2 is still held for this uid
-//        EXPECT_FALSE(changedCache[0]);
-//        if (position == Position::FIRST ||
-//            position == Position::LAST) {
-//            EXPECT_EQ(1UL, conditionTracker.mSlicedConditionState.size());
-//        } else {
-//            EXPECT_EQ(uids.size(), conditionTracker.mSlicedConditionState.size());
-//        }
-//        EXPECT_TRUE(conditionTracker.getChangedToTrueDimensions(allConditions)->empty());
-//        EXPECT_TRUE(conditionTracker.getChangedToFalseDimensions(allConditions)->empty());
-//
-//        LogEvent event4(1 /*tagId*/, 0 /*timestamp*/);
-//        makeWakeLockEvent(&event4, uids, "wl2", 0);  // now release it.
-//        matcherState.clear();
-//        matcherState.push_back(MatchingState::kNotMatched);
-//        matcherState.push_back(MatchingState::kMatched);
-//        conditionCache[0] = ConditionState::kNotEvaluated;
-//        changedCache[0] = false;
-//        conditionTracker.evaluateCondition(event4, matcherState, allPredicates, conditionCache,
-//                                           changedCache);
-//        EXPECT_EQ(0UL, conditionTracker.mSlicedConditionState.size());
-//        EXPECT_TRUE(changedCache[0]);
-//        if (position == Position::FIRST ||
-//            position == Position::LAST) {
-//            EXPECT_EQ(conditionTracker.getChangedToFalseDimensions(allConditions)->size(), 1u);
-//            EXPECT_TRUE(conditionTracker.getChangedToTrueDimensions(allConditions)->empty());
-//        } else {
-//            EXPECT_EQ(conditionTracker.getChangedToFalseDimensions(allConditions)->size(), uids.size());
-//        }
-//
-//        // query again
-//        conditionCache[0] = ConditionState::kNotEvaluated;
-//        conditionTracker.isConditionMet(queryKey, allPredicates,
-//                                        false,
-//                                        conditionCache);
-//        EXPECT_EQ(ConditionState::kFalse, conditionCache[0]);
-//    }
-//
-//}
-//
+TEST(SimpleConditionTrackerTest, TestSlicedCondition) {
+    std::vector<sp<ConditionTracker>> allConditions;
+    for (Position position : {Position::FIRST, Position::LAST}) {
+        SimplePredicate simplePredicate = getWakeLockHeldCondition(
+                true /*nesting*/, true /*default to false*/, true /*output slice by uid*/,
+                position);
+        string conditionName = "WL_HELD_BY_UID2";
+
+        unordered_map<int64_t, int> trackerNameIndexMap;
+        trackerNameIndexMap[StringToId("WAKE_LOCK_ACQUIRE")] = 0;
+        trackerNameIndexMap[StringToId("WAKE_LOCK_RELEASE")] = 1;
+        trackerNameIndexMap[StringToId("RELEASE_ALL")] = 2;
+
+        SimpleConditionTracker conditionTracker(kConfigKey, StringToId(conditionName),
+                                                0 /*condition tracker index*/, simplePredicate,
+                                                trackerNameIndexMap);
+
+        std::vector<int> uids = {111, 222, 333};
+
+        LogEvent event(/*uid=*/-1, /*pid=*/-1);
+        makeWakeLockEvent(&event, /*atomId=*/ 1, /*timestamp=*/ 0, uids, "wl1", /*acquire=*/ 1);
+
+        // one matched start
+        vector<MatchingState> matcherState;
+        matcherState.push_back(MatchingState::kMatched);
+        matcherState.push_back(MatchingState::kNotMatched);
+        matcherState.push_back(MatchingState::kNotMatched);
+        vector<sp<ConditionTracker>> allPredicates;
+        vector<ConditionState> conditionCache(1, ConditionState::kNotEvaluated);
+        vector<bool> changedCache(1, false);
+
+        conditionTracker.evaluateCondition(event, matcherState, allPredicates, conditionCache,
+                                           changedCache);
+
+        if (position == Position::FIRST || position == Position::LAST) {
+            EXPECT_EQ(1UL, conditionTracker.mSlicedConditionState.size());
+        } else {
+            EXPECT_EQ(uids.size(), conditionTracker.mSlicedConditionState.size());
+        }
+        EXPECT_TRUE(changedCache[0]);
+        if (position == Position::FIRST || position == Position::LAST) {
+            EXPECT_EQ(conditionTracker.getChangedToTrueDimensions(allConditions)->size(), 1u);
+            EXPECT_TRUE(conditionTracker.getChangedToFalseDimensions(allConditions)->empty());
+        } else {
+            EXPECT_EQ(conditionTracker.getChangedToTrueDimensions(allConditions)->size(),
+                      uids.size());
+        }
+
+        // Now test query
+        const auto queryKey = getWakeLockQueryKey(position, uids, conditionName);
+        conditionCache[0] = ConditionState::kNotEvaluated;
+
+        conditionTracker.isConditionMet(queryKey, allPredicates, false, conditionCache);
+        EXPECT_EQ(ConditionState::kTrue, conditionCache[0]);
+
+        // another wake lock acquired by this uid
+        LogEvent event2(/*uid=*/-1, /*pid=*/-1);
+        makeWakeLockEvent(&event2, /*atomId=*/ 1, /*timestamp=*/ 0, uids, "wl2", /*acquire=*/ 1);
+        matcherState.clear();
+        matcherState.push_back(MatchingState::kMatched);
+        matcherState.push_back(MatchingState::kNotMatched);
+        conditionCache[0] = ConditionState::kNotEvaluated;
+        changedCache[0] = false;
+        conditionTracker.evaluateCondition(event2, matcherState, allPredicates, conditionCache,
+                                           changedCache);
+        EXPECT_FALSE(changedCache[0]);
+        if (position == Position::FIRST || position == Position::LAST) {
+            EXPECT_EQ(1UL, conditionTracker.mSlicedConditionState.size());
+        } else {
+            EXPECT_EQ(uids.size(), conditionTracker.mSlicedConditionState.size());
+        }
+        EXPECT_TRUE(conditionTracker.getChangedToTrueDimensions(allConditions)->empty());
+        EXPECT_TRUE(conditionTracker.getChangedToFalseDimensions(allConditions)->empty());
+
+
+        // wake lock 1 release
+        LogEvent event3(/*uid=*/-1, /*pid=*/-1);
+        makeWakeLockEvent(&event3, /*atomId=*/1, /*timestamp=*/0, uids, "wl1", /*acquire=*/0);
+        matcherState.clear();
+        matcherState.push_back(MatchingState::kNotMatched);
+        matcherState.push_back(MatchingState::kMatched);
+        conditionCache[0] = ConditionState::kNotEvaluated;
+        changedCache[0] = false;
+        conditionTracker.evaluateCondition(event3, matcherState, allPredicates, conditionCache,
+                                           changedCache);
+        // nothing changes, because wake lock 2 is still held for this uid
+        EXPECT_FALSE(changedCache[0]);
+        if (position == Position::FIRST || position == Position::LAST) {
+            EXPECT_EQ(1UL, conditionTracker.mSlicedConditionState.size());
+        } else {
+            EXPECT_EQ(uids.size(), conditionTracker.mSlicedConditionState.size());
+        }
+        EXPECT_TRUE(conditionTracker.getChangedToTrueDimensions(allConditions)->empty());
+        EXPECT_TRUE(conditionTracker.getChangedToFalseDimensions(allConditions)->empty());
+
+        LogEvent event4(/*uid=*/-1, /*pid=*/-1);
+        makeWakeLockEvent(&event, /*atomId=*/1, /*timestamp=*/ 0, uids, "wl2", /*acquire=*/0);
+        matcherState.clear();
+        matcherState.push_back(MatchingState::kNotMatched);
+        matcherState.push_back(MatchingState::kMatched);
+        conditionCache[0] = ConditionState::kNotEvaluated;
+        changedCache[0] = false;
+        conditionTracker.evaluateCondition(event4, matcherState, allPredicates, conditionCache,
+                                           changedCache);
+        EXPECT_EQ(0UL, conditionTracker.mSlicedConditionState.size());
+        EXPECT_TRUE(changedCache[0]);
+        if (position == Position::FIRST || position == Position::LAST) {
+            EXPECT_EQ(conditionTracker.getChangedToFalseDimensions(allConditions)->size(), 1u);
+            EXPECT_TRUE(conditionTracker.getChangedToTrueDimensions(allConditions)->empty());
+        } else {
+            EXPECT_EQ(conditionTracker.getChangedToFalseDimensions(allConditions)->size(),
+                      uids.size());
+        }
+
+        // query again
+        conditionCache[0] = ConditionState::kNotEvaluated;
+        conditionTracker.isConditionMet(queryKey, allPredicates, false, conditionCache);
+        EXPECT_EQ(ConditionState::kFalse, conditionCache[0]);
+    }
+
+}
+
 //TEST(SimpleConditionTrackerTest, TestSlicedWithNoOutputDim) {
 //    std::vector<sp<ConditionTracker>> allConditions;
 //
diff --git a/cmds/statsd/tests/external/StatsCallbackPuller_test.cpp b/cmds/statsd/tests/external/StatsCallbackPuller_test.cpp
index e416b4c..1ff6621 100644
--- a/cmds/statsd/tests/external/StatsCallbackPuller_test.cpp
+++ b/cmds/statsd/tests/external/StatsCallbackPuller_test.cpp
@@ -28,6 +28,7 @@
 
 #include "../metrics/metrics_test_helper.h"
 #include "src/stats_log_util.h"
+#include "stats_event.h"
 #include "tests/statsd_test_util.h"
 
 #ifdef __ANDROID__
diff --git a/cmds/statsd/tests/statsd_test_util.cpp b/cmds/statsd/tests/statsd_test_util.cpp
index 2bfce9b..d416f13 100644
--- a/cmds/statsd/tests/statsd_test_util.cpp
+++ b/cmds/statsd/tests/statsd_test_util.cpp
@@ -15,6 +15,7 @@
 #include "statsd_test_util.h"
 
 #include <aidl/android/util/StatsEventParcel.h>
+#include "stats_event.h"
 
 using aidl::android::util::StatsEventParcel;
 using std::shared_ptr;
diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java
index dbe3b7b..d7af1b9 100644
--- a/core/java/android/os/Process.java
+++ b/core/java/android/os/Process.java
@@ -529,6 +529,40 @@
     private static int sPidFdSupported = PIDFD_UNKNOWN;
 
     /**
+     * Value used to indicate that there is no special information about an application launch.  App
+     * launches with this policy will occur through the primary or secondary Zygote with no special
+     * treatment.
+     *
+     * @hide
+     */
+    public static final int ZYGOTE_POLICY_FLAG_EMPTY = 0;
+
+    /**
+     * Flag used to indicate that an application launch is user-visible and latency sensitive.  Any
+     * launch with this policy will use a Unspecialized App Process Pool if the target Zygote
+     * supports it.
+     *
+     * @hide
+     */
+    public static final int ZYGOTE_POLICY_FLAG_LATENCY_SENSITIVE = 1 << 0;
+
+    /**
+     * Flag used to indicate that the launch is one in a series of app launches that will be
+     * performed in quick succession.  For future use.
+     *
+     * @hide
+     */
+    public static final int ZYGOTE_POLICY_FLAG_BATCH_LAUNCH = 1 << 1;
+
+    /**
+     * Flag used to indicate that the current launch event is for a system process.  All system
+     * processes are equally important, so none of them should be prioritized over the others.
+     *
+     * @hide
+     */
+    public static final int ZYGOTE_POLICY_FLAG_SYSTEM_PROCESS = 1 << 2;
+
+    /**
      * State associated with the zygote process.
      * @hide
      */
@@ -567,6 +601,7 @@
      * @param appDataDir null-ok the data directory of the app.
      * @param invokeWith null-ok the command to invoke with.
      * @param packageName null-ok the name of the package this process belongs to.
+     * @param zygotePolicyFlags Flags used to determine how to launch the application
      * @param isTopApp whether the process starts for high priority application.
      * @param disabledCompatChanges null-ok list of disabled compat changes for the process being
      *                             started.
@@ -590,6 +625,7 @@
                                            @Nullable String appDataDir,
                                            @Nullable String invokeWith,
                                            @Nullable String packageName,
+                                           int zygotePolicyFlags,
                                            boolean isTopApp,
                                            @Nullable long[] disabledCompatChanges,
                                            @Nullable Map<String, Pair<String, Long>>
@@ -598,7 +634,7 @@
         return ZYGOTE_PROCESS.start(processClass, niceName, uid, gid, gids,
                     runtimeFlags, mountExternal, targetSdkVersion, seInfo,
                     abi, instructionSet, appDataDir, invokeWith, packageName,
-                    /*useUsapPool=*/ true, isTopApp, disabledCompatChanges,
+                    zygotePolicyFlags, isTopApp, disabledCompatChanges,
                     pkgDataInfoMap, zygoteArgs);
     }
 
@@ -622,8 +658,8 @@
         return WebViewZygote.getProcess().start(processClass, niceName, uid, gid, gids,
                     runtimeFlags, mountExternal, targetSdkVersion, seInfo,
                     abi, instructionSet, appDataDir, invokeWith, packageName,
-                    /*useUsapPool=*/ false, /*isTopApp=*/ false, disabledCompatChanges,
-                    /* pkgDataInfoMap */ null, zygoteArgs);
+                    /*zygotePolicyFlags=*/ ZYGOTE_POLICY_FLAG_EMPTY, /*isTopApp=*/ false,
+                disabledCompatChanges, /* pkgDataInfoMap */ null, zygoteArgs);
     }
 
     /**
diff --git a/core/java/android/os/ZygoteProcess.java b/core/java/android/os/ZygoteProcess.java
index 3846f89..34cec06 100644
--- a/core/java/android/os/ZygoteProcess.java
+++ b/core/java/android/os/ZygoteProcess.java
@@ -16,6 +16,9 @@
 
 package android.os;
 
+import static android.os.Process.ZYGOTE_POLICY_FLAG_LATENCY_SENSITIVE;
+import static android.os.Process.ZYGOTE_POLICY_FLAG_SYSTEM_PROCESS;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.compat.annotation.UnsupportedAppUsage;
@@ -119,6 +122,10 @@
         mUsapPoolSecondarySocketAddress =
                 new LocalSocketAddress(Zygote.USAP_POOL_SECONDARY_SOCKET_NAME,
                                        LocalSocketAddress.Namespace.RESERVED);
+
+        // This constructor is used to create the primary and secondary Zygotes, which can support
+        // Unspecialized App Process Pools.
+        mUsapPoolSupported = true;
     }
 
     public ZygoteProcess(LocalSocketAddress primarySocketAddress,
@@ -128,6 +135,10 @@
 
         mUsapPoolSocketAddress = null;
         mUsapPoolSecondarySocketAddress = null;
+
+        // This constructor is used to create the primary and secondary Zygotes, which CAN NOT
+        // support Unspecialized App Process Pools.
+        mUsapPoolSupported = false;
     }
 
     public LocalSocketAddress getPrimarySocketAddress() {
@@ -267,6 +278,14 @@
     private ZygoteState secondaryZygoteState;
 
     /**
+     * If this Zygote supports the creation and maintenance of a USAP pool.
+     *
+     * Currently only the primary and secondary Zygotes support USAP pools. Any
+     * child Zygotes will be unable to create or use a USAP pool.
+     */
+    private final boolean mUsapPoolSupported;
+
+    /**
      * If the USAP pool should be created and used to start applications.
      *
      * Setting this value to false will disable the creation, maintenance, and use of the USAP
@@ -308,13 +327,14 @@
      * @param appDataDir null-ok the data directory of the app.
      * @param invokeWith null-ok the command to invoke with.
      * @param packageName null-ok the name of the package this process belongs to.
+     * @param zygotePolicyFlags Flags used to determine how to launch the application.
+     * @param isTopApp Whether the process starts for high priority application.
      * @param disabledCompatChanges null-ok list of disabled compat changes for the process being
      *                             started.
-     * @param zygoteArgs Additional arguments to supply to the zygote process.
-     * @param isTopApp Whether the process starts for high priority application.
      * @param pkgDataInfoMap Map from related package names to private data directory
      *                       volume UUID and inode number.
      *
+     * @param zygoteArgs Additional arguments to supply to the Zygote process.
      * @return An object that describes the result of the attempt to start the process.
      * @throws RuntimeException on fatal start failure
      */
@@ -329,7 +349,7 @@
                                                   @Nullable String appDataDir,
                                                   @Nullable String invokeWith,
                                                   @Nullable String packageName,
-                                                  boolean useUsapPool,
+                                                  int zygotePolicyFlags,
                                                   boolean isTopApp,
                                                   @Nullable long[] disabledCompatChanges,
                                                   @Nullable Map<String, Pair<String, Long>>
@@ -344,7 +364,7 @@
             return startViaZygote(processClass, niceName, uid, gid, gids,
                     runtimeFlags, mountExternal, targetSdkVersion, seInfo,
                     abi, instructionSet, appDataDir, invokeWith, /*startChildZygote=*/ false,
-                    packageName, useUsapPool, isTopApp, disabledCompatChanges,
+                    packageName, zygotePolicyFlags, isTopApp, disabledCompatChanges,
                     pkgDataInfoMap, zygoteArgs);
         } catch (ZygoteStartFailedEx ex) {
             Log.e(LOG_TAG,
@@ -391,7 +411,7 @@
      */
     @GuardedBy("mLock")
     private Process.ProcessStartResult zygoteSendArgsAndGetResult(
-            ZygoteState zygoteState, boolean useUsapPool, @NonNull ArrayList<String> args)
+            ZygoteState zygoteState, int zygotePolicyFlags, @NonNull ArrayList<String> args)
             throws ZygoteStartFailedEx {
         // Throw early if any of the arguments are malformed. This means we can
         // avoid writing a partial response to the zygote.
@@ -417,7 +437,7 @@
          */
         String msgStr = args.size() + "\n" + String.join("\n", args) + "\n";
 
-        if (useUsapPool && mUsapPoolEnabled && canAttemptUsap(args)) {
+        if (shouldAttemptUsapLaunch(zygotePolicyFlags, args)) {
             try {
                 return attemptUsapSendArgsAndGetResult(zygoteState, msgStr);
             } catch (IOException ex) {
@@ -488,7 +508,43 @@
     }
 
     /**
-     * Flags that may not be passed to a USAP.
+     * Test various member properties and parameters to determine if a launch event should be
+     * handled using an Unspecialized App Process Pool or not.
+     *
+     * @param zygotePolicyFlags Policy flags indicating special behavioral observations about the
+     *                          Zygote command
+     * @param args Arguments that will be passed to the Zygote
+     * @return If the command should be sent to a USAP Pool member or an actual Zygote
+     */
+    private boolean shouldAttemptUsapLaunch(int zygotePolicyFlags, ArrayList<String> args) {
+        return mUsapPoolSupported
+                && mUsapPoolEnabled
+                && policySpecifiesUsapPoolLaunch(zygotePolicyFlags)
+                && commandSupportedByUsap(args);
+    }
+
+    /**
+     * Tests a Zygote policy flag set for various properties that determine if it is eligible for
+     * being handled by an Unspecialized App Process Pool.
+     *
+     * @param zygotePolicyFlags Policy flags indicating special behavioral observations about the
+     *                          Zygote command
+     * @return If the policy allows for use of a USAP pool
+     */
+    private static boolean policySpecifiesUsapPoolLaunch(int zygotePolicyFlags) {
+        /*
+         * Zygote USAP Pool Policy: Launch the new process from the USAP Pool iff the launch event
+         * is latency sensitive but *NOT* a system process.  All system processes are equally
+         * important so we don't want to prioritize one over another.
+         */
+        return (zygotePolicyFlags
+                & (ZYGOTE_POLICY_FLAG_SYSTEM_PROCESS | ZYGOTE_POLICY_FLAG_LATENCY_SENSITIVE))
+                == ZYGOTE_POLICY_FLAG_LATENCY_SENSITIVE;
+    }
+
+    /**
+     * Flags that may not be passed to a USAP.  These may appear as prefixes to individual Zygote
+     * arguments.
      */
     private static final String[] INVALID_USAP_FLAGS = {
         "--query-abi-list",
@@ -505,10 +561,11 @@
 
     /**
      * Tests a command list to see if it is valid to send to a USAP.
+     *
      * @param args  Zygote/USAP command arguments
      * @return  True if the command can be passed to a USAP; false otherwise
      */
-    private static boolean canAttemptUsap(ArrayList<String> args) {
+    private static boolean commandSupportedByUsap(ArrayList<String> args) {
         for (String flag : args) {
             for (String badFlag : INVALID_USAP_FLAGS) {
                 if (flag.startsWith(badFlag)) {
@@ -544,6 +601,7 @@
      * @param startChildZygote Start a sub-zygote. This creates a new zygote process
      * that has its state cloned from this zygote process.
      * @param packageName null-ok the name of the package this process belongs to.
+     * @param zygotePolicyFlags Flags used to determine how to launch the application.
      * @param isTopApp Whether the process starts for high priority application.
      * @param disabledCompatChanges a list of disabled compat changes for the process being started.
      * @param pkgDataInfoMap Map from related package names to private data directory volume UUID
@@ -565,7 +623,7 @@
                                                       @Nullable String invokeWith,
                                                       boolean startChildZygote,
                                                       @Nullable String packageName,
-                                                      boolean useUsapPool,
+                                                      int zygotePolicyFlags,
                                                       boolean isTopApp,
                                                       @Nullable long[] disabledCompatChanges,
                                                       @Nullable Map<String, Pair<String, Long>>
@@ -692,7 +750,7 @@
             // The USAP pool can not be used if the application will not use the systems graphics
             // driver.  If that driver is requested use the Zygote application start path.
             return zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi),
-                                              useUsapPool,
+                                              zygotePolicyFlags,
                                               argsForZygote);
         }
     }
@@ -722,6 +780,10 @@
     private long mLastPropCheckTimestamp = 0;
 
     private boolean fetchUsapPoolEnabledPropWithMinInterval() {
+        // If this Zygote doesn't support USAPs there is no need to fetch any
+        // properties.
+        if (!mUsapPoolSupported) return false;
+
         final long currentTimestamp = SystemClock.elapsedRealtime();
 
         if (mIsFirstPropCheck
@@ -1219,7 +1281,7 @@
                     gids, runtimeFlags, 0 /* mountExternal */, 0 /* targetSdkVersion */, seInfo,
                     abi, instructionSet, null /* appDataDir */, null /* invokeWith */,
                     true /* startChildZygote */, null /* packageName */,
-                    false /* useUsapPool */, false /* isTopApp */,
+                    ZYGOTE_POLICY_FLAG_SYSTEM_PROCESS /* zygotePolicyFlags */, false /* isTopApp */,
                     null /* disabledCompatChanges */, null /* pkgDataInfoMap */, extraArgs);
         } catch (ZygoteStartFailedEx ex) {
             throw new RuntimeException("Starting child-zygote through Zygote failed", ex);
diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java
index a47bd17..54cf693 100644
--- a/core/java/com/android/internal/os/ZygoteInit.java
+++ b/core/java/com/android/internal/os/ZygoteInit.java
@@ -19,6 +19,9 @@
 import static android.system.OsConstants.S_IRWXG;
 import static android.system.OsConstants.S_IRWXO;
 
+import static com.android.internal.util.FrameworkStatsLog.BOOT_TIME_EVENT_ELAPSED_TIME__EVENT__SECONDARY_ZYGOTE_INIT_START;
+import static com.android.internal.util.FrameworkStatsLog.BOOT_TIME_EVENT_ELAPSED_TIME__EVENT__ZYGOTE_INIT_START;
+
 import android.app.ApplicationLoaders;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.pm.SharedLibraryInfo;
@@ -52,7 +55,7 @@
 import android.webkit.WebViewFactory;
 import android.widget.TextView;
 
-import com.android.internal.logging.MetricsLogger;
+import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.Preconditions;
 
 import dalvik.system.DexFile;
@@ -863,11 +866,10 @@
 
         Runnable caller;
         try {
-            // Report Zygote start time to tron unless it is a runtime restart
-            if (!"1".equals(SystemProperties.get("sys.boot_completed"))) {
-                MetricsLogger.histogram(null, "boot_zygote_init",
-                        (int) SystemClock.elapsedRealtime());
-            }
+            // Store now for StatsLogging later.
+            final long startTime = SystemClock.elapsedRealtime();
+            final boolean isRuntimeRestarted = "1".equals(
+                    SystemProperties.get("sys.boot_completed"));
 
             String bootTimeTag = Process.is64Bit() ? "Zygote64Timing" : "Zygote32Timing";
             TimingsTraceLog bootTimingsTraceLog = new TimingsTraceLog(bootTimeTag,
@@ -894,6 +896,17 @@
             }
 
             final boolean isPrimaryZygote = zygoteSocketName.equals(Zygote.PRIMARY_SOCKET_NAME);
+            if (!isRuntimeRestarted) {
+                if (isPrimaryZygote) {
+                    FrameworkStatsLog.write(FrameworkStatsLog.BOOT_TIME_EVENT_ELAPSED_TIME_REPORTED,
+                            BOOT_TIME_EVENT_ELAPSED_TIME__EVENT__ZYGOTE_INIT_START,
+                            startTime);
+                } else if (zygoteSocketName.equals(Zygote.SECONDARY_SOCKET_NAME)) {
+                    FrameworkStatsLog.write(FrameworkStatsLog.BOOT_TIME_EVENT_ELAPSED_TIME_REPORTED,
+                            BOOT_TIME_EVENT_ELAPSED_TIME__EVENT__SECONDARY_ZYGOTE_INIT_START,
+                            startTime);
+                }
+            }
 
             if (abiList == null) {
                 throw new RuntimeException("No ABI list supplied.");
diff --git a/core/proto/android/server/jobscheduler.proto b/core/proto/android/server/jobscheduler.proto
index 4bef2e3..bd1bae6 100644
--- a/core/proto/android/server/jobscheduler.proto
+++ b/core/proto/android/server/jobscheduler.proto
@@ -670,7 +670,7 @@
 
             repeated ExecutionStats execution_stats = 4;
 
-            optional AlarmListener in_quota_alarm_listener = 5;
+            reserved 5; // in_quota_alarm_listener
         }
         repeated PackageStats package_stats = 5;
 
@@ -683,7 +683,25 @@
         }
         repeated UidPackageMapping uid_to_package_cache = 7;
 
-        // Next tag: 8
+        message InQuotaAlarmListener {
+            option (.android.msg_privacy).dest = DEST_AUTOMATIC;
+
+            // The time at which the alarm is set to go off, in the elapsed realtime timebase.
+            optional int64 trigger_time_elapsed = 1;
+
+            message Alarm {
+                option (.android.msg_privacy).dest = DEST_AUTOMATIC;
+
+                optional Package pkg = 1;
+
+                // The time at which the package will be in quota, in the elapsed realtime timebase.
+                optional int64 in_quota_time_elapsed = 2;
+            }
+            repeated Alarm alarms = 2;
+        }
+        optional InQuotaAlarmListener in_quota_alarm_listener = 8;
+
+        // Next tag: 9
     }
     message StorageController {
         option (.android.msg_privacy).dest = DEST_AUTOMATIC;
diff --git a/media/java/android/media/tv/tuner/Tuner.java b/media/java/android/media/tv/tuner/Tuner.java
index 7a684b3..c79e72d 100644
--- a/media/java/android/media/tv/tuner/Tuner.java
+++ b/media/java/android/media/tv/tuner/Tuner.java
@@ -216,8 +216,8 @@
     private native int nativeSetLnb(int lnbId);
     private native int nativeSetLna(boolean enable);
     private native FrontendStatus nativeGetFrontendStatus(int[] statusTypes);
-    private native int nativeGetAvSyncHwId(Filter filter);
-    private native long nativeGetAvSyncTime(int avSyncId);
+    private native Integer nativeGetAvSyncHwId(Filter filter);
+    private native Long nativeGetAvSyncTime(int avSyncId);
     private native int nativeConnectCiCam(int ciCamId);
     private native int nativeDisconnectCiCam();
     private native FrontendInfo nativeGetFrontendInfo(int id);
@@ -463,7 +463,8 @@
     @RequiresPermission(android.Manifest.permission.ACCESS_TV_TUNER)
     public int getAvSyncHwId(@NonNull Filter filter) {
         TunerUtils.checkTunerPermission(mContext);
-        return nativeGetAvSyncHwId(filter);
+        Integer id = nativeGetAvSyncHwId(filter);
+        return id == null ? TunerConstants.INVALID_AV_SYNC_ID : id;
     }
 
     /**
@@ -478,7 +479,8 @@
     @RequiresPermission(android.Manifest.permission.ACCESS_TV_TUNER)
     public long getAvSyncTime(int avSyncHwId) {
         TunerUtils.checkTunerPermission(mContext);
-        return nativeGetAvSyncTime(avSyncHwId);
+        Long time = nativeGetAvSyncTime(avSyncHwId);
+        return time == null ? TunerConstants.TIMESTAMP_UNAVAILABLE : time;
     }
 
     /**
diff --git a/media/java/android/media/tv/tuner/TunerConstants.java b/media/java/android/media/tv/tuner/TunerConstants.java
index 82af658..6d89962 100644
--- a/media/java/android/media/tv/tuner/TunerConstants.java
+++ b/media/java/android/media/tv/tuner/TunerConstants.java
@@ -46,6 +46,19 @@
      * Invalid AV Sync ID.
      */
     public static final int INVALID_AV_SYNC_ID = Constants.Constant.INVALID_AV_SYNC_ID;
+    /**
+     * Timestamp is unavailable.
+     *
+     * <p>Returned by {@link android.media.tv.tuner.filter.TimeFilter#getSourceTime()},
+     * {@link android.media.tv.tuner.filter.TimeFilter#getTimeStamp()}, or
+     * {@link Tuner#getAvSyncTime(int)} when the requested timestamp is not available.
+     *
+     * @see android.media.tv.tuner.filter.TimeFilter#getSourceTime()
+     * @see android.media.tv.tuner.filter.TimeFilter#getTimeStamp()
+     * @see Tuner#getAvSyncTime(int)
+     * @hide
+     */
+    public static final long TIMESTAMP_UNAVAILABLE = -1L;
 
     /** @hide */
     @IntDef(prefix = "SCAN_TYPE_", value = {SCAN_TYPE_UNDEFINED, SCAN_TYPE_AUTO, SCAN_TYPE_BLIND})
diff --git a/media/jni/android_media_tv_Tuner.cpp b/media/jni/android_media_tv_Tuner.cpp
index 5c39f29..e68ccfa 100644
--- a/media/jni/android_media_tv_Tuner.cpp
+++ b/media/jni/android_media_tv_Tuner.cpp
@@ -119,7 +119,6 @@
 using ::android::hardware::tv::tuner::V1_0::ITuner;
 using ::android::hardware::tv::tuner::V1_0::PlaybackSettings;
 using ::android::hardware::tv::tuner::V1_0::RecordSettings;
-using ::android::hardware::tv::tuner::V1_0::Result;
 
 struct fields_t {
     jfieldID tunerContext;
@@ -845,22 +844,85 @@
     return (int)result;
 }
 
-bool JTuner::openDemux() {
+Result JTuner::openDemux() {
     if (mTuner == nullptr) {
-        return false;
+        return Result::NOT_INITIALIZED;
     }
     if (mDemux != nullptr) {
-        return true;
+        return Result::SUCCESS;
     }
-    mTuner->openDemux([&](Result, uint32_t demuxId, const sp<IDemux>& demux) {
+    Result res;
+    mTuner->openDemux([&](Result r, uint32_t demuxId, const sp<IDemux>& demux) {
         mDemux = demux;
         mDemuxId = demuxId;
+        res = r;
         ALOGD("open demux, id = %d", demuxId);
     });
-    if (mDemux == nullptr) {
-        return false;
+    return res;
+}
+
+jobject JTuner::getAvSyncHwId(sp<Filter> filter) {
+    if (mDemux == NULL) {
+        return NULL;
     }
-    return true;
+
+    uint32_t avSyncHwId;
+    Result res;
+    sp<IFilter> iFilterSp = filter->getIFilter();
+    mDemux->getAvSyncHwId(iFilterSp,
+            [&](Result r, uint32_t id) {
+                res = r;
+                avSyncHwId = id;
+            });
+    if (res == Result::SUCCESS) {
+        JNIEnv *env = AndroidRuntime::getJNIEnv();
+        jclass integerClazz = env->FindClass("java/lang/Integer");
+        jmethodID intInit = env->GetMethodID(integerClazz, "<init>", "(I)V");
+        return env->NewObject(integerClazz, intInit, avSyncHwId);
+    }
+    return NULL;
+}
+
+jobject JTuner::getAvSyncTime(jint id) {
+    if (mDemux == NULL) {
+        return NULL;
+    }
+    uint64_t time;
+    Result res;
+    mDemux->getAvSyncTime(static_cast<uint32_t>(id),
+            [&](Result r, uint64_t ts) {
+                res = r;
+                time = ts;
+            });
+    if (res == Result::SUCCESS) {
+        JNIEnv *env = AndroidRuntime::getJNIEnv();
+        jclass longClazz = env->FindClass("java/lang/Long");
+        jmethodID longInit = env->GetMethodID(longClazz, "<init>", "(J)V");
+        return env->NewObject(longClazz, longInit, static_cast<jlong>(time));
+    }
+    return NULL;
+}
+
+int JTuner::connectCiCam(jint id) {
+    if (mDemux == NULL) {
+        Result r = openDemux();
+        if (r != Result::SUCCESS) {
+            return (int) r;
+        }
+    }
+    Result r = mDemux->connectCiCam(static_cast<uint32_t>(id));
+    return (int) r;
+}
+
+int JTuner::disconnectCiCam() {
+    if (mDemux == NULL) {
+        Result r = openDemux();
+        if (r != Result::SUCCESS) {
+            return (int) r;
+        }
+    }
+    Result r = mDemux->disconnectCiCam();
+    return (int) r;
 }
 
 jobject JTuner::openDescrambler() {
@@ -892,7 +954,7 @@
 
 jobject JTuner::openFilter(DemuxFilterType type, int bufferSize) {
     if (mDemux == NULL) {
-        if (!openDemux()) {
+        if (openDemux() != Result::SUCCESS) {
             return NULL;
         }
     }
@@ -917,7 +979,6 @@
             env->NewObject(
                     env->FindClass("android/media/tv/tuner/filter/Filter"),
                     gFields.filterInitID,
-                    mObject,
                     (jint) fId);
 
     sp<Filter> filterSp = new Filter(iFilterSp, filterObj);
@@ -931,7 +992,7 @@
 
 jobject JTuner::openTimeFilter() {
     if (mDemux == NULL) {
-        if (!openDemux()) {
+        if (openDemux() != Result::SUCCESS) {
             return NULL;
         }
     }
@@ -962,7 +1023,7 @@
 jobject JTuner::openDvr(DvrType type, int bufferSize) {
     ALOGD("JTuner::openDvr");
     if (mDemux == NULL) {
-        if (!openDemux()) {
+        if (openDemux() != Result::SUCCESS) {
             return NULL;
         }
     }
@@ -1608,20 +1669,30 @@
     return NULL;
 }
 
-static int android_media_tv_Tuner_gat_av_sync_hw_id(JNIEnv*, jobject, jobject) {
-    return 0;
+static jobject android_media_tv_Tuner_get_av_sync_hw_id(
+        JNIEnv *env, jobject thiz, jobject filter) {
+    sp<Filter> filterSp = getFilter(env, filter);
+    if (filterSp == NULL) {
+        ALOGD("Failed to get sync ID. Filter not found");
+        return NULL;
+    }
+    sp<JTuner> tuner = getTuner(env, thiz);
+    return tuner->getAvSyncHwId(filterSp);
 }
 
-static jlong android_media_tv_Tuner_gat_av_sync_time(JNIEnv*, jobject, jint) {
-    return 0;
+static jobject android_media_tv_Tuner_get_av_sync_time(JNIEnv *env, jobject thiz, jint id) {
+    sp<JTuner> tuner = getTuner(env, thiz);
+    return tuner->getAvSyncTime(id);
 }
 
-static int android_media_tv_Tuner_connect_cicam(JNIEnv*, jobject, jint) {
-    return 0;
+static int android_media_tv_Tuner_connect_cicam(JNIEnv *env, jobject thiz, jint id) {
+    sp<JTuner> tuner = getTuner(env, thiz);
+    return tuner->connectCiCam(id);
 }
 
-static int android_media_tv_Tuner_disconnect_cicam(JNIEnv*, jobject) {
-    return 0;
+static int android_media_tv_Tuner_disconnect_cicam(JNIEnv *env, jobject thiz) {
+    sp<JTuner> tuner = getTuner(env, thiz);
+    return tuner->disconnectCiCam();
 }
 
 static jobject android_media_tv_Tuner_get_frontend_info(JNIEnv *env, jobject thiz, jint id) {
@@ -2500,9 +2571,10 @@
     { "nativeSetLna", "(Z)I", (void *)android_media_tv_Tuner_set_lna },
     { "nativeGetFrontendStatus", "([I)Landroid/media/tv/tuner/frontend/FrontendStatus;",
             (void *)android_media_tv_Tuner_get_frontend_status },
-    { "nativeGetAvSyncHwId", "(Landroid/media/tv/tuner/filter/Filter;)I",
-            (void *)android_media_tv_Tuner_gat_av_sync_hw_id },
-    { "nativeGetAvSyncTime", "(I)J", (void *)android_media_tv_Tuner_gat_av_sync_time },
+    { "nativeGetAvSyncHwId", "(Landroid/media/tv/tuner/filter/Filter;)Ljava/lang/Integer;",
+            (void *)android_media_tv_Tuner_get_av_sync_hw_id },
+    { "nativeGetAvSyncTime", "(I)Ljava/lang/Long;",
+            (void *)android_media_tv_Tuner_get_av_sync_time },
     { "nativeConnectCiCam", "(I)I", (void *)android_media_tv_Tuner_connect_cicam },
     { "nativeDisconnectCiCam", "()I", (void *)android_media_tv_Tuner_disconnect_cicam },
     { "nativeGetFrontendInfo", "(I)Landroid/media/tv/tuner/frontend/FrontendInfo;",
diff --git a/media/jni/android_media_tv_Tuner.h b/media/jni/android_media_tv_Tuner.h
index 32d4899..b786fc4 100644
--- a/media/jni/android_media_tv_Tuner.h
+++ b/media/jni/android_media_tv_Tuner.h
@@ -60,6 +60,7 @@
 using ::android::hardware::tv::tuner::V1_0::LnbId;
 using ::android::hardware::tv::tuner::V1_0::PlaybackStatus;
 using ::android::hardware::tv::tuner::V1_0::RecordStatus;
+using ::android::hardware::tv::tuner::V1_0::Result;
 
 using FilterMQ = MessageQueue<uint8_t, kSynchronizedReadWrite>;
 using DvrMQ = MessageQueue<uint8_t, kSynchronizedReadWrite>;
@@ -141,6 +142,10 @@
 struct JTuner : public RefBase {
     JTuner(JNIEnv *env, jobject thiz);
     sp<ITuner> getTunerService();
+    jobject getAvSyncHwId(sp<Filter> filter);
+    jobject getAvSyncTime(jint id);
+    int connectCiCam(jint id);
+    int disconnectCiCam();
     jobject getFrontendIds();
     jobject openFrontendById(int id);
     jobject getFrontendInfo(int id);
@@ -158,7 +163,7 @@
     jobject openDvr(DvrType type, int bufferSize);
 
 protected:
-    bool openDemux();
+    Result openDemux();
     virtual ~JTuner();
 
 private:
diff --git a/packages/CompanionDeviceManager/AndroidManifest.xml b/packages/CompanionDeviceManager/AndroidManifest.xml
index a9c6685..ea9b52c 100644
--- a/packages/CompanionDeviceManager/AndroidManifest.xml
+++ b/packages/CompanionDeviceManager/AndroidManifest.xml
@@ -30,11 +30,13 @@
     <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
     <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
     <uses-permission android:name="android.permission.RADIO_SCAN_WITHOUT_LOCATION"/>
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
 
     <application
         android:allowClearUserData="true"
         android:label="@string/app_label"
         android:allowBackup="false"
+        android:forceQueryable="true"
         android:supportsRtl="true">
 
         <service
diff --git a/packages/SettingsLib/res/values/colors.xml b/packages/SettingsLib/res/values/colors.xml
index 5e8779f..88c6185 100644
--- a/packages/SettingsLib/res/values/colors.xml
+++ b/packages/SettingsLib/res/values/colors.xml
@@ -39,4 +39,7 @@
 
     <color name="dark_mode_icon_color_single_tone">#99000000</color>
     <color name="light_mode_icon_color_single_tone">#ffffff</color>
+
+    <!-- Yellow 600, used for highlighting "important" conversations in settings & notifications -->
+    <color name="important_conversation">#f9ab00</color>
 </resources>
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 2297ddf..e8e1d0b 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -895,7 +895,7 @@
     <!-- UI debug setting: enable verbose vendor logging [CHAR LIMIT=60] -->
     <string name="enable_verbose_vendor_logging">Enable verbose vendor logging</string>
     <!-- UI debug setting: enable verbose vendor logging summary [CHAR LIMIT=NONE] -->
-    <string name="enable_verbose_vendor_logging_summary">Allow additional vendor logs to be included in bug reports, may contain private information</string>
+    <string name="enable_verbose_vendor_logging_summary">Include additional device-specific vendor logs in bug reports, which may contain private information, use more battery, and/or use more storage.</string>
 
     <!-- UI debug setting: scaling factor for window animations [CHAR LIMIT=25] -->
     <string name="window_animation_scale_title">Window animation scale</string>
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/ConversationIconFactory.java b/packages/SettingsLib/src/com/android/settingslib/notification/ConversationIconFactory.java
index 885b7d3..9dc454f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/ConversationIconFactory.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/ConversationIconFactory.java
@@ -15,32 +15,48 @@
  */
 package com.android.settingslib.notification;
 
+import android.annotation.ColorInt;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherApps;
 import android.content.pm.PackageManager;
 import android.content.pm.ShortcutInfo;
-import android.graphics.Bitmap;
 import android.graphics.Canvas;
-import android.graphics.drawable.BitmapDrawable;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.os.UserHandle;
 import android.util.IconDrawableFactory;
+import android.util.Log;
 
 import com.android.launcher3.icons.BaseIconFactory;
-import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.icons.ShadowGenerator;
+import com.android.settingslib.R;
 
 /**
  * Factory for creating normalized conversation icons.
  * We are not using Launcher's IconFactory because conversation rendering only runs on the UI
- * thread, so there is no need to manage a pool across multiple threads.
+ * thread, so there is no need to manage a pool across multiple threads. Launcher's rendering
+ * also includes shadows, which are only appropriate on top of wallpaper, not embedded in UI.
  */
 public class ConversationIconFactory extends BaseIconFactory {
+    // Geometry of the various parts of the design. All values are 1dp on a 48x48dp icon grid.
+    // Space is left around the "head" (main avatar) for
+    // ........
+    // .HHHHHH.
+    // .HHHrrrr
+    // .HHHrBBr
+    // ....rrrr
+
+    private static final float BASE_ICON_SIZE = 48f;
+    private static final float RING_STROKE_WIDTH = 2f;
+    private static final float HEAD_SIZE = BASE_ICON_SIZE - RING_STROKE_WIDTH * 2 - 2; // 40
+    private static final float BADGE_SIZE = HEAD_SIZE * 0.4f; // 16
 
     final LauncherApps mLauncherApps;
     final PackageManager mPackageManager;
     final IconDrawableFactory mIconDrawableFactory;
+    private int mImportantConversationColor;
 
     public ConversationIconFactory(Context context, LauncherApps la, PackageManager pm,
             IconDrawableFactory iconDrawableFactory, int iconSizePx) {
@@ -49,65 +65,156 @@
         mLauncherApps = la;
         mPackageManager = pm;
         mIconDrawableFactory = iconDrawableFactory;
+        mImportantConversationColor = context.getResources().getColor(
+                R.color.important_conversation, null);
     }
 
-    private int getBadgeSize() {
-        return mContext.getResources().getDimensionPixelSize(
-                com.android.launcher3.icons.R.dimen.profile_badge_size);
-    }
     /**
      * Returns the conversation info drawable
      */
-    private Drawable getConversationDrawable(ShortcutInfo shortcutInfo) {
+    private Drawable getBaseIconDrawable(ShortcutInfo shortcutInfo) {
         return mLauncherApps.getShortcutIconDrawable(shortcutInfo, mFillResIconDpi);
     }
 
     /**
-     * Get the {@link Drawable} that represents the app icon
+     * Get the {@link Drawable} that represents the app icon, badged with the work profile icon
+     * if appropriate.
      */
-    private Drawable getBadgedIcon(String packageName, int userId) {
+    private Drawable getAppBadge(String packageName, int userId) {
+        Drawable badge = null;
         try {
             final ApplicationInfo appInfo = mPackageManager.getApplicationInfoAsUser(
                     packageName, PackageManager.GET_META_DATA, userId);
-            return mIconDrawableFactory.getBadgedIcon(appInfo, userId);
+            badge = mIconDrawableFactory.getBadgedIcon(appInfo, userId);
         } catch (PackageManager.NameNotFoundException e) {
-            return mPackageManager.getDefaultActivityIcon();
+            badge = mPackageManager.getDefaultActivityIcon();
         }
+        return badge;
     }
 
     /**
-     * Turns a Drawable into a Bitmap
+     * Returns a {@link Drawable} for the entire conversation. The shortcut icon will be badged
+     * with the launcher icon of the app specified by packageName.
      */
-    BitmapInfo toBitmap(Drawable userBadgedAppIcon) {
-        Bitmap bitmap = createIconBitmap(
-                userBadgedAppIcon, 1f, getBadgeSize());
-
-        Canvas c = new Canvas();
-        ShadowGenerator shadowGenerator = new ShadowGenerator(getBadgeSize());
-        c.setBitmap(bitmap);
-        shadowGenerator.recreateIcon(Bitmap.createBitmap(bitmap), c);
-        return createIconBitmap(bitmap);
+    public Drawable getConversationDrawable(ShortcutInfo info, String packageName, int uid,
+            boolean important) {
+        return getConversationDrawable(getBaseIconDrawable(info), packageName, uid, important);
     }
 
     /**
-     * Returns a {@link BitmapInfo} for the entire conversation icon including the badge.
+     * Returns a {@link Drawable} for the entire conversation. The drawable will be badged
+     * with the launcher icon of the app specified by packageName.
      */
-    public Bitmap getConversationBitmap(ShortcutInfo info, String packageName, int uid) {
-        return getConversationBitmap(getConversationDrawable(info), packageName, uid);
+    public Drawable getConversationDrawable(Drawable baseIcon, String packageName, int uid,
+            boolean important) {
+        return new ConversationIconDrawable(baseIcon,
+                getAppBadge(packageName, UserHandle.getUserId(uid)),
+                mIconBitmapSize,
+                mImportantConversationColor,
+                important);
     }
 
     /**
-     * Returns a {@link BitmapInfo} for the entire conversation icon including the badge.
+     * Custom Drawable that overlays a badge drawable (e.g. notification small icon or app icon) on
+     * a base icon (conversation/person avatar), plus decorations indicating conversation
+     * importance.
      */
-    public Bitmap getConversationBitmap(Drawable baseIcon, String packageName, int uid) {
-        int userId = UserHandle.getUserId(uid);
-        Drawable badge = getBadgedIcon(packageName, userId);
-        BitmapInfo iconInfo = createBadgedIconBitmap(baseIcon,
-                UserHandle.of(userId),
-                true /* shrinkNonAdaptiveIcons */);
+    public static class ConversationIconDrawable extends Drawable {
+        private Drawable mBaseIcon;
+        private Drawable mBadgeIcon;
+        private int mIconSize;
+        private Paint mRingPaint;
+        private boolean mShowRing;
 
-        badgeWithDrawable(iconInfo.icon,
-                new BitmapDrawable(mContext.getResources(), toBitmap(badge).icon));
-        return iconInfo.icon;
+        public ConversationIconDrawable(Drawable baseIcon,
+                Drawable badgeIcon,
+                int iconSize,
+                @ColorInt int ringColor,
+                boolean showImportanceRing) {
+            mBaseIcon = baseIcon;
+            mBadgeIcon = badgeIcon;
+            mIconSize = iconSize;
+            mShowRing = showImportanceRing;
+            mRingPaint = new Paint();
+            mRingPaint.setStyle(Paint.Style.STROKE);
+            mRingPaint.setColor(ringColor);
+        }
+
+        /**
+         * Show or hide the importance ring.
+         */
+        public void setImportant(boolean important) {
+            if (important != mShowRing) {
+                mShowRing = important;
+                invalidateSelf();
+            }
+        }
+
+        @Override
+        public int getIntrinsicWidth() {
+            return mIconSize;
+        }
+
+        @Override
+        public int getIntrinsicHeight() {
+            return mIconSize;
+        }
+
+        // Similar to badgeWithDrawable, but relying on the bounds of each underlying drawable
+        @Override
+        public void draw(Canvas canvas) {
+            final Rect bounds = getBounds();
+
+            // scale to our internal 48x48 grid
+            final float scale = bounds.width() / BASE_ICON_SIZE;
+            final int centerX = bounds.centerX();
+            final int centerY = bounds.centerX();
+            final int ringStrokeWidth = (int) (RING_STROKE_WIDTH * scale);
+            final int headSize = (int) (HEAD_SIZE * scale);
+            final int badgeSize = (int) (BADGE_SIZE * scale);
+
+            if (mBaseIcon != null) {
+                mBaseIcon.setBounds(
+                        centerX - headSize / 2,
+                        centerY - headSize / 2,
+                        centerX + headSize / 2,
+                        centerY + headSize / 2);
+                mBaseIcon.draw(canvas);
+            } else {
+                Log.w("ConversationIconFactory", "ConversationIconDrawable has null base icon");
+            }
+            if (mBadgeIcon != null) {
+                mBadgeIcon.setBounds(
+                        bounds.right - badgeSize - ringStrokeWidth,
+                        bounds.bottom - badgeSize - ringStrokeWidth,
+                        bounds.right - ringStrokeWidth,
+                        bounds.bottom - ringStrokeWidth);
+                mBadgeIcon.draw(canvas);
+            } else {
+                Log.w("ConversationIconFactory", "ConversationIconDrawable has null badge icon");
+            }
+            if (mShowRing) {
+                mRingPaint.setStrokeWidth(ringStrokeWidth);
+                final float radius = badgeSize * 0.5f + ringStrokeWidth * 0.5f; // stroke outside
+                final float cx = bounds.right - badgeSize * 0.5f - ringStrokeWidth;
+                final float cy = bounds.bottom - badgeSize * 0.5f - ringStrokeWidth;
+                canvas.drawCircle(cx, cy, radius, mRingPaint);
+            }
+        }
+
+        @Override
+        public void setAlpha(int alpha) {
+            // unimplemented
+        }
+
+        @Override
+        public void setColorFilter(ColorFilter colorFilter) {
+            // unimplemented
+        }
+
+        @Override
+        public int getOpacity() {
+            return 0;
+        }
     }
 }
diff --git a/packages/SystemUI/res-keyguard/drawable/bubble_manage_user_education_bg.xml b/packages/SystemUI/res-keyguard/drawable/bubble_manage_user_education_bg.xml
new file mode 100644
index 0000000..64db25b
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/drawable/bubble_manage_user_education_bg.xml
@@ -0,0 +1,21 @@
+<!--
+  ~ Copyright (C) 2020 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.
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+    <solid android:color="?android:attr/colorAccent"/>
+    <corners
+        android:radius="?android:attr/dialogCornerRadius" />
+</shape>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/bubble_stack_user_education_bg.xml b/packages/SystemUI/res/drawable/bubble_stack_user_education_bg.xml
new file mode 100644
index 0000000..4b9219c
--- /dev/null
+++ b/packages/SystemUI/res/drawable/bubble_stack_user_education_bg.xml
@@ -0,0 +1,22 @@
+<!--
+  ~ Copyright (C) 2020 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.
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+    <solid android:color="?android:attr/colorAccent"/>
+    <corners
+        android:bottomRightRadius="360dp"
+        android:topRightRadius="360dp" />
+</shape>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/bubble_stack_user_education.xml b/packages/SystemUI/res/layout/bubble_stack_user_education.xml
new file mode 100644
index 0000000..81b28e6
--- /dev/null
+++ b/packages/SystemUI/res/layout/bubble_stack_user_education.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 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.
+  -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/user_education_view"
+    android:layout_height="wrap_content"
+    android:layout_width="wrap_content"
+    android:paddingTop="48dp"
+    android:paddingBottom="48dp"
+    android:paddingStart="@dimen/bubble_stack_user_education_side_inset"
+    android:paddingEnd="16dp"
+    android:layout_marginEnd="24dp"
+    android:orientation="vertical"
+    android:background="@drawable/bubble_stack_user_education_bg">
+
+    <TextView
+        android:id="@+id/user_education_title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:paddingBottom="16dp"
+        android:fontFamily="@*android:string/config_bodyFontFamilyMedium"
+        android:maxLines="1"
+        android:text="@string/bubbles_user_education_title"
+        android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Headline"/>
+
+    <TextView
+        android:id="@+id/user_education_description"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/bubbles_user_education_description"
+        android:fontFamily="@*android:string/config_bodyFontFamily"
+        android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Body2"/>
+
+</LinearLayout>
diff --git a/packages/SystemUI/res/layout/bubbles_manage_button_education.xml b/packages/SystemUI/res/layout/bubbles_manage_button_education.xml
new file mode 100644
index 0000000..0cabc32
--- /dev/null
+++ b/packages/SystemUI/res/layout/bubbles_manage_button_education.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 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.
+  -->
+<com.android.systemui.bubbles.BubbleManageEducationView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <LinearLayout
+        android:id="@+id/manage_education_view"
+        android:orientation="vertical"
+        android:layout_height="wrap_content"
+        android:layout_width="@dimen/bubbles_manage_education_width">
+
+        <TextView
+            android:id="@+id/user_education_description"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:padding="8dp"
+            android:text="@string/bubbles_user_education_manage"
+            android:fontFamily="@*android:string/config_bodyFontFamily"
+            android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Body2"
+            android:background="@drawable/bubble_manage_user_education_bg"
+        />
+
+        <View
+            android:id="@+id/user_education_pointer"
+            android:layout_width="@dimen/bubble_pointer_width"
+            android:layout_height="@dimen/bubble_pointer_height"
+        />
+
+    </LinearLayout>
+</com.android.systemui.bubbles.BubbleManageEducationView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 12b9254..aefe4a2 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1159,7 +1159,7 @@
     <!-- Extra padding around the dismiss target for bubbles -->
     <dimen name="bubble_dismiss_slop">16dp</dimen>
     <!-- Height of button allowing users to adjust settings for bubbles. -->
-    <dimen name="bubble_settings_size">48dp</dimen>
+    <dimen name="bubble_manage_button_height">48dp</dimen>
     <!-- How far, horizontally, to animate the expanded view over when animating in/out. -->
     <dimen name="bubble_expanded_animate_x_distance">100dp</dimen>
     <!-- How far, vertically, to animate the expanded view over when animating in/out. -->
@@ -1175,16 +1175,22 @@
     <!-- How far offscreen the bubble stack rests. Cuts off padding and part of icon bitmap. -->
     <dimen name="bubble_stack_offscreen">9dp</dimen>
     <!-- How far down the screen the stack starts. -->
-    <dimen name="bubble_stack_starting_offset_y">96dp</dimen>
+    <dimen name="bubble_stack_starting_offset_y">120dp</dimen>
     <!-- Space between the pointer triangle and the bubble expanded view -->
     <dimen name="bubble_pointer_margin">8dp</dimen>
-    <!-- Height of the permission prompt shown with bubbles -->
-    <dimen name="bubble_permission_height">120dp</dimen>
     <!-- Padding applied to the bubble dismiss target. Touches in this padding cause the bubbles to
          snap to the dismiss target. -->
     <dimen name="bubble_dismiss_target_padding_x">40dp</dimen>
     <dimen name="bubble_dismiss_target_padding_y">20dp</dimen>
 
+    <!-- Bubbles user education views -->
+    <dimen name="bubbles_manage_education_width">160dp</dimen>
+    <!-- The inset from the top bound of the manage button to place the user education. -->
+    <dimen name="bubbles_manage_education_top_inset">10dp</dimen>
+    <!-- Size of padding for the user education cling, this should at minimum be larger than
+        individual_bubble_size + some padding. -->
+    <dimen name="bubble_stack_user_education_side_inset">72dp</dimen>
+
     <!-- Size of the RAT type for CellularTile -->
     <dimen name="celltile_rat_type_size">10sp</dimen>
 
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index ff28b4d..496ab43 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2586,6 +2586,12 @@
     <string name="bubble_accessibility_action_move_bottom_right">Move bottom right</string>
     <!-- Text used for the bubble dismiss area. Bubbles dragged to, or flung towards, this area will go away. [CHAR LIMIT=20] -->
     <string name="bubble_dismiss_text">Dismiss</string>
+    <!-- Title text for the bubbles feature education cling shown when a bubble is on screen for the first time. [CHAR LIMIT=60]-->
+    <string name="bubbles_user_education_title">Keep chats up front</string>
+    <!-- Descriptive text for the bubble feature education cling shown when a bubble is on screen for the first time. [CHAR LIMIT=NONE] -->
+    <string name="bubbles_user_education_description">New chats from <xliff:g id="app_name" example="YouTube">%1$s</xliff:g> will appear as bubbles. Tap a bubble to open it. Drag to move it.\n\nTap the bubble</string>
+    <!-- Text for the bubble "manage" button feature education cling shown when a bubble is on screen for the first time. [CHAR LIMIT=80]-->
+    <string name="bubbles_user_education_manage">Tap Manage to turn off bubbles from this app</string>
 
     <!-- Notification content text when the system navigation mode changes as a result of changing the default launcher [CHAR LIMIT=NONE] -->
     <string name="notification_content_system_nav_changed">System navigation updated. To make changes, go to Settings.</string>
diff --git a/packages/SystemUI/src/com/android/systemui/Prefs.java b/packages/SystemUI/src/com/android/systemui/Prefs.java
index f0a82c5..5e6589f 100644
--- a/packages/SystemUI/src/com/android/systemui/Prefs.java
+++ b/packages/SystemUI/src/com/android/systemui/Prefs.java
@@ -57,7 +57,9 @@
             Key.SEEN_RINGER_GUIDANCE_COUNT,
             Key.QS_HAS_TURNED_OFF_MOBILE_DATA,
             Key.TOUCHED_RINGER_TOGGLE,
-            Key.HAS_SEEN_ODI_CAPTIONS_TOOLTIP
+            Key.HAS_SEEN_ODI_CAPTIONS_TOOLTIP,
+            Key.HAS_SEEN_BUBBLES_EDUCATION,
+            Key.HAS_SEEN_BUBBLES_MANAGE_EDUCATION
     })
     public @interface Key {
         @Deprecated
@@ -103,6 +105,8 @@
         String QS_HAS_TURNED_OFF_MOBILE_DATA = "QsHasTurnedOffMobileData";
         String TOUCHED_RINGER_TOGGLE = "TouchedRingerToggle";
         String HAS_SEEN_ODI_CAPTIONS_TOOLTIP = "HasSeenODICaptionsTooltip";
+        String HAS_SEEN_BUBBLES_EDUCATION = "HasSeenBubblesOnboarding";
+        String HAS_SEEN_BUBBLES_MANAGE_EDUCATION = "HasSeenBubblesManageOnboarding";
     }
 
     public static boolean getBoolean(Context context, @Key String key, boolean defaultValue) {
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDebugConfig.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDebugConfig.java
index 3190662..e800011 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDebugConfig.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDebugConfig.java
@@ -16,6 +16,9 @@
 
 package com.android.systemui.bubbles;
 
+import android.content.Context;
+import android.provider.Settings;
+
 import java.util.List;
 
 /**
@@ -41,6 +44,20 @@
     static final boolean DEBUG_BUBBLE_EXPANDED_VIEW = false;
     static final boolean DEBUG_EXPERIMENTS = true;
     static final boolean DEBUG_OVERFLOW = false;
+    static final boolean DEBUG_USER_EDUCATION = false;
+
+    private static final boolean FORCE_SHOW_USER_EDUCATION = false;
+    private static final String FORCE_SHOW_USER_EDUCATION_SETTING =
+            "force_show_bubbles_user_education";
+
+    /**
+     * @return whether we should force show user education for bubbles. Used for debugging & demos.
+     */
+    static boolean forceShowUserEducation(Context context) {
+        boolean forceShow = Settings.Secure.getInt(context.getContentResolver(),
+                FORCE_SHOW_USER_EDUCATION_SETTING, 0) != 0;
+        return FORCE_SHOW_USER_EDUCATION || forceShow;
+    }
 
     static String formatBubblesString(List<Bubble> bubbles, Bubble selected) {
         StringBuilder sb = new StringBuilder();
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
index e3983c5..a6f759f 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
@@ -243,7 +243,7 @@
         mPointerView.setVisibility(INVISIBLE);
 
         mSettingsIconHeight = getContext().getResources().getDimensionPixelSize(
-                R.dimen.bubble_settings_size);
+                R.dimen.bubble_manage_button_height);
         mSettingsIcon = findViewById(R.id.settings_button);
         mSettingsIcon.setOnClickListener(this);
 
@@ -531,6 +531,16 @@
     }
 
     /**
+     * Position of the manage button displayed in the expanded view. Used for placing user
+     * education about the manage button.
+     */
+    public Rect getManageButtonLocationOnScreen() {
+        mTempLoc = mSettingsIcon.getLocationOnScreen();
+        return new Rect(mTempLoc[0], mTempLoc[1], mTempLoc[0] + mSettingsIcon.getWidth(),
+                mTempLoc[1] + mSettingsIcon.getHeight());
+    }
+
+    /**
      * Removes and releases an ActivityView if one was previously created for this bubble.
      */
     public void cleanUpExpandedState() {
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleManageEducationView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleManageEducationView.java
new file mode 100644
index 0000000..f4d6432
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleManageEducationView.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2020 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.bubbles;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.drawable.ShapeDrawable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.internal.util.ContrastColorUtil;
+import com.android.systemui.R;
+import com.android.systemui.recents.TriangleShape;
+
+/**
+ * Educational view to highlight the manage button that allows a user to configure the settings
+ * for the bubble. Shown only the first time a user expands a bubble.
+ */
+public class BubbleManageEducationView extends LinearLayout {
+
+    private View mPointerView;
+    private View mManageView;
+
+    public BubbleManageEducationView(Context context) {
+        this(context, null);
+    }
+
+    public BubbleManageEducationView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public BubbleManageEducationView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public BubbleManageEducationView(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+
+        mManageView = findViewById(R.id.manage_education_view);
+
+        final TypedArray ta = mContext.obtainStyledAttributes(
+                new int[] {android.R.attr.colorAccent,
+                        android.R.attr.textColorPrimaryInverse});
+        final int bgColor = ta.getColor(0, Color.BLACK);
+        int textColor = ta.getColor(1, Color.WHITE);
+        ta.recycle();
+
+        textColor = ContrastColorUtil.ensureTextContrast(textColor, bgColor, true);
+        ((TextView) findViewById(R.id.user_education_description)).setTextColor(textColor);
+
+        final Resources res = getResources();
+        final int pointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width);
+        final int pointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
+
+        ShapeDrawable triangleShape =
+                new ShapeDrawable(TriangleShape.create(
+                        pointerWidth, pointerHeight, false /* isPointingUp */));
+        triangleShape.setTint(bgColor);
+
+        mPointerView = findViewById(R.id.user_education_pointer);
+        mPointerView.setBackground(triangleShape);
+    }
+
+    /**
+     * Specifies the x value this pointer should point to.
+     */
+    public void setPointerPosition(int x) {
+        mPointerView.setTranslationX(x - (mPointerView.getWidth() / 2));
+    }
+
+    /**
+     * Specifies the position for the manage view.
+     */
+    public void setManageViewPosition(int x, int y) {
+        mManageView.setTranslationX(x);
+        mManageView.setTranslationY(y);
+    }
+
+    /**
+     * @return the height of the view that shows the educational text and pointer.
+     */
+    public int getManageViewHeight() {
+        return mManageView.getHeight();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflow.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflow.java
index a0e7449..313bb42 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflow.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflow.java
@@ -62,11 +62,12 @@
                 R.dimen.bubble_overflow_icon_bitmap_size);
     }
 
-    public void setUpOverflow(ViewGroup parentViewGroup) {
+    void setUpOverflow(ViewGroup parentViewGroup, BubbleStackView stackView) {
         mOverflowExpandedView = (BubbleExpandedView) mInflater.inflate(
                 R.layout.bubble_expanded_view, parentViewGroup /* root */,
                 false /* attachToRoot */);
         mOverflowExpandedView.setOverflow(true);
+        mOverflowExpandedView.setStackView(stackView);
 
         updateIcon(mContext, parentViewGroup);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index df8e394..6647069 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -19,9 +19,13 @@
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
 
+import static com.android.systemui.Interpolators.FAST_OUT_SLOW_IN;
+import static com.android.systemui.Prefs.Key.HAS_SEEN_BUBBLES_EDUCATION;
+import static com.android.systemui.Prefs.Key.HAS_SEEN_BUBBLES_MANAGE_EDUCATION;
 import static com.android.systemui.bubbles.BadgedImageView.DOT_STATE_DEFAULT;
 import static com.android.systemui.bubbles.BadgedImageView.DOT_STATE_SUPPRESSED_FOR_FLYOUT;
 import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_STACK_VIEW;
+import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_USER_EDUCATION;
 import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES;
 import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
 
@@ -33,6 +37,8 @@
 import android.content.Context;
 import android.content.res.Configuration;
 import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Color;
 import android.graphics.ColorMatrix;
 import android.graphics.ColorMatrixColorFilter;
 import android.graphics.Paint;
@@ -57,6 +63,7 @@
 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
 import android.view.animation.AccelerateDecelerateInterpolator;
 import android.widget.FrameLayout;
+import android.widget.TextView;
 
 import androidx.annotation.MainThread;
 import androidx.annotation.Nullable;
@@ -66,7 +73,9 @@
 import androidx.dynamicanimation.animation.SpringForce;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ContrastColorUtil;
 import com.android.internal.widget.ViewClippingUtil;
+import com.android.systemui.Prefs;
 import com.android.systemui.R;
 import com.android.systemui.bubbles.animation.ExpandedAnimationController;
 import com.android.systemui.bubbles.animation.PhysicsAnimationLayout;
@@ -88,6 +97,10 @@
 public class BubbleStackView extends FrameLayout {
     private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleStackView" : TAG_BUBBLES;
 
+    /** Animation durations for bubble stack user education views. **/
+    private static final int ANIMATE_STACK_USER_EDUCATION_DURATION = 200;
+    private static final int ANIMATE_STACK_USER_EDUCATION_DURATION_SHORT = 40;
+
     /** How far the flyout needs to be dragged before it's dismissed regardless of velocity. */
     static final float FLYOUT_DRAG_PERCENT_DISMISS = 0.25f;
 
@@ -171,6 +184,12 @@
      * previous one animates out.
      */
     private Runnable mAfterFlyoutHidden;
+    /**
+     * Set when the flyout is tapped, so that we can expand the bubble associated with the flyout
+     * once it collapses.
+     */
+    @Nullable
+    private Bubble mBubbleToExpandAfterFlyoutCollapse = null;
 
     /** Layout change listener that moves the stack to the nearest valid position on rotation. */
     private OnLayoutChangeListener mOrientationChangedListener;
@@ -319,6 +338,14 @@
 
     private BubbleOverflow mBubbleOverflow;
 
+    private boolean mShouldShowUserEducation;
+    private boolean mAnimatingEducationAway;
+    private View mUserEducationView;
+
+    private boolean mShouldShowManageEducation;
+    private BubbleManageEducationView mManageEducationView;
+    private boolean mAnimatingManageEducationAway;
+
     public BubbleStackView(Context context, BubbleData data,
             @Nullable SurfaceSynchronizer synchronizer,
             FloatingContentCoordinator floatingContentCoordinator) {
@@ -361,6 +388,8 @@
                 mDisplaySize, mExpandedViewPadding, res.getConfiguration().orientation);
         mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER;
 
+        setUpUserEducation();
+
         mBubbleContainer = new PhysicsAnimationLayout(context);
         mBubbleContainer.setActiveController(mStackAnimationController);
         mBubbleContainer.setElevation(elevation);
@@ -500,10 +529,50 @@
         });
     }
 
-    void showExpandedViewContents(int displayId) {
-        if (mExpandedBubble != null
-                && mExpandedBubble.getExpandedView().getVirtualDisplayId() == displayId) {
-            mExpandedBubble.setContentVisibility(true);
+    private void setUpUserEducation() {
+        if (mUserEducationView != null) {
+            removeView(mUserEducationView);
+        }
+        mShouldShowUserEducation = shouldShowBubblesEducation();
+        if (DEBUG_USER_EDUCATION) {
+            Log.d(TAG, "shouldShowUserEducation: " + mShouldShowUserEducation);
+        }
+        if (mShouldShowUserEducation) {
+            mUserEducationView = mInflater.inflate(R.layout.bubble_stack_user_education, this,
+                    false /* attachToRoot */);
+            mUserEducationView.setVisibility(GONE);
+
+            final TypedArray ta = mContext.obtainStyledAttributes(
+                    new int[] {android.R.attr.colorAccent,
+                            android.R.attr.textColorPrimaryInverse});
+            final int bgColor = ta.getColor(0, Color.BLACK);
+            int textColor = ta.getColor(1, Color.WHITE);
+            ta.recycle();
+            textColor = ContrastColorUtil.ensureTextContrast(textColor, bgColor, true);
+
+            TextView title = mUserEducationView.findViewById(R.id.user_education_title);
+            TextView description = mUserEducationView.findViewById(R.id.user_education_description);
+            title.setTextColor(textColor);
+            description.setTextColor(textColor);
+
+            addView(mUserEducationView);
+        }
+
+        if (mManageEducationView != null) {
+            removeView(mManageEducationView);
+        }
+        mShouldShowManageEducation = shouldShowManageEducation();
+        if (DEBUG_USER_EDUCATION) {
+            Log.d(TAG, "shouldShowManageEducation: " + mShouldShowManageEducation);
+        }
+        if (mShouldShowManageEducation) {
+            mManageEducationView = (BubbleManageEducationView)
+                    mInflater.inflate(R.layout.bubbles_manage_button_education, this,
+                            false /* attachToRoot */);
+            mManageEducationView.setVisibility(GONE);
+            mManageEducationView.setElevation(mBubbleElevation);
+
+            addView(mManageEducationView);
         }
     }
 
@@ -522,8 +591,8 @@
     private void setUpOverflow() {
         int overflowBtnIndex = 0;
         if (mBubbleOverflow == null) {
-            mBubbleOverflow = new BubbleOverflow(mContext);
-            mBubbleOverflow.setUpOverflow(this);
+            mBubbleOverflow = new BubbleOverflow(getContext());
+            mBubbleOverflow.setUpOverflow(mBubbleContainer, this);
         } else {
             mBubbleContainer.removeView(mBubbleOverflow.getBtn());
             mBubbleOverflow.updateIcon(mContext, this);
@@ -539,6 +608,7 @@
     public void onThemeChanged() {
         setUpFlyout();
         setUpOverflow();
+        setUpUserEducation();
     }
 
     /** Respond to the phone being rotated by repositioning the stack and hiding any flyouts. */
@@ -731,7 +801,7 @@
     Bubble getExpandedBubble() {
         if (mExpandedBubble == null
                 || (mExpandedBubble.getIconView() == mBubbleOverflow.getBtn()
-                    && mExpandedBubble.getKey() == BubbleOverflow.KEY)) {
+                    && BubbleOverflow.KEY.equals(mExpandedBubble.getKey()))) {
             return null;
         }
         return (Bubble) mExpandedBubble;
@@ -743,6 +813,12 @@
             Log.d(TAG, "addBubble: " + bubble);
         }
 
+        if (getBubbleCount() == 0 && mShouldShowUserEducation) {
+            // Override the default stack position if we're showing user education.
+            mStackAnimationController.setStackPosition(
+                    mStackAnimationController.getDefaultStartPosition());
+        }
+
         if (getBubbleCount() == 0) {
             mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide();
         }
@@ -881,6 +957,109 @@
     }
 
     /**
+     * If necessary, shows the user education view for the bubble stack. This appears the first
+     * time a user taps on a bubble.
+     *
+     * @return true if user education was shown, false otherwise.
+     */
+    private boolean maybeShowStackUserEducation() {
+        if (mShouldShowUserEducation && mUserEducationView.getVisibility() != VISIBLE) {
+            Bubble b = mBubbleData.getSelectedBubble();
+            TextView description = mUserEducationView.findViewById(R.id.user_education_description);
+            description.setText(mContext.getString(
+                    R.string.bubbles_user_education_description, b.getAppName()));
+
+            mUserEducationView.setAlpha(0);
+            mUserEducationView.setVisibility(VISIBLE);
+            // Post so we have height of mUserEducationView
+            mUserEducationView.post(() -> {
+                final int viewHeight = mUserEducationView.getHeight();
+                PointF stackPosition = mStackAnimationController.getDefaultStartPosition();
+                final float translationY = stackPosition.y + (mBubbleSize / 2) - (viewHeight / 2);
+                mUserEducationView.setTranslationY(translationY);
+                mUserEducationView.animate()
+                        .setDuration(ANIMATE_STACK_USER_EDUCATION_DURATION)
+                        .setInterpolator(FAST_OUT_SLOW_IN)
+                        .alpha(1);
+            });
+            Prefs.putBoolean(getContext(), HAS_SEEN_BUBBLES_EDUCATION, true);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * If necessary, hides the user education view for the bubble stack.
+     *
+     * @param fromExpansion if true this indicates the hide is happening due to the bubble being
+     *                      expanded, false if due to a touch outside of the bubble stack.
+     */
+    void hideStackUserEducation(boolean fromExpansion) {
+        if (mShouldShowUserEducation
+                && mUserEducationView.getVisibility() == VISIBLE
+                && !mAnimatingEducationAway) {
+            mAnimatingEducationAway = true;
+            mUserEducationView.animate()
+                    .alpha(0)
+                    .setDuration(fromExpansion
+                            ? ANIMATE_STACK_USER_EDUCATION_DURATION_SHORT
+                            : ANIMATE_STACK_USER_EDUCATION_DURATION)
+                    .withEndAction(() -> {
+                        mAnimatingEducationAway = false;
+                        mShouldShowUserEducation = shouldShowBubblesEducation();
+                        mUserEducationView.setVisibility(GONE);
+                    });
+        }
+    }
+
+    /**
+     * If necessary, toggles the user education view for the manage button. This is shown when the
+     * bubble stack is expanded for the first time.
+     *
+     * @param show whether the user education view should show or not.
+     */
+    void maybeShowManageEducation(boolean show) {
+        if (mManageEducationView == null) {
+            return;
+        }
+        if (show
+                && mShouldShowManageEducation
+                && mManageEducationView.getVisibility() != VISIBLE
+                && mIsExpanded) {
+            mManageEducationView.setAlpha(0);
+            mManageEducationView.setVisibility(VISIBLE);
+            mManageEducationView.post(() -> {
+                final Rect position =
+                        mExpandedBubble.getExpandedView().getManageButtonLocationOnScreen();
+                final int viewHeight = mManageEducationView.getManageViewHeight();
+                final int inset = getResources().getDimensionPixelSize(
+                        R.dimen.bubbles_manage_education_top_inset);
+                mManageEducationView.bringToFront();
+                mManageEducationView.setManageViewPosition(position.left,
+                        position.top - viewHeight + inset);
+                mManageEducationView.setPointerPosition(position.centerX() - position.left);
+                mManageEducationView.animate()
+                        .setDuration(ANIMATE_STACK_USER_EDUCATION_DURATION)
+                        .setInterpolator(FAST_OUT_SLOW_IN).alpha(1);
+            });
+            Prefs.putBoolean(getContext(), HAS_SEEN_BUBBLES_MANAGE_EDUCATION, true);
+        } else if (!show
+                && mManageEducationView.getVisibility() == VISIBLE
+                && !mAnimatingManageEducationAway) {
+            mManageEducationView.animate()
+                    .alpha(0)
+                    .setDuration(mIsExpansionAnimating
+                            ? ANIMATE_STACK_USER_EDUCATION_DURATION_SHORT
+                            : ANIMATE_STACK_USER_EDUCATION_DURATION)
+                    .withEndAction(() -> {
+                        mAnimatingManageEducationAway = false;
+                        mShouldShowManageEducation = shouldShowManageEducation();
+                        mManageEducationView.setVisibility(GONE);
+                    });
+        }
+    }
+
+    /**
      * Dismiss the stack of bubbles.
      *
      * @deprecated
@@ -923,7 +1102,17 @@
             return null;
         } else if (mFlyout.getVisibility() == VISIBLE && isIntersecting(mFlyout, x, y)) {
             return mFlyout;
+        } else if (mUserEducationView != null && mUserEducationView.getVisibility() == VISIBLE) {
+            View bubbleChild = mBubbleContainer.getChildAt(0);
+            if (isIntersecting(bubbleChild, x, y)) {
+                return this;
+            } else if (isIntersecting(mUserEducationView, x, y)) {
+                return mUserEducationView;
+            } else {
+                return null;
+            }
         }
+
         // If it wasn't an individual bubble in the expanded state, or the flyout, it's the stack.
         return this;
     }
@@ -933,22 +1122,6 @@
     }
 
     /**
-     * Collapses the stack of bubbles.
-     * <p>
-     * Must be called from the main thread.
-     *
-     * @deprecated use {@link #setExpanded(boolean)} and {@link #setSelectedBubble(Bubble)}
-     */
-    @Deprecated
-    @MainThread
-    void collapseStack() {
-        if (DEBUG_BUBBLE_STACK_VIEW) {
-            Log.d(TAG, "collapseStack()");
-        }
-        mBubbleData.setExpanded(false);
-    }
-
-    /**
      * @deprecated use {@link #setExpanded(boolean)} and {@link #setSelectedBubble(Bubble)}
      */
     @Deprecated
@@ -957,25 +1130,16 @@
         if (DEBUG_BUBBLE_STACK_VIEW) {
             Log.d(TAG, "collapseStack(endRunnable)");
         }
-        collapseStack();
+        mBubbleData.setExpanded(false);
         // TODO - use the runnable at end of animation
         endRunnable.run();
     }
 
-    /**
-     * Expands the stack of bubbles.
-     * <p>
-     * Must be called from the main thread.
-     *
-     * @deprecated use {@link #setExpanded(boolean)} and {@link #setSelectedBubble(Bubble)}
-     */
-    @Deprecated
-    @MainThread
-    void expandStack() {
-        if (DEBUG_BUBBLE_STACK_VIEW) {
-            Log.d(TAG, "expandStack()");
+    void showExpandedViewContents(int displayId) {
+        if (mExpandedBubble != null
+                && mExpandedBubble.getExpandedView().getVirtualDisplayId() == displayId) {
+            mExpandedBubble.setContentVisibility(true);
         }
-        mBubbleData.setExpanded(true);
     }
 
     private void beforeExpandedViewAnimation() {
@@ -995,11 +1159,12 @@
         mIsExpanded = false;
         final BubbleViewProvider previouslySelected = mExpandedBubble;
         beforeExpandedViewAnimation();
+        maybeShowManageEducation(false);
 
         if (DEBUG_BUBBLE_STACK_VIEW) {
             Log.d(TAG, "animateCollapse");
-            Log.d(TAG, BubbleDebugConfig.formatBubblesString(this.getBubblesOnScreen(),
-                    this.getExpandedBubble()));
+            Log.d(TAG, BubbleDebugConfig.formatBubblesString(getBubblesOnScreen(),
+                    getExpandedBubble()));
         }
         updateOverflowBtnVisibility(/* apply */ false);
         mBubbleContainer.cancelAllAnimations();
@@ -1021,6 +1186,7 @@
 
     private void animateExpansion() {
         mIsExpanded = true;
+        hideStackUserEducation(true /* fromExpansion */);
         beforeExpandedViewAnimation();
 
         mBubbleContainer.setActiveController(mExpandedAnimationController);
@@ -1028,6 +1194,7 @@
         mExpandedAnimationController.expandFromStack(() -> {
             updatePointerPosition();
             afterExpandedViewAnimation();
+            maybeShowManageEducation(true);
         } /* after */);
 
         mExpandedViewContainer.setTranslationX(getCollapsedX());
@@ -1074,11 +1241,19 @@
         }
     }
 
+    /** Called when the collapsed stack is tapped on. */
+    void onStackTapped() {
+        if (!maybeShowStackUserEducation()) {
+            mBubbleData.setExpanded(true);
+        }
+    }
+
     /** Called when a drag operation on an individual bubble has started. */
     public void onBubbleDragStart(View bubble) {
         if (DEBUG_BUBBLE_STACK_VIEW) {
             Log.d(TAG, "onBubbleDragStart: bubble=" + bubble);
         }
+        maybeShowManageEducation(false);
         mExpandedAnimationController.prepareForBubbleDrag(bubble);
     }
 
@@ -1129,6 +1304,7 @@
             return;
         }
 
+        hideStackUserEducation(false /* fromExpansion */);
         springInDismissTarget();
         mStackAnimationController.moveStackFromTouch(x, y);
     }
@@ -1191,14 +1367,13 @@
         mFlyout.setTranslationX(mFlyout.getRestingTranslationX() + overscrollTranslation);
     }
 
-    /**
-     * Set when the flyout is tapped, so that we can expand the bubble associated with the flyout
-     * once it collapses.
-     */
-    @Nullable private Bubble mBubbleToExpandAfterFlyoutCollapse = null;
-
     void onFlyoutTapped() {
-        mBubbleToExpandAfterFlyoutCollapse = mBubbleData.getSelectedBubble();
+        if (maybeShowStackUserEducation()) {
+            // If we're showing user education, don't open the bubble show the education first
+            mBubbleToExpandAfterFlyoutCollapse = null;
+        } else {
+            mBubbleToExpandAfterFlyoutCollapse = mBubbleData.getSelectedBubble();
+        }
 
         mFlyout.removeCallbacks(mHideFlyout);
         mHideFlyout.run();
@@ -1221,6 +1396,8 @@
 
         mFlyout.removeCallbacks(mHideFlyout);
         animateFlyoutCollapsed(shouldDismiss, velX);
+
+        maybeShowStackUserEducation();
     }
 
     /**
@@ -1432,6 +1609,7 @@
         if (flyoutMessage == null
                 || flyoutMessage.message == null
                 || !bubble.showFlyout()
+                || (mUserEducationView != null && mUserEducationView.getVisibility() == VISIBLE)
                 || isExpanded()
                 || mIsExpansionAnimating
                 || mIsGestureInProgress
@@ -1517,7 +1695,12 @@
 
     @Override
     public void getBoundsOnScreen(Rect outRect) {
-        // If the bubble menu is open, the entire screen should capture touch events.
+        if (mUserEducationView != null && mUserEducationView.getVisibility() == VISIBLE) {
+            // When user education shows then capture all touches
+            outRect.set(0, 0, getWidth(), getHeight());
+            return;
+        }
+
         if (!mIsExpanded) {
             if (getBubbleCount() > 0) {
                 mBubbleContainer.getChildAt(0).getBoundsOnScreen(outRect);
@@ -1704,6 +1887,18 @@
         return mExpandedBubble.getExpandedView().performBackPressIfNeeded();
     }
 
+    /** Whether the educational view should appear for bubbles. **/
+    private boolean shouldShowBubblesEducation() {
+        return BubbleDebugConfig.forceShowUserEducation(getContext())
+                || !Prefs.getBoolean(getContext(), HAS_SEEN_BUBBLES_EDUCATION, false);
+    }
+
+    /** Whether the educational view should appear for the expanded view "manage" button. **/
+    private boolean shouldShowManageEducation() {
+        return BubbleDebugConfig.forceShowUserEducation(getContext())
+                || !Prefs.getBoolean(getContext(), HAS_SEEN_BUBBLES_MANAGE_EDUCATION, false);
+    }
+
     /** For debugging only */
     List<Bubble> getBubblesOnScreen() {
         List<Bubble> bubbles = new ArrayList<>();
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
index 5e3e747..46d1e0d 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
@@ -24,7 +24,6 @@
 import android.view.ViewConfiguration;
 
 import com.android.systemui.Dependency;
-import com.android.systemui.R;
 
 /**
  * Handles interpreting touches on a {@link BubbleStackView}. This includes expanding, collapsing,
@@ -92,6 +91,7 @@
         // anything, collapse the stack.
         if (action == MotionEvent.ACTION_OUTSIDE || mTouchedView == null) {
             mBubbleData.setExpanded(false);
+            mStack.hideStackUserEducation(false /* fromExpansion */);
             resetForNextGesture();
             return false;
         }
@@ -102,6 +102,7 @@
 
             // Not touching anything touchable, but we shouldn't collapse (e.g. touching edge
             // of expanded view).
+            mStack.maybeShowManageEducation(false);
             resetForNextGesture();
             return false;
         }
@@ -217,9 +218,8 @@
                     }
                 } else if (mTouchedView == mStack.getExpandedBubbleView()) {
                     mBubbleData.setExpanded(false);
-                } else if (isStack || isFlyout) {
-                    // Toggle expansion
-                    mBubbleData.setExpanded(!mBubbleData.isExpanded());
+                } else if (isStack) {
+                    mStack.onStackTapped();
                 } else {
                     final String key = ((BadgedImageView) mTouchedView).getKey();
                     if (key == BubbleOverflow.KEY) {
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
index 245d4af..f22c8fa 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
@@ -879,9 +879,10 @@
     }
 
     /** Moves the stack to a position instantly, with no animation. */
-    private void setStackPosition(PointF pos) {
+    public void setStackPosition(PointF pos) {
         Log.d(TAG, String.format("Setting position to (%f, %f).", pos.x, pos.y));
         mStackPosition.set(pos.x, pos.y);
+        mRestingStackPosition = mStackPosition;
 
         // If we're not the active controller, we don't want to physically move the bubble views.
         if (isActiveController()) {
@@ -902,10 +903,10 @@
         }
     }
 
-    /** Returns the default stack position, which is on the top right. */
-    private PointF getDefaultStartPosition() {
+    /** Returns the default stack position, which is on the top left. */
+    public PointF getDefaultStartPosition() {
         return new PointF(
-                getAllowableStackPositionRegion().right,
+                getAllowableStackPositionRegion().left,
                 getAllowableStackPositionRegion().top + mStackStartingVerticalOffset);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java
index bab7840..a5258fd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java
@@ -325,8 +325,9 @@
     private void bindIcon() {
         ImageView image = findViewById(R.id.conversation_icon);
         if (mShortcutInfo != null) {
-            image.setImageBitmap(mIconFactory.getConversationBitmap(
-                    mShortcutInfo, mPackageName, mAppUid));
+            image.setImageDrawable(mIconFactory.getConversationDrawable(
+                    mShortcutInfo, mPackageName, mAppUid,
+                    mNotificationChannel.isImportantConversation()));
         } else {
             if (mSbn.getNotification().extras.getBoolean(EXTRA_IS_GROUP_CONVERSATION, false)) {
                 // TODO: maybe use a generic group icon, or a composite of recent senders
@@ -480,6 +481,9 @@
                     mContext.getString(R.string.notification_conversation_mute));
             mute.setImageResource(R.drawable.ic_notifications_silence);
         }
+
+        // update icon in case importance has changed
+        bindIcon();
     }
 
     private void updateChannel() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
index c01f6c4..d746822 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
@@ -390,7 +390,7 @@
             };
         }
         ConversationIconFactory iconFactoryLoader = new ConversationIconFactory(mContext,
-                launcherApps, pmUser, IconDrawableFactory.newInstance(mContext),
+                launcherApps, pmUser, IconDrawableFactory.newInstance(mContext, false),
                 mContext.getResources().getDimensionPixelSize(
                         R.dimen.notification_guts_conversation_icon_size));
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java
index 9003de1..d2d76c7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.policy;
 
 import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.wifi.WifiManager.TrafficStateCallback.DATA_ACTIVITY_IN;
 import static android.net.wifi.WifiManager.TrafficStateCallback.DATA_ACTIVITY_INOUT;
 import static android.net.wifi.WifiManager.TrafficStateCallback.DATA_ACTIVITY_NONE;
@@ -157,6 +158,7 @@
     ServiceState mLastServiceState;
     private boolean mUserSetup;
     private boolean mSimDetected;
+    private boolean mForceCellularValidated;
 
     private ConfigurationController.ConfigurationListener mConfigurationListener =
             new ConfigurationController.ConfigurationListener() {
@@ -284,12 +286,41 @@
         mPhoneStateListener = new PhoneStateListener(mReceiverHandler::post) {
             @Override
             public void onActiveDataSubscriptionIdChanged(int subId) {
+                // For data switching from A to B, we assume B is validated for up to 2 seconds iff:
+                // 1) A and B are in the same subscription group e.g. CBRS data switch. And
+                // 2) A was validated before the switch.
+                // This is to provide smooth transition for UI without showing cross during data
+                // switch.
+                if (keepCellularValidationBitInSwitch(mActiveMobileDataSubscription, subId)) {
+                    if (DEBUG) Log.d(TAG, ": mForceCellularValidated to true.");
+                    mForceCellularValidated = true;
+                    mReceiverHandler.removeCallbacks(mClearForceValidated);
+                    mReceiverHandler.postDelayed(mClearForceValidated, 2000);
+                }
                 mActiveMobileDataSubscription = subId;
                 doUpdateMobileControllers();
             }
         };
     }
 
+    private final Runnable mClearForceValidated = () -> {
+        if (DEBUG) Log.d(TAG, ": mClearForceValidated");
+        mForceCellularValidated = false;
+        updateConnectivity();
+    };
+
+    boolean isInGroupDataSwitch(int subId1, int subId2) {
+        SubscriptionInfo info1 = mSubscriptionManager.getActiveSubscriptionInfo(subId1);
+        SubscriptionInfo info2 = mSubscriptionManager.getActiveSubscriptionInfo(subId2);
+        return (info1 != null && info2 != null && info1.getGroupUuid() != null
+            && info1.getGroupUuid().equals(info2.getGroupUuid()));
+    }
+
+    boolean keepCellularValidationBitInSwitch(int sourceSubId, int destSubId) {
+        return mValidatedTransports.get(TRANSPORT_CELLULAR)
+                && isInGroupDataSwitch(sourceSubId, destSubId);
+    }
+
     public DataSaverController getDataSaverController() {
         return mDataSaverController;
     }
@@ -793,6 +824,8 @@
             }
         }
 
+        if (mForceCellularValidated) mValidatedTransports.set(TRANSPORT_CELLULAR);
+
         if (CHATTY) {
             Log.d(TAG, "updateConnectivity: mConnectedTransports=" + mConnectedTransports);
             Log.d(TAG, "updateConnectivity: mValidatedTransports=" + mValidatedTransports);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java
index 138ea39..6c12c76 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java
@@ -30,6 +30,7 @@
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
 
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.anyString;
@@ -53,8 +54,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ShortcutInfo;
 import android.content.pm.ShortcutManager;
-import android.graphics.Bitmap;
-import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
 import android.os.UserHandle;
 import android.provider.Settings;
@@ -117,7 +117,7 @@
     @Mock
     private ShortcutInfo mShortcutInfo;
     @Mock
-    private Bitmap mImage;
+    private Drawable mIconDrawable;
 
     @Rule
     public MockitoRule mockito = MockitoJUnit.rule();
@@ -183,8 +183,9 @@
         when(mShortcutInfo.getShortLabel()).thenReturn("Convo name");
         List<ShortcutInfo> shortcuts = Arrays.asList(mShortcutInfo);
         when(mLauncherApps.getShortcuts(any(), any())).thenReturn(shortcuts);
-        when(mIconFactory.getConversationBitmap(any(ShortcutInfo.class), anyString(), anyInt()))
-                .thenReturn(mImage);
+        when(mIconFactory.getConversationDrawable(
+                any(ShortcutInfo.class), anyString(), anyInt(), anyBoolean()))
+                .thenReturn(mIconDrawable);
 
         mNotificationChannel = new NotificationChannel(
                 TEST_CHANNEL, TEST_CHANNEL_NAME, IMPORTANCE_LOW);
@@ -233,7 +234,7 @@
                 mIconFactory,
                 true);
         final ImageView view = mNotificationInfo.findViewById(R.id.conversation_icon);
-        assertEquals(mImage, ((BitmapDrawable) view.getDrawable()).getBitmap());
+        assertEquals(mIconDrawable, view.getDrawable());
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java
index ac40808..48a3b25 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java
@@ -77,6 +77,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -89,6 +90,7 @@
  * Functional tests for notification inflation from {@link NotificationEntryManager}.
  */
 @SmallTest
+@Ignore("Flaking")
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 public class NotificationEntryManagerInflationTest extends SysuiTestCase {
diff --git a/packages/Tethering/apex/Android.bp b/packages/Tethering/apex/Android.bp
index 94ef11c..96a4d20 100644
--- a/packages/Tethering/apex/Android.bp
+++ b/packages/Tethering/apex/Android.bp
@@ -16,6 +16,7 @@
 
 apex {
     name: "com.android.tethering",
+    updatable: true,
     java_libs: ["framework-tethering"],
     apps: ["Tethering"],
     manifest: "manifest.json",
diff --git a/packages/overlays/IconShapeFlowerOverlay/Android.mk b/packages/overlays/IconShapeFlowerOverlay/Android.mk
new file mode 100644
index 0000000..d410bb7
--- /dev/null
+++ b/packages/overlays/IconShapeFlowerOverlay/Android.mk
@@ -0,0 +1,29 @@
+#
+#  Copyright 2020, 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 := IconShapeFlower
+
+LOCAL_PRODUCT_MODULE := true
+
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+
+LOCAL_PACKAGE_NAME := IconShapeFlowerOverlay
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_RRO_PACKAGE)
diff --git a/packages/overlays/IconShapeFlowerOverlay/AndroidManifest.xml b/packages/overlays/IconShapeFlowerOverlay/AndroidManifest.xml
new file mode 100644
index 0000000..9d20c6b
--- /dev/null
+++ b/packages/overlays/IconShapeFlowerOverlay/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<!--
+  ~ Copyright (C) 2020 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.flower"
+          android:versionCode="1"
+          android:versionName="1.0">
+    <overlay
+        android:targetPackage="android"
+        android:category="android.theme.customization.adaptive_icon_shape"
+        android:priority="1"/>
+
+    <application android:label="@string/icon_shape_flower_overlay" android:hasCode="false"/>
+</manifest>
diff --git a/packages/overlays/IconShapeFlowerOverlay/res/values/config.xml b/packages/overlays/IconShapeFlowerOverlay/res/values/config.xml
new file mode 100644
index 0000000..73f4f21
--- /dev/null
+++ b/packages/overlays/IconShapeFlowerOverlay/res/values/config.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 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,0 C60.6,0 69.9,5.3 75.6,13.5 78.5,17.8 82.3,21.5 86.6,24.5 94.7,30.1 100,39.4 100,50 100,60.6 94.7,69.9 86.5,75.6 82.2,78.5 78.5,82.3 75.5,86.6 69.9,94.7 60.6,100 50,100 39.4,100 30.1,94.7 24.4,86.5 21.5,82.2 17.7,78.5 13.4,75.5 5.3,69.9 0,60.6 0,50 0,39.4 5.3,30.1 13.5,24.4 17.8,21.5 21.5,17.7 24.5,13.4 30.1,5.3 39.4,0 50,0 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/IconShapeFlowerOverlay/res/values/strings.xml b/packages/overlays/IconShapeFlowerOverlay/res/values/strings.xml
new file mode 100644
index 0000000..47c1479
--- /dev/null
+++ b/packages/overlays/IconShapeFlowerOverlay/res/values/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 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">
+    <!-- Flower icon overlay -->
+    <string name="icon_shape_flower_overlay" translatable="false">Flower</string>
+
+</resources>
diff --git a/packages/overlays/IconShapeHexagonOverlay/Android.mk b/packages/overlays/IconShapeHexagonOverlay/Android.mk
new file mode 100644
index 0000000..16ef399
--- /dev/null
+++ b/packages/overlays/IconShapeHexagonOverlay/Android.mk
@@ -0,0 +1,29 @@
+#
+#  Copyright 2020, 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 := IconShapeHexagon
+
+LOCAL_PRODUCT_MODULE := true
+
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+
+LOCAL_PACKAGE_NAME := IconShapeHexagonOverlay
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_RRO_PACKAGE)
diff --git a/packages/overlays/IconShapeHexagonOverlay/AndroidManifest.xml b/packages/overlays/IconShapeHexagonOverlay/AndroidManifest.xml
new file mode 100644
index 0000000..bf408fd
--- /dev/null
+++ b/packages/overlays/IconShapeHexagonOverlay/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<!--
+  ~ Copyright (C) 2020 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.hexagon"
+          android:versionCode="1"
+          android:versionName="1.0">
+    <overlay
+        android:targetPackage="android"
+        android:category="android.theme.customization.adaptive_icon_shape"
+        android:priority="1"/>
+
+    <application android:label="@string/icon_shape_hexagon_overlay" android:hasCode="false"/>
+</manifest>
diff --git a/packages/overlays/IconShapeHexagonOverlay/res/values/config.xml b/packages/overlays/IconShapeHexagonOverlay/res/values/config.xml
new file mode 100644
index 0000000..f7cb595
--- /dev/null
+++ b/packages/overlays/IconShapeHexagonOverlay/res/values/config.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 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">"M12,0 88,0 100,50 88,100 12,100 0,50 12,0 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">0dp</dimen>
+    <!-- Corner radius for bottom sheet system dialogs -->
+    <dimen name="config_bottomDialogCornerRadius">0dp</dimen>
+
+</resources>
diff --git a/packages/overlays/IconShapeHexagonOverlay/res/values/strings.xml b/packages/overlays/IconShapeHexagonOverlay/res/values/strings.xml
new file mode 100644
index 0000000..e00dc9d
--- /dev/null
+++ b/packages/overlays/IconShapeHexagonOverlay/res/values/strings.xml
@@ -0,0 +1,20 @@
+<!--
+  ~ Copyright (C) 2020 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">
+    <!-- Hexagon icon overlay -->
+    <string name="icon_shape_hexagon_overlay" translatable="false">Hexagon</string>
+
+</resources>
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index b7b52b1..5daa61d 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -24,6 +24,7 @@
 import static android.os.Process.ROOT_UID;
 import static android.os.Process.SHELL_UID;
 import static android.os.Process.SYSTEM_UID;
+import static android.os.Process.ZYGOTE_POLICY_FLAG_EMPTY;
 
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BACKGROUND_CHECK;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_FOREGROUND_SERVICE;
@@ -2937,8 +2938,10 @@
         // Not running -- get it started, and enqueue this service record
         // to be executed when the app comes up.
         if (app == null && !permissionsReviewRequired) {
+            // TODO (chriswailes): Change the Zygote policy flags based on if the launch-for-service
+            //  was initiated from a notification tap or not.
             if ((app=mAm.startProcessLocked(procName, r.appInfo, true, intentFlags,
-                    hostingRecord, false, isolated, false)) == null) {
+                    hostingRecord, ZYGOTE_POLICY_FLAG_EMPTY, false, isolated, false)) == null) {
                 String msg = "Unable to launch app "
                         + r.appInfo.packageName + "/"
                         + r.appInfo.uid + " for service "
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index b2fb530..06693360 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -65,6 +65,10 @@
 import static android.os.Process.SIGNAL_USR1;
 import static android.os.Process.SYSTEM_UID;
 import static android.os.Process.THREAD_PRIORITY_FOREGROUND;
+import static android.os.Process.ZYGOTE_POLICY_FLAG_BATCH_LAUNCH;
+import static android.os.Process.ZYGOTE_POLICY_FLAG_EMPTY;
+import static android.os.Process.ZYGOTE_POLICY_FLAG_LATENCY_SENSITIVE;
+import static android.os.Process.ZYGOTE_POLICY_FLAG_SYSTEM_PROCESS;
 import static android.os.Process.ZYGOTE_PROCESS;
 import static android.os.Process.getTotalMemory;
 import static android.os.Process.isThreadInProcess;
@@ -3054,7 +3058,7 @@
             info.targetSdkVersion = Build.VERSION.SDK_INT;
             ProcessRecord proc = mProcessList.startProcessLocked(processName, info /* info */,
                     false /* knownToBeDead */, 0 /* intentFlags */,
-                    sNullHostingRecord  /* hostingRecord */,
+                    sNullHostingRecord  /* hostingRecord */, ZYGOTE_POLICY_FLAG_EMPTY,
                     true /* allowWhileBooting */, true /* isolated */,
                     uid, true /* keepIfLarge */, abiOverride, entryPoint, entryPointArgs,
                     crashHandler);
@@ -3065,12 +3069,12 @@
     @GuardedBy("this")
     final ProcessRecord startProcessLocked(String processName,
             ApplicationInfo info, boolean knownToBeDead, int intentFlags,
-            HostingRecord hostingRecord, boolean allowWhileBooting,
+            HostingRecord hostingRecord, int zygotePolicyFlags, boolean allowWhileBooting,
             boolean isolated, boolean keepIfLarge) {
         return mProcessList.startProcessLocked(processName, info, knownToBeDead, intentFlags,
-                hostingRecord, allowWhileBooting, isolated, 0 /* isolatedUid */, keepIfLarge,
-                null /* ABI override */, null /* entryPoint */, null /* entryPointArgs */,
-                null /* crashHandler */);
+                hostingRecord, zygotePolicyFlags, allowWhileBooting, isolated, 0 /* isolatedUid */,
+                keepIfLarge, null /* ABI override */, null /* entryPoint */,
+                null /* entryPointArgs */, null /* crashHandler */);
     }
 
     boolean isAllowedWhileBooting(ApplicationInfo ai) {
@@ -4953,7 +4957,8 @@
         } catch (RemoteException e) {
             app.resetPackageList(mProcessStats);
             mProcessList.startProcessLocked(app,
-                    new HostingRecord("link fail", processName));
+                    new HostingRecord("link fail", processName),
+                    ZYGOTE_POLICY_FLAG_EMPTY);
             return false;
         }
 
@@ -5372,7 +5377,9 @@
                 for (int ip=0; ip<NP; ip++) {
                     if (DEBUG_PROCESSES) Slog.v(TAG_PROCESSES, "Starting process on hold: "
                             + procs.get(ip));
-                    mProcessList.startProcessLocked(procs.get(ip), new HostingRecord("on-hold"));
+                    mProcessList.startProcessLocked(procs.get(ip),
+                            new HostingRecord("on-hold"),
+                            ZYGOTE_POLICY_FLAG_BATCH_LAUNCH);
                 }
             }
             if (mFactoryTest == FactoryTest.FACTORY_TEST_LOW_LEVEL) {
@@ -7224,8 +7231,9 @@
                             proc = startProcessLocked(cpi.processName,
                                     cpr.appInfo, false, 0,
                                     new HostingRecord("content provider",
-                                    new ComponentName(cpi.applicationInfo.packageName,
-                                            cpi.name)), false, false, false);
+                                        new ComponentName(cpi.applicationInfo.packageName,
+                                                cpi.name)),
+                                    ZYGOTE_POLICY_FLAG_EMPTY, false, false, false);
                             checkTime(startTime, "getContentProviderImpl: after start process");
                             if (proc == null) {
                                 Slog.w(TAG, "Unable to launch app "
@@ -7259,10 +7267,8 @@
             }
             checkTime(startTime, "getContentProviderImpl: done!");
 
-            grantImplicitAccess(userId, null /*intent*/,
-                    UserHandle.getAppId(Binder.getCallingUid()),
-                    UserHandle.getAppId(cpi.applicationInfo.uid)
-            );
+            grantImplicitAccess(userId, null /*intent*/, callingUid,
+                    UserHandle.getAppId(cpi.applicationInfo.uid));
         }
 
         // Wait for the provider to be published...
@@ -7785,7 +7791,8 @@
                         .getPersistentApplications(STOCK_PM_FLAGS | matchFlags).getList();
                 for (ApplicationInfo app : apps) {
                     if (!"android".equals(app.packageName)) {
-                        addAppLocked(app, null, false, null /* ABI override */);
+                        addAppLocked(app, null, false, null /* ABI override */,
+                                ZYGOTE_POLICY_FLAG_BATCH_LAUNCH);
                     }
                 }
             } catch (RemoteException ex) {
@@ -8056,23 +8063,25 @@
 
     @GuardedBy("this")
     final ProcessRecord addAppLocked(ApplicationInfo info, String customProcess, boolean isolated,
-            String abiOverride) {
+            String abiOverride, int zygotePolicyFlags) {
         return addAppLocked(info, customProcess, isolated, false /* disableHiddenApiChecks */,
-                false /* mountExtStorageFull */, abiOverride);
+                false /* mountExtStorageFull */, abiOverride, zygotePolicyFlags);
     }
 
     @GuardedBy("this")
     final ProcessRecord addAppLocked(ApplicationInfo info, String customProcess, boolean isolated,
-            boolean disableHiddenApiChecks, boolean mountExtStorageFull, String abiOverride) {
+            boolean disableHiddenApiChecks, boolean mountExtStorageFull, String abiOverride,
+            int zygotePolicyFlags) {
         return addAppLocked(info, customProcess, isolated, disableHiddenApiChecks,
-                false /* disableTestApiChecks */, mountExtStorageFull, abiOverride);
+                false /* disableTestApiChecks */, mountExtStorageFull, abiOverride,
+                zygotePolicyFlags);
     }
 
     // TODO: Move to ProcessList?
     @GuardedBy("this")
     final ProcessRecord addAppLocked(ApplicationInfo info, String customProcess, boolean isolated,
             boolean disableHiddenApiChecks, boolean disableTestApiChecks,
-            boolean mountExtStorageFull, String abiOverride) {
+            boolean mountExtStorageFull, String abiOverride, int zygotePolicyFlags) {
         ProcessRecord app;
         if (!isolated) {
             app = getProcessRecordLocked(customProcess != null ? customProcess : info.processName,
@@ -8107,7 +8116,8 @@
             mPersistentStartingProcesses.add(app);
             mProcessList.startProcessLocked(app, new HostingRecord("added application",
                     customProcess != null ? customProcess : app.processName),
-                    disableHiddenApiChecks, disableTestApiChecks, mountExtStorageFull, abiOverride);
+                    zygotePolicyFlags, disableHiddenApiChecks, disableTestApiChecks,
+                    mountExtStorageFull, abiOverride);
         }
 
         return app;
@@ -14614,7 +14624,8 @@
             mProcessList.addProcessNameLocked(app);
             app.pendingStart = false;
             mProcessList.startProcessLocked(app,
-                    new HostingRecord("restart", app.processName));
+                    new HostingRecord("restart", app.processName),
+                    ZYGOTE_POLICY_FLAG_EMPTY);
             return true;
         } else if (app.pid > 0 && app.pid != MY_PID) {
             // Goodbye!
@@ -14977,7 +14988,7 @@
             ProcessRecord proc = startProcessLocked(app.processName, app,
                     false, 0,
                     new HostingRecord("backup", hostingName),
-                    false, false, false);
+                    ZYGOTE_POLICY_FLAG_SYSTEM_PROCESS, false, false, false);
             if (proc == null) {
                 Slog.e(TAG, "Unable to start backup agent process " + r);
                 return false;
@@ -16616,7 +16627,8 @@
             }
 
             ProcessRecord app = addAppLocked(ai, defProcess, false, disableHiddenApiChecks,
-                    disableTestApiChecks, mountExtStorageFull, abiOverride);
+                    disableTestApiChecks, mountExtStorageFull, abiOverride,
+                    ZYGOTE_POLICY_FLAG_EMPTY);
             app.setActiveInstrumentation(activeInstr);
             activeInstr.mFinished = false;
             activeInstr.mSourceUid = callingUid;
@@ -18013,7 +18025,8 @@
                 mProcessList.mRemovedProcesses.remove(i);
 
                 if (app.isPersistent()) {
-                    addAppLocked(app.info, null, false, null /* ABI override */);
+                    addAppLocked(app.info, null, false, null /* ABI override */,
+                            ZYGOTE_POLICY_FLAG_BATCH_LAUNCH);
                 }
             }
         }
@@ -19220,8 +19233,8 @@
                     // preempted by other processes before attaching the process of top app.
                     startProcessLocked(processName, info, knownToBeDead, 0 /* intentFlags */,
                             new HostingRecord(hostingType, hostingName, isTop),
-                            false /* allowWhileBooting */, false /* isolated */,
-                            true /* keepIfLarge */);
+                            ZYGOTE_POLICY_FLAG_LATENCY_SENSITIVE, false /* allowWhileBooting */,
+                            false /* isolated */, true /* keepIfLarge */);
                 }
             } finally {
                 Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
diff --git a/services/core/java/com/android/server/am/BroadcastQueue.java b/services/core/java/com/android/server/am/BroadcastQueue.java
index 26ef707..3aec53a 100644
--- a/services/core/java/com/android/server/am/BroadcastQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastQueue.java
@@ -16,6 +16,9 @@
 
 package com.android.server.am;
 
+import static android.os.Process.ZYGOTE_POLICY_FLAG_EMPTY;
+import static android.os.Process.ZYGOTE_POLICY_FLAG_LATENCY_SENSITIVE;
+
 import static com.android.server.am.ActivityManagerDebugConfig.*;
 
 import android.app.ActivityManager;
@@ -1593,7 +1596,9 @@
                     + receiverUid);
         }
 
-        if (brOptions != null && brOptions.getTemporaryAppWhitelistDuration() > 0) {
+        final boolean isActivityCapable =
+                (brOptions != null && brOptions.getTemporaryAppWhitelistDuration() > 0);
+        if (isActivityCapable) {
             scheduleTempWhitelistLocked(receiverUid,
                     brOptions.getTemporaryAppWhitelistDuration(), r);
         }
@@ -1648,6 +1653,7 @@
                 info.activityInfo.applicationInfo, true,
                 r.intent.getFlags() | Intent.FLAG_FROM_BACKGROUND,
                 new HostingRecord("broadcast", r.curComponent),
+                isActivityCapable ? ZYGOTE_POLICY_FLAG_LATENCY_SENSITIVE : ZYGOTE_POLICY_FLAG_EMPTY,
                 (r.intent.getFlags()&Intent.FLAG_RECEIVER_BOOT_UPGRADE) != 0, false, false))
                         == null) {
             // Ah, this recipient is unavailable.  Finish it if necessary,
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index 22559c4..f2bc1fe 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -25,6 +25,7 @@
 import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
 import static android.os.Process.SYSTEM_UID;
 import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
+import static android.os.Process.ZYGOTE_POLICY_FLAG_EMPTY;
 import static android.os.Process.getFreeMemory;
 import static android.os.Process.getTotalMemory;
 import static android.os.Process.killProcessQuiet;
@@ -80,13 +81,11 @@
 import android.os.DropBoxManager;
 import android.os.Handler;
 import android.os.IBinder;
-import android.os.IVold;
 import android.os.Looper;
 import android.os.Message;
 import android.os.PowerManager;
 import android.os.Process;
 import android.os.RemoteException;
-import android.os.ServiceManager;
 import android.os.StrictMode;
 import android.os.SystemClock;
 import android.os.SystemProperties;
@@ -1640,7 +1639,7 @@
      */
     @GuardedBy("mService")
     boolean startProcessLocked(ProcessRecord app, HostingRecord hostingRecord,
-            boolean disableHiddenApiChecks, boolean disableTestApiChecks,
+            int zygotePolicyFlags, boolean disableHiddenApiChecks, boolean disableTestApiChecks,
             boolean mountExtStorageFull, String abiOverride) {
         if (app.pendingStart) {
             return true;
@@ -1733,8 +1732,7 @@
             }
             // Run the app in safe mode if its manifest requests so or the
             // system is booted in safe mode.
-            if ((app.info.flags & ApplicationInfo.FLAG_VM_SAFE_MODE) != 0 ||
-                    mService.mSafeMode == true) {
+            if ((app.info.flags & ApplicationInfo.FLAG_VM_SAFE_MODE) != 0 || mService.mSafeMode) {
                 runtimeFlags |= Zygote.DEBUG_ENABLE_SAFEMODE;
             }
             if ((app.info.privateFlags & ApplicationInfo.PRIVATE_FLAG_PROFILEABLE_BY_SHELL) != 0) {
@@ -1846,8 +1844,8 @@
             final String entryPoint = "android.app.ActivityThread";
 
             return startProcessLocked(hostingRecord, entryPoint, app, uid, gids,
-                    runtimeFlags, mountExternal, seInfo, requiredAbi, instructionSet, invokeWith,
-                    startTime);
+                    runtimeFlags, zygotePolicyFlags, mountExternal, seInfo, requiredAbi,
+                    instructionSet, invokeWith, startTime);
         } catch (RuntimeException e) {
             Slog.e(ActivityManagerService.TAG, "Failure starting process " + app.processName, e);
 
@@ -1864,9 +1862,8 @@
     }
 
     @GuardedBy("mService")
-    boolean startProcessLocked(HostingRecord hostingRecord,
-            String entryPoint,
-            ProcessRecord app, int uid, int[] gids, int runtimeFlags, int mountExternal,
+    boolean startProcessLocked(HostingRecord hostingRecord, String entryPoint, ProcessRecord app,
+            int uid, int[] gids, int runtimeFlags, int zygotePolicyFlags, int mountExternal,
             String seInfo, String requiredAbi, String instructionSet, String invokeWith,
             long startTime) {
         app.pendingStart = true;
@@ -1895,15 +1892,15 @@
             if (DEBUG_PROCESSES) Slog.i(TAG_PROCESSES,
                     "Posting procStart msg for " + app.toShortString());
             mService.mProcStartHandler.post(() -> handleProcessStart(
-                    app, entryPoint, gids, runtimeFlags, mountExternal, requiredAbi,
-                    instructionSet, invokeWith, startSeq));
+                    app, entryPoint, gids, runtimeFlags, zygotePolicyFlags, mountExternal,
+                    requiredAbi, instructionSet, invokeWith, startSeq));
             return true;
         } else {
             try {
                 final Process.ProcessStartResult startResult = startProcess(hostingRecord,
                         entryPoint, app,
-                        uid, gids, runtimeFlags, mountExternal, seInfo, requiredAbi, instructionSet,
-                        invokeWith, startTime);
+                        uid, gids, runtimeFlags, zygotePolicyFlags, mountExternal, seInfo,
+                        requiredAbi, instructionSet, invokeWith, startTime);
                 handleProcessStartedLocked(app, startResult.pid, startResult.usingWrapper,
                         startSeq, false);
             } catch (RuntimeException e) {
@@ -1923,8 +1920,8 @@
      * <p>Note: this function doesn't hold the global AM lock intentionally.</p>
      */
     private void handleProcessStart(final ProcessRecord app, final String entryPoint,
-            final int[] gids, final int runtimeFlags, final int mountExternal,
-            final String requiredAbi, final String instructionSet,
+            final int[] gids, final int runtimeFlags, int zygotePolicyFlags,
+            final int mountExternal, final String requiredAbi, final String instructionSet,
             final String invokeWith, final long startSeq) {
         // If there is a precede instance of the process, wait for its death with a timeout.
         // Use local reference since we are not using locks here
@@ -1959,8 +1956,10 @@
         }
         try {
             final Process.ProcessStartResult startResult = startProcess(app.hostingRecord,
-                    entryPoint, app, app.startUid, gids, runtimeFlags, mountExternal,
-                    app.seInfo, requiredAbi, instructionSet, invokeWith, app.startTime);
+                    entryPoint, app, app.startUid, gids, runtimeFlags, zygotePolicyFlags,
+                    mountExternal, app.seInfo, requiredAbi, instructionSet, invokeWith,
+                    app.startTime);
+
             synchronized (mService) {
                 handleProcessStartedLocked(app, startResult, startSeq);
             }
@@ -2113,9 +2112,9 @@
     }
 
     private Process.ProcessStartResult startProcess(HostingRecord hostingRecord, String entryPoint,
-            ProcessRecord app, int uid, int[] gids, int runtimeFlags, int mountExternal,
-            String seInfo, String requiredAbi, String instructionSet, String invokeWith,
-            long startTime) {
+            ProcessRecord app, int uid, int[] gids, int runtimeFlags, int zygotePolicyFlags,
+            int mountExternal, String seInfo, String requiredAbi, String instructionSet,
+            String invokeWith, long startTime) {
         try {
             Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "Start proc: " +
                     app.processName);
@@ -2163,14 +2162,15 @@
                         app.processName, uid, uid, gids, runtimeFlags, mountExternal,
                         app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,
                         app.info.dataDir, null, app.info.packageName,
-                        /*useUsapPool=*/ false, isTopApp, app.mDisabledCompatChanges,
-                        pkgDataInfoMap, new String[]{PROC_START_SEQ_IDENT + app.startSeq});
+                        /*zygotePolicyFlags=*/ ZYGOTE_POLICY_FLAG_EMPTY, isTopApp,
+                        app.mDisabledCompatChanges, pkgDataInfoMap,
+                        new String[]{PROC_START_SEQ_IDENT + app.startSeq});
             } else {
                 startResult = Process.start(entryPoint,
                         app.processName, uid, uid, gids, runtimeFlags, mountExternal,
                         app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,
-                        app.info.dataDir, invokeWith, app.info.packageName, isTopApp,
-                        app.mDisabledCompatChanges, pkgDataInfoMap,
+                        app.info.dataDir, invokeWith, app.info.packageName, zygotePolicyFlags,
+                        isTopApp, app.mDisabledCompatChanges, pkgDataInfoMap,
                         new String[]{PROC_START_SEQ_IDENT + app.startSeq});
             }
             checkSlow(startTime, "startProcess: returned from zygote!");
@@ -2181,14 +2181,14 @@
     }
 
     @GuardedBy("mService")
-    final void startProcessLocked(ProcessRecord app, HostingRecord hostingRecord) {
-        startProcessLocked(app, hostingRecord, null /* abiOverride */);
+    void startProcessLocked(ProcessRecord app, HostingRecord hostingRecord, int zygotePolicyFlags) {
+        startProcessLocked(app, hostingRecord, zygotePolicyFlags, null /* abiOverride */);
     }
 
     @GuardedBy("mService")
     final boolean startProcessLocked(ProcessRecord app, HostingRecord hostingRecord,
-            String abiOverride) {
-        return startProcessLocked(app, hostingRecord,
+            int zygotePolicyFlags, String abiOverride) {
+        return startProcessLocked(app, hostingRecord, zygotePolicyFlags,
                 false /* disableHiddenApiChecks */, false /* disableTestApiChecks */,
                 false /* mountExtStorageFull */, abiOverride);
     }
@@ -2196,8 +2196,9 @@
     @GuardedBy("mService")
     final ProcessRecord startProcessLocked(String processName, ApplicationInfo info,
             boolean knownToBeDead, int intentFlags, HostingRecord hostingRecord,
-            boolean allowWhileBooting, boolean isolated, int isolatedUid, boolean keepIfLarge,
-            String abiOverride, String entryPoint, String[] entryPointArgs, Runnable crashHandler) {
+            int zygotePolicyFlags, boolean allowWhileBooting, boolean isolated, int isolatedUid,
+            boolean keepIfLarge, String abiOverride, String entryPoint, String[] entryPointArgs,
+            Runnable crashHandler) {
         long startTime = SystemClock.uptimeMillis();
         ProcessRecord app;
         if (!isolated) {
@@ -2308,7 +2309,8 @@
         }
 
         checkSlow(startTime, "startProcess: stepping in to startProcess");
-        final boolean success = startProcessLocked(app, hostingRecord, abiOverride);
+        final boolean success =
+                startProcessLocked(app, hostingRecord, zygotePolicyFlags, abiOverride);
         checkSlow(startTime, "startProcess: done starting proc!");
         return success ? app : null;
     }
@@ -2631,7 +2633,8 @@
             mService.handleAppDiedLocked(app, willRestart, allowRestart);
             if (willRestart) {
                 removeLruProcessLocked(app);
-                mService.addAppLocked(app.info, null, false, null /* ABI override */);
+                mService.addAppLocked(app.info, null, false, null /* ABI override */,
+                        ZYGOTE_POLICY_FLAG_EMPTY);
             }
         } else {
             mRemovedProcesses.add(app);
diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
index 3f8f6bf..e426574 100644
--- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
+++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
@@ -436,8 +436,7 @@
         mStoragedUidIoStatsReader = new StoragedUidIoStatsReader();
 
         // Initialize PROC_STATS
-        // TODO (b/148402814): Change this directory to stats_pull.
-        mBaseDir = new File(SystemServiceManager.ensureSystemDir(), "stats_companion");
+        mBaseDir = new File(SystemServiceManager.ensureSystemDir(), "stats_pull");
 
         // Disables throttler on CPU time readers.
         mCpuUidUserSysTimeReader = new KernelCpuUidUserSysTimeReader(false);
diff --git a/services/core/java/com/android/server/utils/quota/QuotaTracker.java b/services/core/java/com/android/server/utils/quota/QuotaTracker.java
index a8cf9f6..115b5c8 100644
--- a/services/core/java/com/android/server/utils/quota/QuotaTracker.java
+++ b/services/core/java/com/android/server/utils/quota/QuotaTracker.java
@@ -552,6 +552,7 @@
                     mTriggerTimeElapsed = nextTriggerTimeElapsed;
                 }
             } else {
+                cancelAlarm(this);
                 mTriggerTimeElapsed = 0;
             }
         }
diff --git a/tests/BootImageProfileTest/DISABLED_TEST_MAPPING b/tests/BootImageProfileTest/TEST_MAPPING
similarity index 100%
rename from tests/BootImageProfileTest/DISABLED_TEST_MAPPING
rename to tests/BootImageProfileTest/TEST_MAPPING