Merge "Introduce static @hide PowerManager.isRebootingUserspaceSupportedImpl()" into rvc-dev
diff --git a/apex/media/framework/Android.bp b/apex/media/framework/Android.bp
index 99e82e7..579963b 100644
--- a/apex/media/framework/Android.bp
+++ b/apex/media/framework/Android.bp
@@ -94,7 +94,6 @@
         // TODO(b/135922046) remove this
         include_dirs: ["frameworks/base/core/java"],
     },
-    sdk_version: "system_current",
 }
 
 droidstubs {
diff --git a/apex/permission/framework/Android.bp b/apex/permission/framework/Android.bp
index 793247e..fc9052e 100644
--- a/apex/permission/framework/Android.bp
+++ b/apex/permission/framework/Android.bp
@@ -46,12 +46,10 @@
     name: "framework-permission-stubs-defaults",
     srcs: [ ":framework-permission-sources" ],
     libs: [ "framework-annotations-lib" ],
-    sdk_version: "system_current",
 }
 
 droidstubs {
     name: "framework-permission-stubs-srcs-publicapi",
-    sdk_version: "system_current",
     defaults: [
         "framework-module-stubs-defaults-publicapi",
         "framework-permission-stubs-defaults",
@@ -60,7 +58,6 @@
 
 droidstubs {
     name: "framework-permission-stubs-srcs-systemapi",
-    sdk_version: "system_current",
     defaults: [
         "framework-module-stubs-defaults-systemapi",
         "framework-permission-stubs-defaults",
@@ -69,7 +66,6 @@
 
 droidstubs {
     name: "framework-permission-api-module_libs_api",
-    sdk_version: "system_current",
     defaults: [
         "framework-module-api-defaults-module_libs_api",
         "framework-permission-stubs-defaults",
@@ -78,7 +74,6 @@
 
 droidstubs {
     name: "framework-permission-stubs-srcs-module_libs_api",
-    sdk_version: "system_current",
     defaults: [
         "framework-module-stubs-defaults-module_libs_api",
         "framework-permission-stubs-defaults",
diff --git a/apex/sdkextensions/framework/Android.bp b/apex/sdkextensions/framework/Android.bp
index 707113b..3eabb88 100644
--- a/apex/sdkextensions/framework/Android.bp
+++ b/apex/sdkextensions/framework/Android.bp
@@ -48,7 +48,6 @@
     name: "framework-sdkextensions-stubs-defaults",
     srcs: [ ":framework-sdkextensions-sources" ],
     libs: [ "framework-annotations-lib" ],
-    sdk_version: "system_current",
 }
 
 droidstubs {
diff --git a/api/test-current.txt b/api/test-current.txt
index 22b051b..641767c 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -214,6 +214,7 @@
     field public static final String OPSTR_GPS = "android:gps";
     field public static final String OPSTR_INSTANT_APP_START_FOREGROUND = "android:instant_app_start_foreground";
     field public static final String OPSTR_LEGACY_STORAGE = "android:legacy_storage";
+    field public static final String OPSTR_MANAGE_EXTERNAL_STORAGE = "android:manage_external_storage";
     field public static final String OPSTR_MANAGE_IPSEC_TUNNELS = "android:manage_ipsec_tunnels";
     field public static final String OPSTR_MUTE_MICROPHONE = "android:mute_microphone";
     field public static final String OPSTR_NEIGHBORING_CELLS = "android:neighboring_cells";
diff --git a/cmds/statsd/benchmark/filter_value_benchmark.cpp b/cmds/statsd/benchmark/filter_value_benchmark.cpp
index 28bf21a..743ccc4 100644
--- a/cmds/statsd/benchmark/filter_value_benchmark.cpp
+++ b/cmds/statsd/benchmark/filter_value_benchmark.cpp
@@ -14,12 +14,14 @@
  * limitations under the License.
  */
 #include <vector>
-#include "benchmark/benchmark.h"
+
 #include "FieldValue.h"
 #include "HashableDimensionKey.h"
+#include "benchmark/benchmark.h"
 #include "logd/LogEvent.h"
-#include "stats_log_util.h"
+#include "metric_util.h"
 #include "stats_event.h"
+#include "stats_log_util.h"
 
 namespace android {
 namespace os {
@@ -34,24 +36,13 @@
 
     std::vector<int> attributionUids = {100, 100};
     std::vector<string> attributionTags = {"LOCATION", "LOCATION"};
+    writeAttribution(statsEvent, attributionUids, attributionTags);
 
-    vector<const char*> cTags(attributionTags.size());
-    for (int i = 0; i < cTags.size(); i++) {
-        cTags[i] = attributionTags[i].c_str();
-    }
-
-    AStatsEvent_writeAttributionChain(statsEvent,
-                                      reinterpret_cast<const uint32_t*>(attributionUids.data()),
-                                      cTags.data(), attributionUids.size());
     AStatsEvent_writeFloat(statsEvent, 3.2f);
     AStatsEvent_writeString(statsEvent, "LOCATION");
     AStatsEvent_writeInt64(statsEvent, 990);
-    AStatsEvent_build(statsEvent);
 
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
-    event->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, event);
 
     field_matcher->set_field(1);
     auto child = field_matcher->add_child();
diff --git a/cmds/statsd/benchmark/get_dimensions_for_condition_benchmark.cpp b/cmds/statsd/benchmark/get_dimensions_for_condition_benchmark.cpp
index c7d01cc..7a45565 100644
--- a/cmds/statsd/benchmark/get_dimensions_for_condition_benchmark.cpp
+++ b/cmds/statsd/benchmark/get_dimensions_for_condition_benchmark.cpp
@@ -14,12 +14,14 @@
  * limitations under the License.
  */
 #include <vector>
-#include "benchmark/benchmark.h"
+
 #include "FieldValue.h"
 #include "HashableDimensionKey.h"
+#include "benchmark/benchmark.h"
 #include "logd/LogEvent.h"
-#include "stats_log_util.h"
+#include "metric_util.h"
 #include "stats_event.h"
+#include "stats_log_util.h"
 
 namespace android {
 namespace os {
@@ -34,24 +36,13 @@
 
     std::vector<int> attributionUids = {100, 100};
     std::vector<string> attributionTags = {"LOCATION", "LOCATION"};
+    writeAttribution(statsEvent, attributionUids, attributionTags);
 
-    vector<const char*> cTags(attributionTags.size());
-    for (int i = 0; i < cTags.size(); i++) {
-        cTags[i] = attributionTags[i].c_str();
-    }
-
-    AStatsEvent_writeAttributionChain(statsEvent,
-                                      reinterpret_cast<const uint32_t*>(attributionUids.data()),
-                                      cTags.data(), attributionUids.size());
     AStatsEvent_writeFloat(statsEvent, 3.2f);
     AStatsEvent_writeString(statsEvent, "LOCATION");
     AStatsEvent_writeInt64(statsEvent, 990);
-    AStatsEvent_build(statsEvent);
 
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
-    event->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, event);
 
     link->conditionId = 1;
 
diff --git a/cmds/statsd/benchmark/metric_util.cpp b/cmds/statsd/benchmark/metric_util.cpp
index 482d66f..89fd3d9 100644
--- a/cmds/statsd/benchmark/metric_util.cpp
+++ b/cmds/statsd/benchmark/metric_util.cpp
@@ -247,21 +247,37 @@
     return dimensions;
 }
 
+void writeAttribution(AStatsEvent* statsEvent, const vector<int>& attributionUids,
+                      const vector<string>& attributionTags) {
+    vector<const char*> cTags(attributionTags.size());
+    for (int i = 0; i < cTags.size(); i++) {
+        cTags[i] = attributionTags[i].c_str();
+    }
+
+    AStatsEvent_writeAttributionChain(statsEvent,
+                                      reinterpret_cast<const uint32_t*>(attributionUids.data()),
+                                      cTags.data(), attributionUids.size());
+}
+
+void parseStatsEventToLogEvent(AStatsEvent* statsEvent, LogEvent* logEvent) {
+    AStatsEvent_build(statsEvent);
+
+    size_t size;
+    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
+    logEvent->parseBuffer(buf, size);
+
+    AStatsEvent_release(statsEvent);
+}
+
 std::unique_ptr<LogEvent> CreateScreenStateChangedEvent(
         uint64_t timestampNs, const android::view::DisplayStateEnum state) {
     AStatsEvent* statsEvent = AStatsEvent_obtain();
     AStatsEvent_setAtomId(statsEvent, util::SCREEN_STATE_CHANGED);
     AStatsEvent_overwriteTimestamp(statsEvent, timestampNs);
-
     AStatsEvent_writeInt32(statsEvent, state);
-    AStatsEvent_build(statsEvent);
-
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
 
     std::unique_ptr<LogEvent> logEvent = std::make_unique<LogEvent>(/*uid=*/0, /*pid=*/0);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent.get());
     return logEvent;
 }
 
@@ -272,24 +288,12 @@
     AStatsEvent_setAtomId(statsEvent, util::SCHEDULED_JOB_STATE_CHANGED);
     AStatsEvent_overwriteTimestamp(statsEvent, timestampNs);
 
-    vector<const char*> cTags(attributionTags.size());
-    for (int i = 0; i < cTags.size(); i++) {
-        cTags[i] = attributionTags[i].c_str();
-    }
-
-    AStatsEvent_writeAttributionChain(statsEvent,
-                                      reinterpret_cast<const uint32_t*>(attributionUids.data()),
-                                      cTags.data(), attributionUids.size());
+    writeAttribution(statsEvent, attributionUids, attributionTags);
     AStatsEvent_writeString(statsEvent, jobName.c_str());
     AStatsEvent_writeInt32(statsEvent, state);
-    AStatsEvent_build(statsEvent);
-
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
 
     std::unique_ptr<LogEvent> logEvent = std::make_unique<LogEvent>(/*uid=*/0, /*pid=*/0);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent.get());
     return logEvent;
 }
 
@@ -319,24 +323,12 @@
     AStatsEvent_setAtomId(statsEvent, util::SYNC_STATE_CHANGED);
     AStatsEvent_overwriteTimestamp(statsEvent, timestampNs);
 
-    vector<const char*> cTags(attributionTags.size());
-    for (int i = 0; i < cTags.size(); i++) {
-        cTags[i] = attributionTags[i].c_str();
-    }
-
-    AStatsEvent_writeAttributionChain(statsEvent,
-                                      reinterpret_cast<const uint32_t*>(attributionUids.data()),
-                                      cTags.data(), attributionUids.size());
+    writeAttribution(statsEvent, attributionUids, attributionTags);
     AStatsEvent_writeString(statsEvent, name.c_str());
     AStatsEvent_writeInt32(statsEvent, state);
-    AStatsEvent_build(statsEvent);
-
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
 
     std::unique_ptr<LogEvent> logEvent = std::make_unique<LogEvent>(/*uid=*/0, /*pid=*/0);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent.get());
     return logEvent;
 }
 
diff --git a/cmds/statsd/benchmark/metric_util.h b/cmds/statsd/benchmark/metric_util.h
index c5fcf7c..3efaa85 100644
--- a/cmds/statsd/benchmark/metric_util.h
+++ b/cmds/statsd/benchmark/metric_util.h
@@ -18,6 +18,7 @@
 #include "frameworks/base/cmds/statsd/src/statsd_config.pb.h"
 #include "src/StatsLogProcessor.h"
 #include "src/logd/LogEvent.h"
+#include "stats_event.h"
 #include "statslog.h"
 
 namespace android {
@@ -92,6 +93,11 @@
 FieldMatcher CreateAttributionUidDimensions(const int atomId,
                                             const std::vector<Position>& positions);
 
+void writeAttribution(AStatsEvent* statsEvent, const vector<int>& attributionUids,
+                      const vector<string>& attributionTags);
+
+void parseStatsEventToLogEvent(AStatsEvent* statsEvent, LogEvent* logEvent);
+
 // Create log event for screen state changed.
 std::unique_ptr<LogEvent> CreateScreenStateChangedEvent(
         uint64_t timestampNs, const android::view::DisplayStateEnum state);
diff --git a/cmds/statsd/src/FieldValue.cpp b/cmds/statsd/src/FieldValue.cpp
index 4385964..cfc1de4 100644
--- a/cmds/statsd/src/FieldValue.cpp
+++ b/cmds/statsd/src/FieldValue.cpp
@@ -120,9 +120,9 @@
 }
 
 int32_t getUidIfExists(const FieldValue& value) {
-    // the field is uid field if the field is the uid field in attribution node or marked as
-    // is_uid in atoms.proto
-    bool isUid = isAttributionUidField(value) || isUidField(value.mField, value.mValue);
+    // the field is uid field if the field is the uid field in attribution node
+    // or annotated as such in the atom
+    bool isUid = isAttributionUidField(value) || isUidField(value);
     return isUid ? value.mValue.int_value : -1;
 }
 
@@ -134,16 +134,8 @@
     return false;
 }
 
-bool isUidField(const Field& field, const Value& value) {
-    auto it = android::util::AtomsInfo::kAtomsWithUidField.find(field.getTag());
-
-    if (it != android::util::AtomsInfo::kAtomsWithUidField.end()) {
-        int uidField = it->second;  // uidField is the field number in proto
-        return field.getDepth() == 0 && field.getPosAtDepth(0) == uidField &&
-               value.getType() == INT;
-    }
-
-    return false;
+bool isUidField(const FieldValue& fieldValue) {
+    return fieldValue.mAnnotations.isUidField();
 }
 
 Value::Value(const Value& from) {
diff --git a/cmds/statsd/src/FieldValue.h b/cmds/statsd/src/FieldValue.h
index 3536e5a..92e09ea 100644
--- a/cmds/statsd/src/FieldValue.h
+++ b/cmds/statsd/src/FieldValue.h
@@ -367,7 +367,8 @@
     enum {
         NESTED_POS = 0x0,
         PRIMARY_POS = 0x1,
-        EXCLUSIVE_POS = 0x2
+        EXCLUSIVE_POS = 0x2,
+        UID_POS = 0x3
     };
 
     inline void setNested(bool nested) { setBitmaskAtPos(NESTED_POS, nested); }
@@ -376,6 +377,8 @@
 
     inline void setExclusiveState(bool exclusive) { setBitmaskAtPos(EXCLUSIVE_POS, exclusive); }
 
+    inline void setUidField(bool isUid) { setBitmaskAtPos(UID_POS, isUid); }
+
     inline void setResetState(int resetState) { mResetState = resetState; }
 
     // Default value = false
@@ -387,6 +390,9 @@
     // Default value = false
     inline bool isExclusiveState() const { return getValueFromBitmask(EXCLUSIVE_POS); }
 
+    // Default value = false
+    inline bool isUidField() const { return getValueFromBitmask(UID_POS); }
+
     // If a reset state is not sent in the StatsEvent, returns -1. Note that a
     // reset satate is only sent if and only if a reset should be triggered.
     inline int getResetState() const { return mResetState; }
@@ -402,7 +408,7 @@
     }
 
     // This is a bitmask over all annotations stored in boolean form. Because
-    // there are only 3 booleans, just one byte is required.
+    // there are only 4 booleans, just one byte is required.
     uint8_t mBooleanBitmask = 0;
 
     int mResetState = -1;
@@ -449,7 +455,7 @@
 void translateFieldMatcher(const FieldMatcher& matcher, std::vector<Matcher>* output);
 
 bool isAttributionUidField(const Field& field, const Value& value);
-bool isUidField(const Field& field, const Value& value);
+bool isUidField(const FieldValue& fieldValue);
 
 bool equalDimensions(const std::vector<Matcher>& dimension_a,
                      const std::vector<Matcher>& dimension_b);
diff --git a/cmds/statsd/src/StatsLogProcessor.cpp b/cmds/statsd/src/StatsLogProcessor.cpp
index 982a63e..325cbc7 100644
--- a/cmds/statsd/src/StatsLogProcessor.cpp
+++ b/cmds/statsd/src/StatsLogProcessor.cpp
@@ -138,13 +138,6 @@
     }
 }
 
-void updateUid(Value* value, int hostUid) {
-    int uid = value->int_value;
-    if (uid != hostUid) {
-        value->setInt(hostUid);
-    }
-}
-
 void StatsLogProcessor::mapIsolatedUidToHostUidIfNecessaryLocked(LogEvent* event) const {
     if (android::util::AtomsInfo::kAtomsWithAttributionChain.find(event->GetTagId()) !=
         android::util::AtomsInfo::kAtomsWithAttributionChain.end()) {
@@ -154,22 +147,15 @@
             }
             if (isAttributionUidField(value)) {
                 const int hostUid = mUidMap->getHostUidOrSelf(value.mValue.int_value);
-                updateUid(&value.mValue, hostUid);
+                value.mValue.setInt(hostUid);
             }
         }
     } else {
-        auto it = android::util::AtomsInfo::kAtomsWithUidField.find(event->GetTagId());
-        if (it != android::util::AtomsInfo::kAtomsWithUidField.end()) {
-            int uidField = it->second;  // uidField is the field number in proto,
-                                        // starting from 1
-            if (uidField > 0 && (int)event->getValues().size() >= uidField &&
-                (event->getValues())[uidField - 1].mValue.getType() == INT) {
-                Value& value = (*event->getMutableValues())[uidField - 1].mValue;
-                const int hostUid = mUidMap->getHostUidOrSelf(value.int_value);
-                updateUid(&value, hostUid);
-            } else {
-                ALOGE("Malformed log, uid not found. %s", event->ToString().c_str());
-            }
+        int uidFieldIndex = event->getUidFieldIndex();
+        if (uidFieldIndex != -1) {
+           Value& value = (*event->getMutableValues())[uidFieldIndex].mValue;
+           const int hostUid = mUidMap->getHostUidOrSelf(value.int_value);
+           value.setInt(hostUid);
         }
     }
 }
diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto
index d1995bc..2a1716e 100644
--- a/cmds/statsd/src/atoms.proto
+++ b/cmds/statsd/src/atoms.proto
@@ -419,6 +419,7 @@
         SharesheetStarted sharesheet_started = 259 [(module) = "framework"];
         RankingSelected ranking_selected = 260 [(module) = "framework"];
         TvSettingsUIInteracted tvsettings_ui_interacted = 261 [(module) = "tv_settings"];
+        LauncherStaticLayout launcher_snapshot = 262 [(module) = "sysui"];
         SdkExtensionStatus sdk_extension_status = 354;
     }
 
@@ -2979,14 +2980,98 @@
     optional int32 duration_millis = 7;
 }
 
+/**
+ * Logs when Launcher (HomeScreen) UI has changed or was interacted.
+ *
+ * Logged from:
+ *   packages/apps/Launcher3
+ */
 message LauncherUIChanged {
-    optional android.stats.launcher.LauncherAction action = 1;
+    optional android.stats.launcher.LauncherAction action = 1 [deprecated = true];
     optional android.stats.launcher.LauncherState src_state = 2;
     optional android.stats.launcher.LauncherState dst_state = 3;
-    optional android.stats.launcher.LauncherExtension extension = 4 [(log_mode) = MODE_BYTES];
-    optional bool is_swipe_up_enabled = 5;
+    optional android.stats.launcher.LauncherExtension extension = 4 [(log_mode) = MODE_BYTES, deprecated = true];
+    optional bool is_swipe_up_enabled = 5 [deprecated = true];
+
+    // The event id (e.g., app launch, drag and drop, long press)
+    optional int32 event_id = 6;
+    // The event's source or target id (e.g., icon, task, button)
+    optional int32 target_id = 7;
+    // If the target needs to be tracked, use this id field
+    optional int32 instance_id = 8;
+    optional int32 uid = 9 [(is_uid) = true];
+    optional string package_name = 10;
+    optional string component_name = 11;
+
+    // (x, y) coordinate and the index information of the target on the container
+    optional int32 grid_x = 12;
+    optional int32 grid_y = 13;
+    optional int32 page_id = 14;
+
+    // e.g., folder icon's (x, y) location and index information on the workspace
+    optional int32 grid_x_parent = 15;
+    optional int32 grid_y_parent = 16;
+    optional int32 page_id_parent = 17;
+
+    // e.g., SEARCHBOX_ALLAPPS, FOLDER_WORKSPACE
+    optional int32 hierarchy = 18;
+
+    optional bool is_work_profile = 19;
+
+    // Used to store the predicted rank of the target
+    optional int32 rank = 20;
+
+    // e.g., folderLabelState can be captured in the following two fields
+    optional int32 from_state = 21;
+    optional int32 to_state = 22;
+
+    // e.g., autofilled or suggested texts that are not user entered
+    optional string edittext = 23;
 }
 
+/**
+ * Used for snapshot of the HomeScreen UI elements
+ *
+ * Logged from:
+ *   packages/apps/Launcher3
+ */
+message LauncherStaticLayout {
+    // The event id (e.g., snapshot, drag and drop)
+    optional int32 event_id = 1;
+    // The event's source or target id (e.g., icon, shortcut, widget)
+    optional int32 target_id = 2;
+    // If the target needs to be tracked, use this id field
+    optional int32 instance_id = 3;
+    optional int32 uid = 4 [(is_uid) = true];
+    optional string package_name = 5;
+    optional string component_name = 6;
+
+    // (x, y) coordinate and the index information of the target on the container
+    optional int32 grid_x = 7;
+    optional int32 grid_y = 8;
+    optional int32 page_id = 9;
+
+    // e.g., folder icon's (x, y) location and index information on the workspace
+    optional int32 grid_x_parent = 10;
+    optional int32 grid_y_parent = 11;
+    optional int32 page_id_parent = 12;
+
+    // e.g., WORKSPACE, HOTSEAT, FOLDER_WORKSPACE, FOLDER_HOTSEAT
+    optional int32 hierarchy = 13;
+
+    optional bool is_work_profile = 14;
+
+    // e.g., PIN, WIDGET TRAY, APPS TRAY, PREDICTION
+    optional int32 origin = 15;
+}
+
+/**
+ * Logs when Wallpaper or ThemePicker UI has changed.
+ *
+ * Logged from:
+ *   packages/apps/ThemePicker
+ *   packages/apps/WallpaperPicker2
+ */
 message StyleUIChanged {
     optional android.stats.style.Action action = 1;
     optional int32 color_package_hash = 2;
diff --git a/cmds/statsd/src/external/puller_util.cpp b/cmds/statsd/src/external/puller_util.cpp
index aee7256..90247cf 100644
--- a/cmds/statsd/src/external/puller_util.cpp
+++ b/cmds/statsd/src/external/puller_util.cpp
@@ -49,10 +49,14 @@
  */
 void mapAndMergeIsolatedUidsToHostUid(vector<shared_ptr<LogEvent>>& data, const sp<UidMap>& uidMap,
                                       int tagId, const vector<int>& additiveFieldsVec) {
-    if ((android::util::AtomsInfo::kAtomsWithAttributionChain.find(tagId) ==
-         android::util::AtomsInfo::kAtomsWithAttributionChain.end()) &&
-        (android::util::AtomsInfo::kAtomsWithUidField.find(tagId) ==
-         android::util::AtomsInfo::kAtomsWithUidField.end())) {
+    bool hasAttributionChain = (android::util::AtomsInfo::kAtomsWithAttributionChain.find(tagId) !=
+                                android::util::AtomsInfo::kAtomsWithAttributionChain.end());
+    // To check if any LogEvent has a uid field, we can just check the first
+    // LogEvent because all atoms with this tagId should have the uid
+    // annotation.
+    bool hasUidField = (data[0]->getUidFieldIndex() != -1);
+
+    if (!hasAttributionChain && !hasUidField) {
         VLOG("No uid or attribution chain to merge, atom %d", tagId);
         return;
     }
@@ -75,19 +79,13 @@
                 }
             }
         } else {
-            auto it = android::util::AtomsInfo::kAtomsWithUidField.find(tagId);
-            if (it != android::util::AtomsInfo::kAtomsWithUidField.end()) {
-                int uidField = it->second;  // uidField is the field number in proto,
-                // starting from 1
-                if (uidField > 0 && (int)event->getValues().size() >= uidField &&
-                    (event->getValues())[uidField - 1].mValue.getType() == INT) {
-                    Value& value = (*event->getMutableValues())[uidField - 1].mValue;
-                    const int hostUid = uidMap->getHostUidOrSelf(value.int_value);
-                    value.setInt(hostUid);
-                } else {
-                    ALOGE("Malformed log, uid not found. %s", event->ToString().c_str());
-                    return;
-                }
+            int uidFieldIndex = event->getUidFieldIndex();
+            if (uidFieldIndex != -1) {
+                Value& value = (*event->getMutableValues())[uidFieldIndex].mValue;
+                const int hostUid = uidMap->getHostUidOrSelf(value.int_value);
+                value.setInt(hostUid);
+            } else {
+                ALOGE("Malformed log, uid not found. %s", event->ToString().c_str());
             }
         }
     }
diff --git a/cmds/statsd/src/logd/LogEvent.cpp b/cmds/statsd/src/logd/LogEvent.cpp
index 96bf04f..8b6a864 100644
--- a/cmds/statsd/src/logd/LogEvent.cpp
+++ b/cmds/statsd/src/logd/LogEvent.cpp
@@ -240,14 +240,20 @@
     last[1] = last[2] = false;
 }
 
+// Assumes that mValues is not empty
+bool LogEvent::checkPreviousValueType(Type expected) {
+    return mValues[mValues.size() - 1].mValue.getType() == expected;
+}
+
 void LogEvent::parseIsUidAnnotation(uint8_t annotationType) {
-    if (mValues.empty() || annotationType != BOOL_TYPE) {
+    if (mValues.empty() || !checkPreviousValueType(INT) || annotationType != BOOL_TYPE) {
         mValid = false;
         return;
     }
 
     bool isUid = readNextValue<uint8_t>();
     if (isUid) mUidFieldIndex = mValues.size() - 1;
+    mValues[mValues.size() - 1].mAnnotations.setUidField(isUid);
 }
 
 void LogEvent::parseTruncateTimestampAnnotation(uint8_t annotationType) {
diff --git a/cmds/statsd/src/logd/LogEvent.h b/cmds/statsd/src/logd/LogEvent.h
index 9e21c77..4eeb7d6 100644
--- a/cmds/statsd/src/logd/LogEvent.h
+++ b/cmds/statsd/src/logd/LogEvent.h
@@ -213,6 +213,7 @@
     void parseExclusiveStateAnnotation(uint8_t annotationType);
     void parseTriggerStateResetAnnotation(uint8_t annotationType);
     void parseStateNestedAnnotation(uint8_t annotationType);
+    bool checkPreviousValueType(Type expected);
 
     /**
      * The below three variables are only valid during the execution of
diff --git a/cmds/statsd/src/matchers/matcher_util.cpp b/cmds/statsd/src/matchers/matcher_util.cpp
index 1f8bbd7..2b4c6a3 100644
--- a/cmds/statsd/src/matchers/matcher_util.cpp
+++ b/cmds/statsd/src/matchers/matcher_util.cpp
@@ -81,18 +81,17 @@
     return matched;
 }
 
-bool tryMatchString(const UidMap& uidMap, const Field& field, const Value& value,
-                    const string& str_match) {
-    if (isAttributionUidField(field, value) || isUidField(field, value)) {
-        int uid = value.int_value;
+bool tryMatchString(const UidMap& uidMap, const FieldValue& fieldValue, const string& str_match) {
+    if (isAttributionUidField(fieldValue) || isUidField(fieldValue)) {
+        int uid = fieldValue.mValue.int_value;
         auto aidIt = UidMap::sAidToUidMapping.find(str_match);
         if (aidIt != UidMap::sAidToUidMapping.end()) {
             return ((int)aidIt->second) == uid;
         }
         std::set<string> packageNames = uidMap.getAppNamesFromUid(uid, true /* normalize*/);
         return packageNames.find(str_match) != packageNames.end();
-    } else if (value.getType() == STRING) {
-        return value.str_value == str_match;
+    } else if (fieldValue.mValue.getType() == STRING) {
+        return fieldValue.mValue.str_value == str_match;
     }
     return false;
 }
@@ -228,8 +227,7 @@
         }
         case FieldValueMatcher::ValueMatcherCase::kEqString: {
             for (int i = start; i < end; i++) {
-                if (tryMatchString(uidMap, values[i].mField, values[i].mValue,
-                                   matcher.eq_string())) {
+                if (tryMatchString(uidMap, values[i], matcher.eq_string())) {
                     return true;
                 }
             }
@@ -240,7 +238,7 @@
             for (int i = start; i < end; i++) {
                 bool notEqAll = true;
                 for (const auto& str : str_list.str_value()) {
-                    if (tryMatchString(uidMap, values[i].mField, values[i].mValue, str)) {
+                    if (tryMatchString(uidMap, values[i], str)) {
                         notEqAll = false;
                         break;
                     }
@@ -255,7 +253,7 @@
             const auto& str_list = matcher.eq_any_string();
             for (int i = start; i < end; i++) {
                 for (const auto& str : str_list.str_value()) {
-                    if (tryMatchString(uidMap, values[i].mField, values[i].mValue, str)) {
+                    if (tryMatchString(uidMap, values[i], str)) {
                         return true;
                     }
                 }
diff --git a/cmds/statsd/tests/FieldValue_test.cpp b/cmds/statsd/tests/FieldValue_test.cpp
index 0bf24f1..f5ba8fd 100644
--- a/cmds/statsd/tests/FieldValue_test.cpp
+++ b/cmds/statsd/tests/FieldValue_test.cpp
@@ -41,22 +41,10 @@
     AStatsEvent_setAtomId(statsEvent, atomId);
     AStatsEvent_overwriteTimestamp(statsEvent, timestamp);
 
-    vector<const char*> cTags(attributionTags.size());
-    for (int i = 0; i < cTags.size(); i++) {
-        cTags[i] = attributionTags[i].c_str();
-    }
-
-    AStatsEvent_writeAttributionChain(statsEvent,
-                                      reinterpret_cast<const uint32_t*>(attributionUids.data()),
-                                      cTags.data(), attributionUids.size());
+    writeAttribution(statsEvent, attributionUids, attributionTags);
     AStatsEvent_writeString(statsEvent, name.c_str());
-    AStatsEvent_build(statsEvent);
 
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
-    logEvent->parseBuffer(buf, size);
-
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent);
 }
 
 void makeLogEvent(LogEvent* logEvent, const int32_t atomId, const int64_t timestamp,
@@ -66,22 +54,10 @@
     AStatsEvent_setAtomId(statsEvent, atomId);
     AStatsEvent_overwriteTimestamp(statsEvent, timestamp);
 
-    vector<const char*> cTags(attributionTags.size());
-    for (int i = 0; i < cTags.size(); i++) {
-        cTags[i] = attributionTags[i].c_str();
-    }
-
-    AStatsEvent_writeAttributionChain(statsEvent,
-                                      reinterpret_cast<const uint32_t*>(attributionUids.data()),
-                                      cTags.data(), attributionUids.size());
+    writeAttribution(statsEvent, attributionUids, attributionTags);
     AStatsEvent_writeInt32(statsEvent, value);
-    AStatsEvent_build(statsEvent);
 
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
-    logEvent->parseBuffer(buf, size);
-
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent);
 }
 }  // anonymous namespace
 
diff --git a/cmds/statsd/tests/LogEntryMatcher_test.cpp b/cmds/statsd/tests/LogEntryMatcher_test.cpp
index 6f4c8e4..26423d4 100644
--- a/cmds/statsd/tests/LogEntryMatcher_test.cpp
+++ b/cmds/statsd/tests/LogEntryMatcher_test.cpp
@@ -18,6 +18,7 @@
 #include <log/logprint.h>
 #include <stdio.h>
 
+#include "annotations.h"
 #include "frameworks/base/cmds/statsd/src/statsd_config.pb.h"
 #include "matchers/matcher_util.h"
 #include "stats_event.h"
@@ -48,15 +49,9 @@
     AStatsEvent* statsEvent = AStatsEvent_obtain();
     AStatsEvent_setAtomId(statsEvent, atomId);
     AStatsEvent_overwriteTimestamp(statsEvent, timestamp);
-
     AStatsEvent_writeInt32(statsEvent, value);
-    AStatsEvent_build(statsEvent);
 
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
-    logEvent->parseBuffer(buf, size);
-
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent);
 }
 
 void makeFloatLogEvent(LogEvent* logEvent, const int32_t atomId, const int64_t timestamp,
@@ -64,15 +59,9 @@
     AStatsEvent* statsEvent = AStatsEvent_obtain();
     AStatsEvent_setAtomId(statsEvent, atomId);
     AStatsEvent_overwriteTimestamp(statsEvent, timestamp);
-
     AStatsEvent_writeFloat(statsEvent, floatValue);
-    AStatsEvent_build(statsEvent);
 
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
-    logEvent->parseBuffer(buf, size);
-
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent);
 }
 
 void makeStringLogEvent(LogEvent* logEvent, const int32_t atomId, const int64_t timestamp,
@@ -80,32 +69,20 @@
     AStatsEvent* statsEvent = AStatsEvent_obtain();
     AStatsEvent_setAtomId(statsEvent, atomId);
     AStatsEvent_overwriteTimestamp(statsEvent, timestamp);
-
     AStatsEvent_writeString(statsEvent, name.c_str());
-    AStatsEvent_build(statsEvent);
 
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
-    logEvent->parseBuffer(buf, size);
-
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent);
 }
 
-void makeIntStringLogEvent(LogEvent* logEvent, const int32_t atomId, const int64_t timestamp,
-                           const int32_t value, const string& name) {
+void makeIntWithBoolAnnotationLogEvent(LogEvent* logEvent, const int32_t atomId,
+                                       const int32_t field, const uint8_t annotationId,
+                                       const bool annotationValue) {
     AStatsEvent* statsEvent = AStatsEvent_obtain();
     AStatsEvent_setAtomId(statsEvent, atomId);
-    AStatsEvent_overwriteTimestamp(statsEvent, timestamp);
+    AStatsEvent_writeInt32(statsEvent, field);
+    AStatsEvent_addBoolAnnotation(statsEvent, annotationId, annotationValue);
 
-    AStatsEvent_writeInt32(statsEvent, value);
-    AStatsEvent_writeString(statsEvent, name.c_str());
-    AStatsEvent_build(statsEvent);
-
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
-    logEvent->parseBuffer(buf, size);
-
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent);
 }
 
 void makeAttributionLogEvent(LogEvent* logEvent, const int32_t atomId, const int64_t timestamp,
@@ -115,22 +92,10 @@
     AStatsEvent_setAtomId(statsEvent, atomId);
     AStatsEvent_overwriteTimestamp(statsEvent, timestamp);
 
-    vector<const char*> cTags(attributionTags.size());
-    for (int i = 0; i < cTags.size(); i++) {
-        cTags[i] = attributionTags[i].c_str();
-    }
-
-    AStatsEvent_writeAttributionChain(statsEvent,
-                                      reinterpret_cast<const uint32_t*>(attributionUids.data()),
-                                      cTags.data(), attributionUids.size());
+    writeAttribution(statsEvent, attributionUids, attributionTags);
     AStatsEvent_writeString(statsEvent, name.c_str());
-    AStatsEvent_build(statsEvent);
 
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
-    logEvent->parseBuffer(buf, size);
-
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent);
 }
 
 void makeBoolLogEvent(LogEvent* logEvent, const int32_t atomId, const int64_t timestamp,
@@ -141,13 +106,8 @@
 
     AStatsEvent_writeBool(statsEvent, bool1);
     AStatsEvent_writeBool(statsEvent, bool2);
-    AStatsEvent_build(statsEvent);
 
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
-    logEvent->parseBuffer(buf, size);
-
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent);
 }
 
 }  // anonymous namespace
@@ -416,21 +376,20 @@
     simpleMatcher->add_field_value_matcher()->set_field(1);
     simpleMatcher->mutable_field_value_matcher(0)->set_eq_string("pkg0");
 
-    // Set up the event
+    // Make event without is_uid annotation.
     LogEvent event1(/*uid=*/0, /*pid=*/0);
     makeIntLogEvent(&event1, TAG_ID, 0, 1111);
-
-    LogEvent event2(/*uid=*/0, /*pid=*/0);
-    makeIntStringLogEvent(&event2, TAG_ID_2, 0, 1111, "some value");
-
-    // Tag not in kAtomsWithUidField
     EXPECT_FALSE(matchesSimple(uidMap, *simpleMatcher, event1));
 
-    // Tag found in kAtomsWithUidField and has matching uid
+    // Make event with is_uid annotation.
+    LogEvent event2(/*uid=*/0, /*pid=*/0);
+    makeIntWithBoolAnnotationLogEvent(&event2, TAG_ID_2, 1111, ANNOTATION_ID_IS_UID, true);
+
+    // Event has is_uid annotation, so mapping from uid to package name occurs.
     simpleMatcher->set_atom_id(TAG_ID_2);
     EXPECT_TRUE(matchesSimple(uidMap, *simpleMatcher, event2));
 
-    // Tag found in kAtomsWithUidField but has non-matching uid
+    // Event has is_uid annotation, but uid maps to different package name.
     simpleMatcher->mutable_field_value_matcher(0)->set_eq_string("Pkg2");
     EXPECT_FALSE(matchesSimple(uidMap, *simpleMatcher, event2));
 }
diff --git a/cmds/statsd/tests/condition/SimpleConditionTracker_test.cpp b/cmds/statsd/tests/condition/SimpleConditionTracker_test.cpp
index 7febb35..ba5b032 100644
--- a/cmds/statsd/tests/condition/SimpleConditionTracker_test.cpp
+++ b/cmds/statsd/tests/condition/SimpleConditionTracker_test.cpp
@@ -67,22 +67,12 @@
     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();
-    }
-    AStatsEvent_writeAttributionChain(statsEvent, reinterpret_cast<const uint32_t*>(uids.data()),
-                                      cTags.data(), uids.size());
+    writeAttribution(statsEvent, uids, tags);
 
     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);
+    parseStatsEventToLogEvent(statsEvent, logEvent);
 }
 
 } // anonymous namespace
diff --git a/cmds/statsd/tests/e2e/GaugeMetric_e2e_push_test.cpp b/cmds/statsd/tests/e2e/GaugeMetric_e2e_push_test.cpp
index 81e1c05..60403f2 100644
--- a/cmds/statsd/tests/e2e/GaugeMetric_e2e_push_test.cpp
+++ b/cmds/statsd/tests/e2e/GaugeMetric_e2e_push_test.cpp
@@ -84,14 +84,9 @@
     AStatsEvent_writeString(statsEvent, calling_pkg_name.c_str());
     AStatsEvent_writeInt32(statsEvent, is_instant_app);
     AStatsEvent_writeInt32(statsEvent, activity_start_msec);
-    AStatsEvent_build(statsEvent);
-
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
 
     std::unique_ptr<LogEvent> logEvent = std::make_unique<LogEvent>(/*uid=*/0, /*pid=*/0);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent.get());
     return logEvent;
 }
 
diff --git a/cmds/statsd/tests/external/StatsPuller_test.cpp b/cmds/statsd/tests/external/StatsPuller_test.cpp
index e8200d5..5043358 100644
--- a/cmds/statsd/tests/external/StatsPuller_test.cpp
+++ b/cmds/statsd/tests/external/StatsPuller_test.cpp
@@ -64,16 +64,10 @@
     AStatsEvent* statsEvent = AStatsEvent_obtain();
     AStatsEvent_setAtomId(statsEvent, pullTagId);
     AStatsEvent_overwriteTimestamp(statsEvent, eventTimeNs);
-
     AStatsEvent_writeInt64(statsEvent, value);
-    AStatsEvent_build(statsEvent);
-
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
 
     std::unique_ptr<LogEvent> logEvent = std::make_unique<LogEvent>(/*uid=*/0, /*pid=*/0);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent.get());
     return logEvent;
 }
 
diff --git a/cmds/statsd/tests/external/puller_util_test.cpp b/cmds/statsd/tests/external/puller_util_test.cpp
index 15425d8..c2cfb37 100644
--- a/cmds/statsd/tests/external/puller_util_test.cpp
+++ b/cmds/statsd/tests/external/puller_util_test.cpp
@@ -21,8 +21,10 @@
 #include <vector>
 
 #include "../metrics/metrics_test_helper.h"
+#include "annotations.h"
 #include "stats_event.h"
 #include "statslog_statsdtest.h"
+#include "tests/statsd_test_util.h"
 
 #ifdef __ANDROID__
 
@@ -52,15 +54,15 @@
 
 void extractIntoVector(vector<shared_ptr<LogEvent>> events,
                       vector<vector<int>>& ret) {
-  ret.clear();
-  status_t err;
-  for (const auto& event : events) {
-    vector<int> vec;
-    vec.push_back(event->GetInt(1, &err));
-    vec.push_back(event->GetInt(2, &err));
-    vec.push_back(event->GetInt(3, &err));
-    ret.push_back(vec);
-  }
+    ret.clear();
+    status_t err;
+    for (const auto& event : events) {
+        vector<int> vec;
+        vec.push_back(event->GetInt(1, &err));
+        vec.push_back(event->GetInt(2, &err));
+        vec.push_back(event->GetInt(3, &err));
+        ret.push_back(vec);
+    }
 }
 
 std::shared_ptr<LogEvent> makeUidLogEvent(uint64_t timestampNs, int uid, int data1, int data2) {
@@ -69,16 +71,12 @@
     AStatsEvent_overwriteTimestamp(statsEvent, timestampNs);
 
     AStatsEvent_writeInt32(statsEvent, uid);
+    AStatsEvent_addBoolAnnotation(statsEvent, ANNOTATION_ID_IS_UID, true);
     AStatsEvent_writeInt32(statsEvent, data1);
     AStatsEvent_writeInt32(statsEvent, data2);
-    AStatsEvent_build(statsEvent);
-
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
 
     std::shared_ptr<LogEvent> logEvent = std::make_unique<LogEvent>(/*uid=*/0, /*pid=*/0);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent.get());
     return logEvent;
 }
 
@@ -86,16 +84,10 @@
     AStatsEvent* statsEvent = AStatsEvent_obtain();
     AStatsEvent_setAtomId(statsEvent, nonUidAtomTagId);
     AStatsEvent_overwriteTimestamp(statsEvent, timestampNs);
-
     AStatsEvent_writeInt32(statsEvent, data1);
-    AStatsEvent_build(statsEvent);
-
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
 
     std::shared_ptr<LogEvent> logEvent = std::make_unique<LogEvent>(/*uid=*/0, /*pid=*/0);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent.get());
     return logEvent;
 }
 
diff --git a/cmds/statsd/tests/log_event/LogEventQueue_test.cpp b/cmds/statsd/tests/log_event/LogEventQueue_test.cpp
index 6dc041f..a15f95b 100644
--- a/cmds/statsd/tests/log_event/LogEventQueue_test.cpp
+++ b/cmds/statsd/tests/log_event/LogEventQueue_test.cpp
@@ -21,6 +21,7 @@
 #include <thread>
 
 #include "stats_event.h"
+#include "tests/statsd_test_util.h"
 
 namespace android {
 namespace os {
@@ -37,14 +38,9 @@
     AStatsEvent* statsEvent = AStatsEvent_obtain();
     AStatsEvent_setAtomId(statsEvent, 10);
     AStatsEvent_overwriteTimestamp(statsEvent, timestampNs);
-    AStatsEvent_build(statsEvent);
-
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
 
     std::unique_ptr<LogEvent> logEvent = std::make_unique<LogEvent>(/*uid=*/0, /*pid=*/0);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent.get());
     return logEvent;
 }
 
diff --git a/cmds/statsd/tests/metrics/CountMetricProducer_test.cpp b/cmds/statsd/tests/metrics/CountMetricProducer_test.cpp
index d55996c..65f8de6 100644
--- a/cmds/statsd/tests/metrics/CountMetricProducer_test.cpp
+++ b/cmds/statsd/tests/metrics/CountMetricProducer_test.cpp
@@ -46,26 +46,17 @@
     AStatsEvent* statsEvent = AStatsEvent_obtain();
     AStatsEvent_setAtomId(statsEvent, atomId);
     AStatsEvent_overwriteTimestamp(statsEvent, timestampNs);
-    AStatsEvent_build(statsEvent);
 
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent);
 }
 
 void makeLogEvent(LogEvent* logEvent, int64_t timestampNs, int atomId, string uid) {
     AStatsEvent* statsEvent = AStatsEvent_obtain();
     AStatsEvent_setAtomId(statsEvent, atomId);
     AStatsEvent_overwriteTimestamp(statsEvent, timestampNs);
-
     AStatsEvent_writeString(statsEvent, uid.c_str());
-    AStatsEvent_build(statsEvent);
 
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent);
 }
 
 }  // namespace
diff --git a/cmds/statsd/tests/metrics/DurationMetricProducer_test.cpp b/cmds/statsd/tests/metrics/DurationMetricProducer_test.cpp
index 6143dc0..30f8159 100644
--- a/cmds/statsd/tests/metrics/DurationMetricProducer_test.cpp
+++ b/cmds/statsd/tests/metrics/DurationMetricProducer_test.cpp
@@ -26,6 +26,7 @@
 #include "src/condition/ConditionWizard.h"
 #include "src/stats_log_util.h"
 #include "stats_event.h"
+#include "tests/statsd_test_util.h"
 
 using namespace android::os::statsd;
 using namespace testing;
@@ -48,12 +49,8 @@
     AStatsEvent* statsEvent = AStatsEvent_obtain();
     AStatsEvent_setAtomId(statsEvent, atomId);
     AStatsEvent_overwriteTimestamp(statsEvent, timestampNs);
-    AStatsEvent_build(statsEvent);
 
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent);
 }
 
 }  // namespace
diff --git a/cmds/statsd/tests/metrics/EventMetricProducer_test.cpp b/cmds/statsd/tests/metrics/EventMetricProducer_test.cpp
index e58bbb7..97647a7 100644
--- a/cmds/statsd/tests/metrics/EventMetricProducer_test.cpp
+++ b/cmds/statsd/tests/metrics/EventMetricProducer_test.cpp
@@ -43,14 +43,9 @@
     AStatsEvent* statsEvent = AStatsEvent_obtain();
     AStatsEvent_setAtomId(statsEvent, atomId);
     AStatsEvent_overwriteTimestamp(statsEvent, timestampNs);
-
     AStatsEvent_writeString(statsEvent, str.c_str());
-    AStatsEvent_build(statsEvent);
 
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent);
 }
 }  // anonymous namespace
 
diff --git a/cmds/statsd/tests/metrics/GaugeMetricProducer_test.cpp b/cmds/statsd/tests/metrics/GaugeMetricProducer_test.cpp
index 2fe05a4..42d0d5d 100644
--- a/cmds/statsd/tests/metrics/GaugeMetricProducer_test.cpp
+++ b/cmds/statsd/tests/metrics/GaugeMetricProducer_test.cpp
@@ -64,14 +64,9 @@
     AStatsEvent_writeInt32(statsEvent, value1);
     AStatsEvent_writeString(statsEvent, str1.c_str());
     AStatsEvent_writeInt32(statsEvent, value2);
-    AStatsEvent_build(statsEvent);
 
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
     shared_ptr<LogEvent> logEvent = std::make_shared<LogEvent>(/*uid=*/0, /*pid=*/0);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
-
+    parseStatsEventToLogEvent(statsEvent, logEvent.get());
     return logEvent;
 }
 }  // anonymous namespace
diff --git a/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp b/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp
index b623a09..009e49a 100644
--- a/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp
+++ b/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp
@@ -611,7 +611,7 @@
 
     vector<shared_ptr<LogEvent>> allData;
     allData.clear();
-    allData.push_back(CreateTwoValueLogEvent(tagId, bucket2StartTimeNs + 1, 1, 110));
+    allData.push_back(CreateRepeatedValueLogEvent(tagId, bucket2StartTimeNs + 1, 110));
     valueProducer->onDataPulled(allData, /** succeed */ true, bucket2StartTimeNs);
     assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {10}, {bucketSizeNs - 8});
 
@@ -656,7 +656,7 @@
     valueProducer.prepareFirstBucket();
 
     LogEvent event1(/*uid=*/0, /*pid=*/0);
-    CreateTwoValueLogEvent(&event1, tagId, bucketStartTimeNs + 10, 1, 10);
+    CreateRepeatedValueLogEvent(&event1, tagId, bucketStartTimeNs + 10, 10);
     valueProducer.onMatchedLogEvent(1 /*log matcher index*/, event1);
     EXPECT_EQ(1UL, valueProducer.mCurrentSlicedBucket.size());
 
@@ -665,14 +665,14 @@
     EXPECT_EQ(bucketStartTimeNs + 150, valueProducer.mCurrentBucketStartTimeNs);
 
     LogEvent event2(/*uid=*/0, /*pid=*/0);
-    CreateTwoValueLogEvent(&event2, tagId, bucketStartTimeNs + 59 * NS_PER_SEC, 1, 10);
+    CreateRepeatedValueLogEvent(&event2, tagId, bucketStartTimeNs + 59 * NS_PER_SEC, 10);
     valueProducer.onMatchedLogEvent(1 /*log matcher index*/, event2);
     EXPECT_EQ(1UL, valueProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
     EXPECT_EQ(bucketStartTimeNs + 150, valueProducer.mCurrentBucketStartTimeNs);
 
     // Next value should create a new bucket.
     LogEvent event3(/*uid=*/0, /*pid=*/0);
-    CreateTwoValueLogEvent(&event3, tagId, bucketStartTimeNs + 65 * NS_PER_SEC, 1, 10);
+    CreateRepeatedValueLogEvent(&event3, tagId, bucketStartTimeNs + 65 * NS_PER_SEC, 10);
     valueProducer.onMatchedLogEvent(1 /*log matcher index*/, event3);
     EXPECT_EQ(2UL, valueProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
     EXPECT_EQ(bucketStartTimeNs + bucketSizeNs, valueProducer.mCurrentBucketStartTimeNs);
@@ -812,10 +812,10 @@
     valueProducer.prepareFirstBucket();
 
     LogEvent event1(/*uid=*/0, /*pid=*/0);
-    CreateTwoValueLogEvent(&event1, tagId, bucketStartTimeNs + 10, 1, 10);
+    CreateRepeatedValueLogEvent(&event1, tagId, bucketStartTimeNs + 10, 10);
 
     LogEvent event2(/*uid=*/0, /*pid=*/0);
-    CreateTwoValueLogEvent(&event2, tagId, bucketStartTimeNs + 20, 1, 20);
+    CreateRepeatedValueLogEvent(&event2, tagId, bucketStartTimeNs + 20, 20);
 
     valueProducer.onMatchedLogEvent(1 /*log matcher index*/, event1);
     // has one slice
@@ -856,7 +856,7 @@
     valueProducer.mCondition = ConditionState::kFalse;
 
     LogEvent event1(/*uid=*/0, /*pid=*/0);
-    CreateTwoValueLogEvent(&event1, tagId, bucketStartTimeNs + 10, 1, 10);
+    CreateRepeatedValueLogEvent(&event1, tagId, bucketStartTimeNs + 10, 10);
     valueProducer.onMatchedLogEvent(1 /*log matcher index*/, event1);
     // has 1 slice
     EXPECT_EQ(0UL, valueProducer.mCurrentSlicedBucket.size());
@@ -864,7 +864,7 @@
     valueProducer.onConditionChangedLocked(true, bucketStartTimeNs + 15);
 
     LogEvent event2(/*uid=*/0, /*pid=*/0);
-    CreateTwoValueLogEvent(&event2, tagId, bucketStartTimeNs + 20, 1, 20);
+    CreateRepeatedValueLogEvent(&event2, tagId, bucketStartTimeNs + 20, 20);
     valueProducer.onMatchedLogEvent(1 /*log matcher index*/, event2);
 
     // has one slice
@@ -875,7 +875,7 @@
     EXPECT_EQ(20, curInterval.value.long_value);
 
     LogEvent event3(/*uid=*/0, /*pid=*/0);
-    CreateTwoValueLogEvent(&event3, tagId, bucketStartTimeNs + 30, 1, 30);
+    CreateRepeatedValueLogEvent(&event3, tagId, bucketStartTimeNs + 30, 30);
     valueProducer.onMatchedLogEvent(1 /*log matcher index*/, event3);
 
     // has one slice
@@ -886,7 +886,7 @@
     valueProducer.onConditionChangedLocked(false, bucketStartTimeNs + 35);
 
     LogEvent event4(/*uid=*/0, /*pid=*/0);
-    CreateTwoValueLogEvent(&event4, tagId, bucketStartTimeNs + 40, 1, 40);
+    CreateRepeatedValueLogEvent(&event4, tagId, bucketStartTimeNs + 40, 40);
     valueProducer.onMatchedLogEvent(1 /*log matcher index*/, event4);
 
     // has one slice
@@ -1195,10 +1195,10 @@
     valueProducer.prepareFirstBucket();
 
     LogEvent event1(/*uid=*/0, /*pid=*/0);
-    CreateTwoValueLogEvent(&event1, tagId, bucketStartTimeNs + 10, 1, 10);
+    CreateRepeatedValueLogEvent(&event1, tagId, bucketStartTimeNs + 10, 10);
 
     LogEvent event2(/*uid=*/0, /*pid=*/0);
-    CreateTwoValueLogEvent(&event2, tagId, bucketStartTimeNs + 20, 1, 20);
+    CreateRepeatedValueLogEvent(&event2, tagId, bucketStartTimeNs + 20, 20);
 
     valueProducer.onMatchedLogEvent(1 /*log matcher index*/, event1);
     // has one slice
@@ -1238,10 +1238,10 @@
     valueProducer.prepareFirstBucket();
 
     LogEvent event1(/*uid=*/0, /*pid=*/0);
-    CreateTwoValueLogEvent(&event1, tagId, bucketStartTimeNs + 10, 1, 10);
+    CreateRepeatedValueLogEvent(&event1, tagId, bucketStartTimeNs + 10, 10);
 
     LogEvent event2(/*uid=*/0, /*pid=*/0);
-    CreateTwoValueLogEvent(&event2, tagId, bucketStartTimeNs + 20, 1, 20);
+    CreateRepeatedValueLogEvent(&event2, tagId, bucketStartTimeNs + 20, 20);
     valueProducer.onMatchedLogEvent(1 /*log matcher index*/, event1);
 
     // has one slice
@@ -1283,10 +1283,10 @@
     valueProducer.prepareFirstBucket();
 
     LogEvent event1(/*uid=*/0, /*pid=*/0);
-    CreateTwoValueLogEvent(&event1, tagId, bucketStartTimeNs + 10, 1, 10);
+    CreateRepeatedValueLogEvent(&event1, tagId, bucketStartTimeNs + 10, 10);
 
     LogEvent event2(/*uid=*/0, /*pid=*/0);
-    CreateTwoValueLogEvent(&event2, tagId, bucketStartTimeNs + 20, 1, 15);
+    CreateRepeatedValueLogEvent(&event2, tagId, bucketStartTimeNs + 20, 15);
     valueProducer.onMatchedLogEvent(1 /*log matcher index*/, event1);
     // has one slice
     EXPECT_EQ(1UL, valueProducer.mCurrentSlicedBucket.size());
@@ -1331,10 +1331,10 @@
     valueProducer.prepareFirstBucket();
 
     LogEvent event1(/*uid=*/0, /*pid=*/0);
-    CreateTwoValueLogEvent(&event1, tagId, bucketStartTimeNs + 10, 1, 10);
+    CreateRepeatedValueLogEvent(&event1, tagId, bucketStartTimeNs + 10, 10);
 
     LogEvent event2(/*uid=*/0, /*pid=*/0);
-    CreateTwoValueLogEvent(&event2, tagId, bucketStartTimeNs + 20, 1, 15);
+    CreateRepeatedValueLogEvent(&event2, tagId, bucketStartTimeNs + 20, 15);
     valueProducer.onMatchedLogEvent(1 /*log matcher index*/, event1);
     // has one slice
     EXPECT_EQ(1UL, valueProducer.mCurrentSlicedBucket.size());
@@ -1374,10 +1374,10 @@
     valueProducer.prepareFirstBucket();
 
     LogEvent event1(/*uid=*/0, /*pid=*/0);
-    CreateTwoValueLogEvent(&event1, tagId, bucketStartTimeNs + 10, 1, 10);
+    CreateRepeatedValueLogEvent(&event1, tagId, bucketStartTimeNs + 10, 10);
 
     LogEvent event2(/*uid=*/0, /*pid=*/0);
-    CreateTwoValueLogEvent(&event2, tagId, bucketStartTimeNs + 15, 1, 15);
+    CreateRepeatedValueLogEvent(&event2, tagId, bucketStartTimeNs + 15, 15);
     valueProducer.onMatchedLogEvent(1 /*log matcher index*/, event1);
     // has one slice
     EXPECT_EQ(1UL, valueProducer.mCurrentSlicedBucket.size());
@@ -1398,7 +1398,7 @@
 
     // no change in data.
     LogEvent event3(/*uid=*/0, /*pid=*/0);
-    CreateTwoValueLogEvent(&event3, tagId, bucket2StartTimeNs + 10, 1, 15);
+    CreateRepeatedValueLogEvent(&event3, tagId, bucket2StartTimeNs + 10, 15);
     valueProducer.onMatchedLogEvent(1 /*log matcher index*/, event3);
     EXPECT_EQ(1UL, valueProducer.mCurrentSlicedBucket.size());
     curInterval = valueProducer.mCurrentSlicedBucket.begin()->second[0];
@@ -1408,7 +1408,7 @@
     EXPECT_EQ(true, curInterval.hasValue);
 
     LogEvent event4(/*uid=*/0, /*pid=*/0);
-    CreateTwoValueLogEvent(&event4, tagId, bucket2StartTimeNs + 15, 1, 15);
+    CreateRepeatedValueLogEvent(&event4, tagId, bucket2StartTimeNs + 15, 15);
     valueProducer.onMatchedLogEvent(1 /*log matcher index*/, event4);
     EXPECT_EQ(1UL, valueProducer.mCurrentSlicedBucket.size());
     curInterval = valueProducer.mCurrentSlicedBucket.begin()->second[0];
@@ -2166,7 +2166,7 @@
     // Bucket start.
     vector<shared_ptr<LogEvent>> allData;
     allData.clear();
-    allData.push_back(CreateTwoValueLogEvent(tagId, bucketStartTimeNs + 1, 1, 110));
+    allData.push_back(CreateRepeatedValueLogEvent(tagId, bucketStartTimeNs + 1, 110));
     valueProducer->onDataPulled(allData, /** succeed */ false, bucketStartTimeNs);
 
     valueProducer->onConditionChanged(false, bucketStartTimeNs + 2);
@@ -2174,7 +2174,7 @@
 
     // Bucket end.
     allData.clear();
-    allData.push_back(CreateTwoValueLogEvent(tagId, bucket2StartTimeNs + 1, 1, 140));
+    allData.push_back(CreateRepeatedValueLogEvent(tagId, bucket2StartTimeNs + 1, 140));
     valueProducer->onDataPulled(allData, /** succeed */ true, bucket2StartTimeNs);
 
     valueProducer->flushIfNeededLocked(bucket2StartTimeNs + 1);
diff --git a/cmds/statsd/tests/shell/ShellSubscriber_test.cpp b/cmds/statsd/tests/shell/ShellSubscriber_test.cpp
index ac3ad69..7b952d7 100644
--- a/cmds/statsd/tests/shell/ShellSubscriber_test.cpp
+++ b/cmds/statsd/tests/shell/ShellSubscriber_test.cpp
@@ -171,13 +171,9 @@
     AStatsEvent_overwriteTimestamp(statsEvent, 1111L);
     AStatsEvent_writeInt32(statsEvent, uid);
     AStatsEvent_writeInt64(statsEvent, timeMillis);
-    AStatsEvent_build(statsEvent);
-
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
 
     std::shared_ptr<LogEvent> logEvent = std::make_shared<LogEvent>(/*uid=*/0, /*pid=*/0);
-    logEvent->parseBuffer(buf, size);
+    parseStatsEventToLogEvent(statsEvent, logEvent.get());
     return logEvent;
 }
 
diff --git a/cmds/statsd/tests/state/StateTracker_test.cpp b/cmds/statsd/tests/state/StateTracker_test.cpp
index a5b8e1c..78c80bc 100644
--- a/cmds/statsd/tests/state/StateTracker_test.cpp
+++ b/cmds/statsd/tests/state/StateTracker_test.cpp
@@ -62,7 +62,7 @@
 
 // START: build event functions.
 // Incorrect event - missing fields
-std::shared_ptr<LogEvent> buildIncorrectOverlayEvent(int uid, const std::string& packageName,
+std::unique_ptr<LogEvent> buildIncorrectOverlayEvent(int uid, const std::string& packageName,
                                                      int state) {
     AStatsEvent* statsEvent = AStatsEvent_obtain();
     AStatsEvent_setAtomId(statsEvent, util::OVERLAY_STATE_CHANGED);
@@ -72,14 +72,9 @@
     AStatsEvent_writeString(statsEvent, packageName.c_str());
     // Missing field 3 - using_alert_window.
     AStatsEvent_writeInt32(statsEvent, state);
-    AStatsEvent_build(statsEvent);
-
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
 
     std::unique_ptr<LogEvent> logEvent = std::make_unique<LogEvent>(/*uid=*/0, /*pid=*/0);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent.get());
     return logEvent;
 }
 
@@ -93,14 +88,9 @@
     AStatsEvent_writeString(statsEvent, packageName.c_str());
     AStatsEvent_writeInt32(statsEvent, true);       // using_alert_window
     AStatsEvent_writeString(statsEvent, "string");  // exclusive state: string instead of int
-    AStatsEvent_build(statsEvent);
-
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
 
     std::unique_ptr<LogEvent> logEvent = std::make_unique<LogEvent>(/*uid=*/0, /*pid=*/0);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent.get());
     return logEvent;
 }
 // END: build event functions.
diff --git a/cmds/statsd/tests/statsd_test_util.cpp b/cmds/statsd/tests/statsd_test_util.cpp
index 7d765d3..ed3cf5b 100644
--- a/cmds/statsd/tests/statsd_test_util.cpp
+++ b/cmds/statsd/tests/statsd_test_util.cpp
@@ -507,23 +507,26 @@
 }
 // END: get primary key functions
 
-shared_ptr<LogEvent> CreateTwoValueLogEvent(int atomId, int64_t eventTimeNs, int32_t value1,
-                                            int32_t value2) {
-    AStatsEvent* statsEvent = AStatsEvent_obtain();
-    AStatsEvent_setAtomId(statsEvent, atomId);
-    AStatsEvent_overwriteTimestamp(statsEvent, eventTimeNs);
+void writeAttribution(AStatsEvent* statsEvent, const vector<int>& attributionUids,
+                      const vector<string>& attributionTags) {
+    vector<const char*> cTags(attributionTags.size());
+    for (int i = 0; i < cTags.size(); i++) {
+        cTags[i] = attributionTags[i].c_str();
+    }
 
-    AStatsEvent_writeInt32(statsEvent, value1);
-    AStatsEvent_writeInt32(statsEvent, value2);
+    AStatsEvent_writeAttributionChain(statsEvent,
+                                      reinterpret_cast<const uint32_t*>(attributionUids.data()),
+                                      cTags.data(), attributionUids.size());
+}
+
+void parseStatsEventToLogEvent(AStatsEvent* statsEvent, LogEvent* logEvent) {
     AStatsEvent_build(statsEvent);
 
     size_t size;
     uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
-    shared_ptr<LogEvent> logEvent = std::make_shared<LogEvent>(/*uid=*/0, /*pid=*/0);
     logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
 
-    return logEvent;
+    AStatsEvent_release(statsEvent);
 }
 
 void CreateTwoValueLogEvent(LogEvent* logEvent, int atomId, int64_t eventTimeNs, int32_t value1,
@@ -534,31 +537,14 @@
 
     AStatsEvent_writeInt32(statsEvent, value1);
     AStatsEvent_writeInt32(statsEvent, value2);
-    AStatsEvent_build(statsEvent);
 
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent);
 }
 
-shared_ptr<LogEvent> CreateThreeValueLogEvent(int atomId, int64_t eventTimeNs, int32_t value1,
-                                              int32_t value2, int32_t value3) {
-    AStatsEvent* statsEvent = AStatsEvent_obtain();
-    AStatsEvent_setAtomId(statsEvent, atomId);
-    AStatsEvent_overwriteTimestamp(statsEvent, eventTimeNs);
-
-    AStatsEvent_writeInt32(statsEvent, value1);
-    AStatsEvent_writeInt32(statsEvent, value2);
-    AStatsEvent_writeInt32(statsEvent, value3);
-    AStatsEvent_build(statsEvent);
-
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
+shared_ptr<LogEvent> CreateTwoValueLogEvent(int atomId, int64_t eventTimeNs, int32_t value1,
+                                            int32_t value2) {
     shared_ptr<LogEvent> logEvent = std::make_shared<LogEvent>(/*uid=*/0, /*pid=*/0);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
-
+    CreateTwoValueLogEvent(logEvent.get(), atomId, eventTimeNs, value1, value2);
     return logEvent;
 }
 
@@ -571,29 +557,14 @@
     AStatsEvent_writeInt32(statsEvent, value1);
     AStatsEvent_writeInt32(statsEvent, value2);
     AStatsEvent_writeInt32(statsEvent, value3);
-    AStatsEvent_build(statsEvent);
 
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent);
 }
 
-shared_ptr<LogEvent> CreateRepeatedValueLogEvent(int atomId, int64_t eventTimeNs, int32_t value) {
-    AStatsEvent* statsEvent = AStatsEvent_obtain();
-    AStatsEvent_setAtomId(statsEvent, atomId);
-    AStatsEvent_overwriteTimestamp(statsEvent, eventTimeNs);
-
-    AStatsEvent_writeInt32(statsEvent, value);
-    AStatsEvent_writeInt32(statsEvent, value);
-    AStatsEvent_build(statsEvent);
-
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
+shared_ptr<LogEvent> CreateThreeValueLogEvent(int atomId, int64_t eventTimeNs, int32_t value1,
+                                              int32_t value2, int32_t value3) {
     shared_ptr<LogEvent> logEvent = std::make_shared<LogEvent>(/*uid=*/0, /*pid=*/0);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
-
+    CreateThreeValueLogEvent(logEvent.get(), atomId, eventTimeNs, value1, value2, value3);
     return logEvent;
 }
 
@@ -605,26 +576,13 @@
 
     AStatsEvent_writeInt32(statsEvent, value);
     AStatsEvent_writeInt32(statsEvent, value);
-    AStatsEvent_build(statsEvent);
 
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent);
 }
 
-shared_ptr<LogEvent> CreateNoValuesLogEvent(int atomId, int64_t eventTimeNs) {
-    AStatsEvent* statsEvent = AStatsEvent_obtain();
-    AStatsEvent_setAtomId(statsEvent, atomId);
-    AStatsEvent_overwriteTimestamp(statsEvent, eventTimeNs);
-    AStatsEvent_build(statsEvent);
-
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
+shared_ptr<LogEvent> CreateRepeatedValueLogEvent(int atomId, int64_t eventTimeNs, int32_t value) {
     shared_ptr<LogEvent> logEvent = std::make_shared<LogEvent>(/*uid=*/0, /*pid=*/0);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
-
+    CreateRepeatedValueLogEvent(logEvent.get(), atomId, eventTimeNs, value);
     return logEvent;
 }
 
@@ -632,12 +590,14 @@
     AStatsEvent* statsEvent = AStatsEvent_obtain();
     AStatsEvent_setAtomId(statsEvent, atomId);
     AStatsEvent_overwriteTimestamp(statsEvent, eventTimeNs);
-    AStatsEvent_build(statsEvent);
 
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent);
+}
+
+shared_ptr<LogEvent> CreateNoValuesLogEvent(int atomId, int64_t eventTimeNs) {
+    shared_ptr<LogEvent> logEvent = std::make_shared<LogEvent>(/*uid=*/0, /*pid=*/0);
+    CreateNoValuesLogEvent(logEvent.get(), atomId, eventTimeNs);
+    return logEvent;
 }
 
 std::unique_ptr<LogEvent> CreateScreenStateChangedEvent(
@@ -645,16 +605,10 @@
     AStatsEvent* statsEvent = AStatsEvent_obtain();
     AStatsEvent_setAtomId(statsEvent, util::SCREEN_STATE_CHANGED);
     AStatsEvent_overwriteTimestamp(statsEvent, timestampNs);
-
     AStatsEvent_writeInt32(statsEvent, state);
-    AStatsEvent_build(statsEvent);
-
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
 
     std::unique_ptr<LogEvent> logEvent = std::make_unique<LogEvent>(/*uid=*/0, /*pid=*/0);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent.get());
     return logEvent;
 }
 
@@ -662,16 +616,10 @@
     AStatsEvent* statsEvent = AStatsEvent_obtain();
     AStatsEvent_setAtomId(statsEvent, util::BATTERY_SAVER_MODE_STATE_CHANGED);
     AStatsEvent_overwriteTimestamp(statsEvent, timestampNs);
-
     AStatsEvent_writeInt32(statsEvent, BatterySaverModeStateChanged::ON);
-    AStatsEvent_build(statsEvent);
-
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
 
     std::unique_ptr<LogEvent> logEvent = std::make_unique<LogEvent>(/*uid=*/0, /*pid=*/0);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent.get());
     return logEvent;
 }
 
@@ -679,16 +627,10 @@
     AStatsEvent* statsEvent = AStatsEvent_obtain();
     AStatsEvent_setAtomId(statsEvent, util::BATTERY_SAVER_MODE_STATE_CHANGED);
     AStatsEvent_overwriteTimestamp(statsEvent, timestampNs);
-
     AStatsEvent_writeInt32(statsEvent, BatterySaverModeStateChanged::OFF);
-    AStatsEvent_build(statsEvent);
-
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
 
     std::unique_ptr<LogEvent> logEvent = std::make_unique<LogEvent>(/*uid=*/0, /*pid=*/0);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent.get());
     return logEvent;
 }
 
@@ -696,16 +638,10 @@
     AStatsEvent* statsEvent = AStatsEvent_obtain();
     AStatsEvent_setAtomId(statsEvent, util::PLUGGED_STATE_CHANGED);
     AStatsEvent_overwriteTimestamp(statsEvent, timestampNs);
-
     AStatsEvent_writeInt32(statsEvent, state);
-    AStatsEvent_build(statsEvent);
-
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
 
     std::unique_ptr<LogEvent> logEvent = std::make_unique<LogEvent>(/*uid=*/0, /*pid=*/0);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent.get());
     return logEvent;
 }
 
@@ -713,16 +649,10 @@
     AStatsEvent* statsEvent = AStatsEvent_obtain();
     AStatsEvent_setAtomId(statsEvent, util::SCREEN_BRIGHTNESS_CHANGED);
     AStatsEvent_overwriteTimestamp(statsEvent, timestampNs);
-
     AStatsEvent_writeInt32(statsEvent, level);
-    AStatsEvent_build(statsEvent);
-
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
 
     std::unique_ptr<LogEvent> logEvent = std::make_unique<LogEvent>(/*uid=*/0, /*pid=*/0);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent.get());
     return logEvent;
 }
 
@@ -733,24 +663,12 @@
     AStatsEvent_setAtomId(statsEvent, util::SCHEDULED_JOB_STATE_CHANGED);
     AStatsEvent_overwriteTimestamp(statsEvent, timestampNs);
 
-    vector<const char*> cTags(attributionTags.size());
-    for (int i = 0; i < cTags.size(); i++) {
-        cTags[i] = attributionTags[i].c_str();
-    }
-
-    AStatsEvent_writeAttributionChain(statsEvent,
-                                      reinterpret_cast<const uint32_t*>(attributionUids.data()),
-                                      cTags.data(), attributionUids.size());
+    writeAttribution(statsEvent, attributionUids, attributionTags);
     AStatsEvent_writeString(statsEvent, jobName.c_str());
     AStatsEvent_writeInt32(statsEvent, state);
-    AStatsEvent_build(statsEvent);
-
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
 
     std::unique_ptr<LogEvent> logEvent = std::make_unique<LogEvent>(/*uid=*/0, /*pid=*/0);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent.get());
     return logEvent;
 }
 
@@ -780,25 +698,13 @@
     AStatsEvent_setAtomId(statsEvent, util::WAKELOCK_STATE_CHANGED);
     AStatsEvent_overwriteTimestamp(statsEvent, timestampNs);
 
-    vector<const char*> cTags(attributionTags.size());
-    for (int i = 0; i < cTags.size(); i++) {
-        cTags[i] = attributionTags[i].c_str();
-    }
-
-    AStatsEvent_writeAttributionChain(statsEvent,
-                                      reinterpret_cast<const uint32_t*>(attributionUids.data()),
-                                      cTags.data(), attributionUids.size());
+    writeAttribution(statsEvent, attributionUids, attributionTags);
     AStatsEvent_writeInt32(statsEvent, android::os::WakeLockLevelEnum::PARTIAL_WAKE_LOCK);
     AStatsEvent_writeString(statsEvent, wakelockName.c_str());
     AStatsEvent_writeInt32(statsEvent, state);
-    AStatsEvent_build(statsEvent);
-
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
 
     std::unique_ptr<LogEvent> logEvent = std::make_unique<LogEvent>(/*uid=*/0, /*pid=*/0);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent.get());
     return logEvent;
 }
 
@@ -828,14 +734,9 @@
     AStatsEvent_writeString(statsEvent, "pkg_name");
     AStatsEvent_writeString(statsEvent, "class_name");
     AStatsEvent_writeInt32(statsEvent, state);
-    AStatsEvent_build(statsEvent);
-
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
 
     std::unique_ptr<LogEvent> logEvent = std::make_unique<LogEvent>(/*uid=*/0, /*pid=*/0);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent.get());
     return logEvent;
 }
 
@@ -858,24 +759,12 @@
     AStatsEvent_setAtomId(statsEvent, util::SYNC_STATE_CHANGED);
     AStatsEvent_overwriteTimestamp(statsEvent, timestampNs);
 
-    vector<const char*> cTags(attributionTags.size());
-    for (int i = 0; i < cTags.size(); i++) {
-        cTags[i] = attributionTags[i].c_str();
-    }
-
-    AStatsEvent_writeAttributionChain(statsEvent,
-                                      reinterpret_cast<const uint32_t*>(attributionUids.data()),
-                                      cTags.data(), attributionUids.size());
+    writeAttribution(statsEvent, attributionUids, attributionTags);
     AStatsEvent_writeString(statsEvent, name.c_str());
     AStatsEvent_writeInt32(statsEvent, state);
-    AStatsEvent_build(statsEvent);
-
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
 
     std::unique_ptr<LogEvent> logEvent = std::make_unique<LogEvent>(/*uid=*/0, /*pid=*/0);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent.get());
     return logEvent;
 }
 
@@ -904,14 +793,9 @@
     AStatsEvent_writeInt32(statsEvent, uid);
     AStatsEvent_writeString(statsEvent, "");
     AStatsEvent_writeInt32(statsEvent, state);
-    AStatsEvent_build(statsEvent);
-
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
 
     std::unique_ptr<LogEvent> logEvent = std::make_unique<LogEvent>(/*uid=*/0, /*pid=*/0);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent.get());
     return logEvent;
 }
 
@@ -928,14 +812,9 @@
     AStatsEvent_writeInt32(statsEvent, uid);
     AStatsEvent_writeString(statsEvent, "eventType");
     AStatsEvent_writeString(statsEvent, "processName");
-    AStatsEvent_build(statsEvent);
-
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
 
     std::unique_ptr<LogEvent> logEvent = std::make_unique<LogEvent>(/*uid=*/0, /*pid=*/0);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent.get());
     return logEvent;
 }
 
@@ -948,14 +827,9 @@
     AStatsEvent_writeInt32(statsEvent, hostUid);
     AStatsEvent_writeInt32(statsEvent, isolatedUid);
     AStatsEvent_writeInt32(statsEvent, is_create);
-    AStatsEvent_build(statsEvent);
-
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
 
     std::unique_ptr<LogEvent> logEvent = std::make_unique<LogEvent>(/*uid=*/0, /*pid=*/0);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent.get());
     return logEvent;
 }
 
@@ -967,14 +841,9 @@
 
     AStatsEvent_writeInt32(statsEvent, uid);
     AStatsEvent_writeInt32(statsEvent, state);
-    AStatsEvent_build(statsEvent);
-
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
 
     std::unique_ptr<LogEvent> logEvent = std::make_unique<LogEvent>(/*uid=*/0, /*pid=*/0);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent.get());
     return logEvent;
 }
 
@@ -988,26 +857,14 @@
     AStatsEvent_setAtomId(statsEvent, util::BLE_SCAN_STATE_CHANGED);
     AStatsEvent_overwriteTimestamp(statsEvent, timestampNs);
 
-    vector<const char*> cTags(attributionTags.size());
-    for (int i = 0; i < cTags.size(); i++) {
-        cTags[i] = attributionTags[i].c_str();
-    }
-
-    AStatsEvent_writeAttributionChain(statsEvent,
-                                      reinterpret_cast<const uint32_t*>(attributionUids.data()),
-                                      cTags.data(), attributionUids.size());
+    writeAttribution(statsEvent, attributionUids, attributionTags);
     AStatsEvent_writeInt32(statsEvent, state);
     AStatsEvent_writeInt32(statsEvent, filtered);       // filtered
     AStatsEvent_writeInt32(statsEvent, firstMatch);     // first match
     AStatsEvent_writeInt32(statsEvent, opportunistic);  // opportunistic
-    AStatsEvent_build(statsEvent);
-
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
 
     std::unique_ptr<LogEvent> logEvent = std::make_unique<LogEvent>(/*uid=*/0, /*pid=*/0);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent.get());
     return logEvent;
 }
 
@@ -1023,14 +880,9 @@
     AStatsEvent_writeString(statsEvent, packageName.c_str());
     AStatsEvent_writeInt32(statsEvent, usingAlertWindow);
     AStatsEvent_writeInt32(statsEvent, state);
-    AStatsEvent_build(statsEvent);
-
-    size_t size;
-    uint8_t* buf = AStatsEvent_getBuffer(statsEvent, &size);
 
     std::unique_ptr<LogEvent> logEvent = std::make_unique<LogEvent>(/*uid=*/0, /*pid=*/0);
-    logEvent->parseBuffer(buf, size);
-    AStatsEvent_release(statsEvent);
+    parseStatsEventToLogEvent(statsEvent, logEvent.get());
     return logEvent;
 }
 
diff --git a/cmds/statsd/tests/statsd_test_util.h b/cmds/statsd/tests/statsd_test_util.h
index f24705a..d6ea77eb 100644
--- a/cmds/statsd/tests/statsd_test_util.h
+++ b/cmds/statsd/tests/statsd_test_util.h
@@ -25,6 +25,7 @@
 #include "src/hash.h"
 #include "src/logd/LogEvent.h"
 #include "src/stats_log_util.h"
+#include "stats_event.h"
 #include "statslog_statsdtest.h"
 
 namespace android {
@@ -189,6 +190,12 @@
 void getPartialWakelockKey(int uid, HashableDimensionKey* key);
 // END: get primary key functions
 
+void writeAttribution(AStatsEvent* statsEvent, const vector<int>& attributionUids,
+                      const vector<string>& attributionTags);
+
+// Builds statsEvent to get buffer that is parsed into logEvent then releases statsEvent.
+void parseStatsEventToLogEvent(AStatsEvent* statsEvent, LogEvent* logEvent);
+
 shared_ptr<LogEvent> CreateTwoValueLogEvent(int atomId, int64_t eventTimeNs, int32_t value1,
                                             int32_t value2);
 
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 46b06fb..3a708a6 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -1395,6 +1395,7 @@
     public static final String OPSTR_QUERY_ALL_PACKAGES = "android:query_all_packages";
     /** @hide Access all external storage */
     @SystemApi
+    @TestApi
     public static final String OPSTR_MANAGE_EXTERNAL_STORAGE =
             "android:manage_external_storage";
 
diff --git a/core/java/android/app/IWindowToken.aidl b/core/java/android/app/IWindowToken.aidl
index 8ea881f..3627b0f 100644
--- a/core/java/android/app/IWindowToken.aidl
+++ b/core/java/android/app/IWindowToken.aidl
@@ -30,4 +30,6 @@
  */
 oneway interface IWindowToken {
     void onConfigurationChanged(in Configuration newConfig, int newDisplayId);
+
+    void onWindowTokenRemoved();
 }
diff --git a/core/java/android/app/WindowContext.java b/core/java/android/app/WindowContext.java
index 878993e..3a06c9d 100644
--- a/core/java/android/app/WindowContext.java
+++ b/core/java/android/app/WindowContext.java
@@ -28,6 +28,8 @@
 import android.view.WindowManagerGlobal;
 import android.view.WindowManagerImpl;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.lang.ref.Reference;
 
 /**
@@ -75,8 +77,6 @@
             // config back to the client.
             result = mWms.addWindowTokenWithOptions(
                     mToken, type, getDisplayId(), options, getPackageName());
-
-            // TODO(window-context): remove token with a DeathObserver
         }  catch (RemoteException e) {
             mOwnsToken = false;
             throw e.rethrowFromSystemServer();
@@ -100,6 +100,13 @@
 
     @Override
     protected void finalize() throws Throwable {
+        release();
+        super.finalize();
+    }
+
+    /** Used for test to invoke because we can't invoke finalize directly. */
+    @VisibleForTesting
+    public void release() {
         if (mOwnsToken) {
             try {
                 mWms.removeWindowToken(mToken, getDisplayId());
@@ -108,6 +115,12 @@
                 throw e.rethrowFromSystemServer();
             }
         }
-        super.finalize();
+        destroy();
+    }
+
+    void destroy() {
+        final ContextImpl impl = (ContextImpl) getBaseContext();
+        impl.scheduleFinalCleanup(getClass().getName(), "WindowContext");
+        Reference.reachabilityFence(this);
     }
 }
diff --git a/core/java/android/app/WindowTokenClient.java b/core/java/android/app/WindowTokenClient.java
index ed0179b..301960e 100644
--- a/core/java/android/app/WindowTokenClient.java
+++ b/core/java/android/app/WindowTokenClient.java
@@ -20,6 +20,9 @@
 import android.content.res.Configuration;
 import android.os.Bundle;
 import android.os.IBinder;
+import android.view.WindowManagerGlobal;
+
+import java.lang.ref.WeakReference;
 
 /**
  * Client implementation of {@link IWindowToken}. It can receive configuration change callbacks from
@@ -31,9 +34,9 @@
 public class WindowTokenClient extends IWindowToken.Stub {
     /**
      * Attached {@link Context} for this window token to update configuration and resources.
-     * Initialized by {@link #attachContext(Context)}.
+     * Initialized by {@link #attachContext(WindowContext)}.
      */
-    private Context mContext = null;
+    private WeakReference<WindowContext> mContextRef = null;
 
     private final ResourcesManager mResourcesManager = ResourcesManager.getInstance();
 
@@ -47,30 +50,46 @@
      * @param context context to be attached
      * @throws IllegalStateException if attached context has already existed.
      */
-    void attachContext(@NonNull Context context) {
-        if (mContext != null) {
+    void attachContext(@NonNull WindowContext context) {
+        if (mContextRef != null) {
             throw new IllegalStateException("Context is already attached.");
         }
-        mContext = context;
-        ContextImpl impl = ContextImpl.getImpl(mContext);
+        mContextRef = new WeakReference<>(context);
+        final ContextImpl impl = ContextImpl.getImpl(context);
         impl.setResources(impl.createWindowContextResources());
     }
 
     @Override
     public void onConfigurationChanged(Configuration newConfig, int newDisplayId) {
-        final int currentDisplayId = mContext.getDisplayId();
+        final Context context = mContextRef.get();
+        if (context == null) {
+            return;
+        }
+        final int currentDisplayId = context.getDisplayId();
         final boolean displayChanged = newDisplayId != currentDisplayId;
-        final Configuration config = new Configuration(mContext.getResources()
+        final Configuration config = new Configuration(context.getResources()
                 .getConfiguration());
         final boolean configChanged = config.isOtherSeqNewer(newConfig)
                 && config.updateFrom(newConfig) != 0;
         if (displayChanged || configChanged) {
             // TODO(ag/9789103): update resource manager logic to track non-activity tokens
-            mResourcesManager.updateResourcesForActivity(asBinder(), config, newDisplayId,
+            mResourcesManager.updateResourcesForActivity(this, config, newDisplayId,
                     displayChanged);
         }
         if (displayChanged) {
-            mContext.updateDisplay(newDisplayId);
+            context.updateDisplay(newDisplayId);
         }
     }
+
+    @Override
+    public void onWindowTokenRemoved() {
+        final WindowContext context = mContextRef.get();
+        if (context != null) {
+            context.destroy();
+            mContextRef.clear();
+        }
+        // If a secondary display is detached, release all views attached to this token.
+        WindowManagerGlobal.getInstance().closeAll(this, mContextRef.getClass().getName(),
+                "WindowContext");
+    }
 }
diff --git a/core/java/android/app/admin/DevicePolicyKeyguardService.java b/core/java/android/app/admin/DevicePolicyKeyguardService.java
index db833ec..473725f 100644
--- a/core/java/android/app/admin/DevicePolicyKeyguardService.java
+++ b/core/java/android/app/admin/DevicePolicyKeyguardService.java
@@ -16,12 +16,15 @@
 
 package android.app.admin;
 
+import android.annotation.MainThread;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.app.Service;
 import android.content.Intent;
+import android.os.Handler;
 import android.os.IBinder;
+import android.os.Looper;
 import android.os.RemoteException;
 import android.util.Log;
 import android.view.SurfaceControlViewHost;
@@ -41,27 +44,34 @@
 @SystemApi
 public class DevicePolicyKeyguardService extends Service {
     private static final String TAG = "DevicePolicyKeyguardService";
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
     private IKeyguardCallback mCallback;
 
     private final IKeyguardClient mClient = new IKeyguardClient.Stub() {
+        @MainThread
         @Override
         public void onCreateKeyguardSurface(@Nullable IBinder hostInputToken,
-                IKeyguardCallback callback) {
+                @NonNull IKeyguardCallback callback) {
             mCallback = callback;
-            SurfaceControlViewHost.SurfacePackage surfacePackage =
-                    DevicePolicyKeyguardService.this.onCreateKeyguardSurface(hostInputToken);
+            mHandler.post(() -> {
+                SurfaceControlViewHost.SurfacePackage surfacePackage =
+                        DevicePolicyKeyguardService.this.onCreateKeyguardSurface(hostInputToken);
 
-            if (mCallback != null) {
                 try {
                     mCallback.onRemoteContentReady(surfacePackage);
                 } catch (RemoteException e) {
                     Log.e(TAG, "Failed to return created SurfacePackage", e);
                 }
-            }
+            });
         }
     };
 
     @Override
+    public void onDestroy() {
+        mHandler.removeCallbacksAndMessages(null);
+    }
+
+    @Override
     @Nullable
     public final IBinder onBind(@Nullable Intent intent) {
         return mClient.asBinder();
@@ -97,6 +107,10 @@
      */
     @Nullable
     public void dismiss() {
+        if (mCallback == null) {
+            Log.w(TAG, "KeyguardCallback was unexpectedly null");
+            return;
+        }
         try {
             mCallback.onDismiss();
         } catch (RemoteException e) {
diff --git a/core/java/android/bluetooth/BluetoothAdapter.java b/core/java/android/bluetooth/BluetoothAdapter.java
index fc48e7f..f216db6 100644
--- a/core/java/android/bluetooth/BluetoothAdapter.java
+++ b/core/java/android/bluetooth/BluetoothAdapter.java
@@ -979,14 +979,17 @@
                 8, BLUETOOTH_GET_STATE_CACHE_PROPERTY) {
                 @Override
                 protected Integer recompute(Void query) {
-                    // This function must be called while holding the
-                    // mServiceLock, and with mService not null. The public
-                    // getState() method makes this guarantee.
                     try {
-                        return mService.getState();
+                        mServiceLock.readLock().lock();
+                        if (mService != null) {
+                            return mService.getState();
+                        }
                     } catch (RemoteException e) {
-                        throw e.rethrowFromSystemServer();
+                        Log.e(TAG, "", e);
+                    } finally {
+                        mServiceLock.readLock().unlock();
                     }
+                    return BluetoothAdapter.STATE_OFF;
                 }
             };
 
@@ -1013,24 +1016,7 @@
     @RequiresPermission(Manifest.permission.BLUETOOTH)
     @AdapterState
     public int getState() {
-        int state = BluetoothAdapter.STATE_OFF;
-
-        try {
-            mServiceLock.readLock().lock();
-            // The test for mService must either be outside the cache, or
-            // the cache must be invalidated when mService changes.
-            if (mService != null) {
-                state = mBluetoothGetStateCache.query(null);
-            }
-        } catch (RuntimeException e) {
-            if (e.getCause() instanceof RemoteException) {
-                Log.e(TAG, "", e.getCause());
-            } else {
-                throw e;
-            }
-        } finally {
-            mServiceLock.readLock().unlock();
-        }
+        int state = mBluetoothGetStateCache.query(null);
 
         // Consider all internal states as OFF
         if (state == BluetoothAdapter.STATE_BLE_ON || state == BluetoothAdapter.STATE_BLE_TURNING_ON
diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java
index e446f4f..0a4627d 100644
--- a/core/java/android/content/ContentResolver.java
+++ b/core/java/android/content/ContentResolver.java
@@ -687,6 +687,19 @@
     public static final int NOTIFY_DELETE = 1 << 4;
 
     /**
+     * Flag for {@link #notifyChange(Uri, ContentObserver, int)}: typically set
+     * by a {@link ContentProvider} to indicate that this notification should
+     * not be subject to any delays when dispatching to apps running in the
+     * background.
+     * <p>
+     * Using this flag may negatively impact system health and performance, and
+     * should be used sparingly.
+     *
+     * @hide
+     */
+    public static final int NOTIFY_NO_DELAY = 1 << 15;
+
+    /**
      * No exception, throttled by app standby normally.
      * @hide
      */
diff --git a/core/java/android/database/sqlite/SQLiteCantOpenDatabaseException.java b/core/java/android/database/sqlite/SQLiteCantOpenDatabaseException.java
index 6f01796..5d4b48d 100644
--- a/core/java/android/database/sqlite/SQLiteCantOpenDatabaseException.java
+++ b/core/java/android/database/sqlite/SQLiteCantOpenDatabaseException.java
@@ -22,4 +22,9 @@
     public SQLiteCantOpenDatabaseException(String error) {
         super(error);
     }
+
+    /** @hide */
+    public SQLiteCantOpenDatabaseException(String error, Throwable cause) {
+        super(error, cause);
+    }
 }
diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java
index bcb3934..f7c96a3 100644
--- a/core/java/android/database/sqlite/SQLiteConnection.java
+++ b/core/java/android/database/sqlite/SQLiteConnection.java
@@ -36,6 +36,9 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Date;
@@ -215,12 +218,31 @@
     }
 
     private void open() {
+        final String file = mConfiguration.path;
         final int cookie = mRecentOperations.beginOperation("open", null, null);
         try {
-            mConnectionPtr = nativeOpen(mConfiguration.path, mConfiguration.openFlags,
+            mConnectionPtr = nativeOpen(file, mConfiguration.openFlags,
                     mConfiguration.label,
                     NoPreloadHolder.DEBUG_SQL_STATEMENTS, NoPreloadHolder.DEBUG_SQL_TIME,
                     mConfiguration.lookasideSlotSize, mConfiguration.lookasideSlotCount);
+        } catch (SQLiteCantOpenDatabaseException e) {
+            String message = String.format("Cannot open database '%s'", file);
+
+            final Path path = FileSystems.getDefault().getPath(file);
+            final Path dir = path.getParent();
+
+            if (!Files.isDirectory(dir)) {
+                message += ": Directory " + dir + " doesn't exist";
+            } else if (!Files.exists(path)) {
+                message += ": File " + path + " doesn't exist";
+            } else if (!Files.isReadable(path)) {
+                message += ": File " + path + " is not readable";
+            } else if (Files.isDirectory(path)) {
+                message += ": Path " + path + " is a directory";
+            } else {
+                message += ": Unknown reason";
+            }
+            throw new SQLiteCantOpenDatabaseException(message, e);
         } finally {
             mRecentOperations.endOperation(cookie);
         }
diff --git a/core/java/android/hardware/camera2/CameraMetadata.java b/core/java/android/hardware/camera2/CameraMetadata.java
index 91dae66..aa75f60 100644
--- a/core/java/android/hardware/camera2/CameraMetadata.java
+++ b/core/java/android/hardware/camera2/CameraMetadata.java
@@ -17,6 +17,7 @@
 package android.hardware.camera2;
 
 import android.annotation.NonNull;
+import android.compat.annotation.UnsupportedAppUsage;
 import android.hardware.camera2.impl.CameraMetadataNative;
 import android.hardware.camera2.impl.PublicKey;
 import android.hardware.camera2.impl.SyntheticKey;
@@ -100,6 +101,8 @@
      *
      * @hide
      */
+    @UnsupportedAppUsage(publicAlternatives = "This method is exposed for native "
+                        + "{@code ACameraMetadata_fromCameraMetadata} in {@code libcamera2ndk}.")
     public long getNativeMetadataPtr() {
         if (mNativeInstance == null) {
             return 0;
diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
index 84ac90b..b0bacb9 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -124,13 +124,20 @@
      * @param type Window type to be used with this token.
      * @param options A bundle used to pass window-related options.
      * @param displayId The ID of the display where this token should be added.
-     * @param packageName The name of package to request to add window token.
+     * @param packageName The name of package to request to add window token. Could be {@code null}
+     *                    if callers holds the MANAGE_APP_TOKENS permission.
      * @return {@link WindowManagerGlobal#ADD_OKAY} if the addition was successful, an error code
      *         otherwise.
      */
     int addWindowTokenWithOptions(IBinder token, int type, int displayId, in Bundle options,
             String packageName);
     void addWindowToken(IBinder token, int type, int displayId);
+    /**
+     * Remove window token on a specific display.
+     *
+     * @param token Token to be removed
+     * @displayId The ID of the display where this token should be removed.
+     */
     void removeWindowToken(IBinder token, int displayId);
     void prepareAppTransition(int transit, boolean alwaysKeepCurrent);
 
@@ -735,4 +742,11 @@
      * Called to show global actions.
      */
     void showGlobalActions();
+
+    /**
+     * Sets layer tracing flags for SurfaceFlingerTrace. 
+     *
+     * @param flags see definition in SurfaceTracing.cpp
+     */
+    void setLayerTracingFlags(int flags);
 }
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index ab65e3a..1086774 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -2888,7 +2888,8 @@
     /**
      * Acquire a frame rate flexibility token, which allows surface flinger to freely switch display
      * frame rates. This is used by CTS tests to put the device in a consistent state. See
-     * ISurfaceComposer::acquireFrameRateFlexibilityToken().
+     * ISurfaceComposer::acquireFrameRateFlexibilityToken(). The caller must have the
+     * ACCESS_SURFACE_FLINGER permission, or else the call will fail, returning 0.
      * @hide
      */
     @TestApi
diff --git a/core/java/android/view/textclassifier/ConversationActions.java b/core/java/android/view/textclassifier/ConversationActions.java
index 842ba29..6ad5cb9 100644
--- a/core/java/android/view/textclassifier/ConversationActions.java
+++ b/core/java/android/view/textclassifier/ConversationActions.java
@@ -27,8 +27,6 @@
 import android.os.Parcelable;
 import android.text.SpannedString;
 
-import com.android.internal.util.Preconditions;
-
 import java.lang.annotation.Retention;
 import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
@@ -491,7 +489,11 @@
              */
             @NonNull
             public Builder setMaxSuggestions(@IntRange(from = -1) int maxSuggestions) {
-                mMaxSuggestions = Preconditions.checkArgumentNonnegative(maxSuggestions);
+                if (maxSuggestions < -1) {
+                    throw new IllegalArgumentException("maxSuggestions has to be greater than or "
+                            + "equal to -1.");
+                }
+                mMaxSuggestions = maxSuggestions;
                 return this;
             }
 
diff --git a/core/java/android/window/VirtualDisplayTaskEmbedder.java b/core/java/android/window/VirtualDisplayTaskEmbedder.java
index 1afbfeb..0f26d5d 100644
--- a/core/java/android/window/VirtualDisplayTaskEmbedder.java
+++ b/core/java/android/window/VirtualDisplayTaskEmbedder.java
@@ -16,7 +16,6 @@
 
 package android.window;
 
-import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC;
@@ -247,7 +246,6 @@
     protected ActivityOptions prepareActivityOptions(ActivityOptions options) {
         options = super.prepareActivityOptions(options);
         options.setLaunchDisplayId(getDisplayId());
-        options.setLaunchWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
         return options;
     }
 
diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java
index 71cf5ca..b6c58e1 100644
--- a/core/java/com/android/internal/policy/DecorView.java
+++ b/core/java/com/android/internal/policy/DecorView.java
@@ -68,7 +68,6 @@
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.InsetDrawable;
 import android.graphics.drawable.LayerDrawable;
-import android.os.Build.VERSION_CODES;
 import android.util.DisplayMetrics;
 import android.util.Log;
 import android.util.Pair;
@@ -120,7 +119,6 @@
 import com.android.internal.widget.FloatingToolbar;
 
 import java.util.List;
-import java.util.function.Function;
 
 /** @hide */
 public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
@@ -283,11 +281,6 @@
     private Insets mLastBackgroundInsets = Insets.NONE;
     private boolean mDrawLegacyNavigationBarBackground;
 
-    /**
-     * Whether the app targets an SDK that uses the new insets APIs.
-     */
-    private boolean mUseNewInsetsApi;
-
     private PendingInsetsController mPendingInsetsController = new PendingInsetsController();
 
     DecorView(Context context, int featureId, PhoneWindow window,
@@ -319,7 +312,6 @@
         initResizingPaints();
 
         mLegacyNavigationBarBackgroundPaint.setColor(Color.BLACK);
-        mUseNewInsetsApi = context.getApplicationInfo().targetSdkVersion >= VERSION_CODES.R;
     }
 
     void setBackgroundFallback(@Nullable Drawable fallbackDrawable) {
@@ -1189,23 +1181,23 @@
         // these flags wouldn't make the window draw behind the navigation bar, unless
         // LAYOUT_HIDE_NAVIGATION was set.
         //
-        // Note: Once the app targets R+, we no longer do this logic because we can't rely on
-        // SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION to indicate whether the app wants to handle it by
-        // themselves.
+        // Note: Once the app uses the R+ Window.setDecorFitsSystemWindows(false) API we no longer
+        // consume insets because they might no longer set SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION.
         boolean hideNavigation = (sysUiVisibility & SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0
                 || !(controller == null || controller.isRequestedVisible(ITYPE_NAVIGATION_BAR));
+        boolean decorFitsSystemWindows = mWindow.mDecorFitsSystemWindows;
         boolean forceConsumingNavBar = (mForceWindowDrawsBarBackgrounds
                         && (attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) == 0
                         && (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0
+                        && decorFitsSystemWindows
                         && !hideNavigation)
                 || (mLastShouldAlwaysConsumeSystemBars && hideNavigation);
 
         boolean consumingNavBar =
                 ((attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0
                         && (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0
-                        && !hideNavigation
-                        // TODO IME wrap_content windows need to have margin to work properly
-                        && (!mUseNewInsetsApi || isImeWindow))
+                        && decorFitsSystemWindows
+                        && !hideNavigation)
                 || forceConsumingNavBar;
 
         // If we didn't request fullscreen layout, but we still got it because of the
@@ -1216,6 +1208,7 @@
                 || (attrs.flags & FLAG_FULLSCREEN) != 0
                 || !(controller == null || controller.isRequestedVisible(ITYPE_STATUS_BAR));
         boolean consumingStatusBar = (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) == 0
+                && decorFitsSystemWindows
                 && (attrs.flags & FLAG_LAYOUT_IN_SCREEN) == 0
                 && (attrs.flags & FLAG_LAYOUT_INSET_DECOR) == 0
                 && mForceWindowDrawsBarBackgrounds
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index 138d0dd..25c114f 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -343,8 +343,7 @@
     /** @see ViewRootImpl#mActivityConfigCallback */
     private ActivityConfigCallback mActivityConfigCallback;
 
-    private OnContentApplyWindowInsetsListener mPendingOnContentApplyWindowInsetsListener =
-            sDefaultContentInsetsApplier;
+    boolean mDecorFitsSystemWindows = true;
 
     static class WindowManagerHolder {
         static final IWindowManager sWindowManager = IWindowManager.Stub.asInterface(
@@ -2138,9 +2137,7 @@
     /** Notify when decor view is attached to window and {@link ViewRootImpl} is available. */
     void onViewRootImplSet(ViewRootImpl viewRoot) {
         viewRoot.setActivityConfigCallback(mActivityConfigCallback);
-        viewRoot.setOnContentApplyWindowInsetsListener(
-                mPendingOnContentApplyWindowInsetsListener);
-        mPendingOnContentApplyWindowInsetsListener = null;
+        applyDecorFitsSystemWindows();
     }
 
     static private final String FOCUSED_ID_TAG = "android:focusedViewId";
@@ -3907,14 +3904,16 @@
 
     @Override
     public void setDecorFitsSystemWindows(boolean decorFitsSystemWindows) {
+        mDecorFitsSystemWindows = decorFitsSystemWindows;
+        applyDecorFitsSystemWindows();
+    }
+
+    private void applyDecorFitsSystemWindows() {
         ViewRootImpl impl = getViewRootImplOrNull();
-        OnContentApplyWindowInsetsListener listener = decorFitsSystemWindows
-                ? sDefaultContentInsetsApplier
-                : null;
         if (impl != null) {
-            impl.setOnContentApplyWindowInsetsListener(listener);
-        } else {
-            mPendingOnContentApplyWindowInsetsListener = listener;
+            impl.setOnContentApplyWindowInsetsListener(mDecorFitsSystemWindows
+                    ? sDefaultContentInsetsApplier
+                    : null);
         }
     }
 }
diff --git a/core/proto/android/stats/devicepolicy/device_policy_enums.proto b/core/proto/android/stats/devicepolicy/device_policy_enums.proto
index 684a292..896ee4f 100644
--- a/core/proto/android/stats/devicepolicy/device_policy_enums.proto
+++ b/core/proto/android/stats/devicepolicy/device_policy_enums.proto
@@ -186,4 +186,15 @@
   RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK= 159;
   RESOLVER_EMPTY_STATE_NO_APPS_RESOLVED= 160;
   RESOLVER_AUTOLAUNCH_CROSS_PROFILE_TARGET = 161;
+  CROSS_PROFILE_SETTINGS_PAGE_LAUNCHED_FROM_APP = 162;
+  CROSS_PROFILE_SETTINGS_PAGE_LAUNCHED_FROM_SETTINGS = 163;
+  CROSS_PROFILE_SETTINGS_PAGE_ADMIN_RESTRICTED = 164;
+  CROSS_PROFILE_SETTINGS_PAGE_MISSING_WORK_APP = 165;
+  CROSS_PROFILE_SETTINGS_PAGE_MISSING_PERSONAL_APP = 166;
+  CROSS_PROFILE_SETTINGS_PAGE_MISSING_INSTALL_BANNER_INTENT = 167;
+  CROSS_PROFILE_SETTINGS_PAGE_INSTALL_BANNER_CLICKED = 168;
+  CROSS_PROFILE_SETTINGS_PAGE_INSTALL_BANNER_NO_INTENT_CLICKED  = 169;
+  CROSS_PROFILE_SETTINGS_PAGE_USER_CONSENTED = 170;
+  CROSS_PROFILE_SETTINGS_PAGE_USER_DECLINED_CONSENT = 171;
+  CROSS_PROFILE_SETTINGS_PAGE_PERMISSION_REVOKED = 172;
 }
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 451363f..c72239c 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -4122,7 +4122,10 @@
          set of pages referenced over time.
          <p>Declaring the permission implies intention to use the API and the user of the
          device can grant permission through the Settings application.
-         <p>Protection level: signature|privileged|appop -->
+         <p>Protection level: signature|privileged|appop
+         <p>A data loader has to be the one which provides data to install an app.
+         <p>A data loader has to have both permission:LOADER_USAGE_STATS AND
+         appop:LOADER_USAGE_STATS allowed to be able to access the read logs. -->
     <permission android:name="android.permission.LOADER_USAGE_STATS"
         android:protectionLevel="signature|privileged|appop" />
     <uses-permission android:name="android.permission.LOADER_USAGE_STATS" />
diff --git a/core/tests/coretests/src/android/app/WindowContextTest.java b/core/tests/coretests/src/android/app/WindowContextTest.java
new file mode 100644
index 0000000..630e16a
--- /dev/null
+++ b/core/tests/coretests/src/android/app/WindowContextTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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 android.app;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.os.IBinder;
+import android.platform.test.annotations.Presubmit;
+import android.view.Display;
+import android.view.IWindowManager;
+import android.view.WindowManagerGlobal;
+
+import androidx.test.filters.FlakyTest;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for {@link WindowContext}
+ *
+ * <p>Build/Install/Run:
+ *  atest FrameworksCoreTests:WindowContextTest
+ *
+ * <p>This test class is a part of Window Manager Service tests and specified in
+ * {@link com.android.server.wm.test.filters.FrameworksTestsFilter}.
+ */
+@FlakyTest(bugId = 150812449, detail = "Remove after confirmed it's stable.")
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class WindowContextTest {
+    @Test
+    public void testWindowContextRelease_doRemoveWindowToken() throws Throwable {
+        final Context instContext = InstrumentationRegistry.getInstrumentation()
+                .getTargetContext();
+        final Display display = instContext.getSystemService(DisplayManager.class)
+                .getDisplay(DEFAULT_DISPLAY);
+        final Context context = instContext.createDisplayContext(display);
+        final WindowContext windowContext = new WindowContext(context, TYPE_APPLICATION_OVERLAY,
+                null /* options */);
+
+        final IBinder token = windowContext.getActivityToken();
+
+        final IWindowManager wms = WindowManagerGlobal.getWindowManagerService();
+        assertTrue("Token must be registered to WMS", wms.isWindowToken(token));
+
+        windowContext.release();
+
+        assertFalse("Token must be unregistered to WMS", wms.isWindowToken(token));
+    }
+}
diff --git a/core/tests/coretests/src/android/view/WindowMetricsTest.java b/core/tests/coretests/src/android/view/WindowMetricsTest.java
index 74524bf..ddc977d 100644
--- a/core/tests/coretests/src/android/view/WindowMetricsTest.java
+++ b/core/tests/coretests/src/android/view/WindowMetricsTest.java
@@ -56,17 +56,17 @@
 
     @Before
     public void setUp() {
-        final Context insetContext = InstrumentationRegistry.getInstrumentation()
+        final Context instContext = InstrumentationRegistry.getInstrumentation()
                 .getTargetContext();
-        final Display display = insetContext.getSystemService(DisplayManager.class)
+        final Display display = instContext.getSystemService(DisplayManager.class)
                 .getDisplay(DEFAULT_DISPLAY);
-        mWindowContext = insetContext.createDisplayContext(display)
+        mWindowContext = instContext.createDisplayContext(display)
                 .createWindowContext(TYPE_APPLICATION_OVERLAY, null /* options */);
         mWm = mWindowContext.getSystemService(WindowManager.class);
     }
 
     @Test
-    public void testAddViewANdRemoveView_GetMetrics_DoNotCrash() {
+    public void testAddViewAndRemoveView_GetMetrics_DoNotCrash() {
         final View view = new View(mWindowContext);
         final WindowManager.LayoutParams params =
                 new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY);
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index 07cf415..19ad6f7 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -1507,6 +1507,18 @@
       "group": "WM_DEBUG_REMOTE_ANIMATIONS",
       "at": "com\/android\/server\/wm\/RemoteAnimationController.java"
     },
+    "838570988": {
+      "message": "Could not report token removal to the window token client.",
+      "level": "WARN",
+      "group": "WM_ERROR",
+      "at": "com\/android\/server\/wm\/WindowToken.java"
+    },
+    "845234215": {
+      "message": "App is requesting an orientation, return %d for display id=%d",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_ORIENTATION",
+      "at": "com\/android\/server\/wm\/DisplayContent.java"
+    },
     "853091290": {
       "message": "Moved stack=%s behind stack=%s",
       "level": "DEBUG",
diff --git a/location/java/android/location/LocationRequest.java b/location/java/android/location/LocationRequest.java
index 4dd1a29..5f0acc8 100644
--- a/location/java/android/location/LocationRequest.java
+++ b/location/java/android/location/LocationRequest.java
@@ -264,8 +264,8 @@
                 /* numUpdates= */ Integer.MAX_VALUE,
                 /* smallestDisplacement= */ 0,
                 /* hideFromAppOps= */ false,
-                /* lowPowerMode= */ false,
                 /* locationSettingsIgnored= */ false,
+                /* lowPowerMode= */ false,
                 /* workSource= */ null);
     }
 
@@ -282,8 +282,8 @@
                 src.mNumUpdates,
                 src.mSmallestDisplacement,
                 src.mHideFromAppOps,
-                src.mLowPowerMode,
                 src.mLocationSettingsIgnored,
+                src.mLowPowerMode,
                 src.mWorkSource);
     }
 
diff --git a/packages/CarSystemUI/res/layout/notification_center_activity.xml b/packages/CarSystemUI/res/layout/notification_center_activity.xml
index 4fef489..0af74c4 100644
--- a/packages/CarSystemUI/res/layout/notification_center_activity.xml
+++ b/packages/CarSystemUI/res/layout/notification_center_activity.xml
@@ -20,8 +20,6 @@
     android:id="@+id/notification_view"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:visibility="invisible"
-    android:layout_marginBottom="@dimen/navigation_bar_height"
     android:background="@color/notification_shade_background_color">
 
     <View
diff --git a/packages/CarSystemUI/res/layout/notification_panel_container.xml b/packages/CarSystemUI/res/layout/notification_panel_container.xml
index bf71396..3b53c6a 100644
--- a/packages/CarSystemUI/res/layout/notification_panel_container.xml
+++ b/packages/CarSystemUI/res/layout/notification_panel_container.xml
@@ -18,4 +18,5 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/notification_container"
     android:layout_width="match_parent"
-    android:layout_height="match_parent"/>
+    android:layout_height="match_parent"
+    android:visibility="invisible"/>
diff --git a/packages/CarSystemUI/res/layout/sysui_overlay_window.xml b/packages/CarSystemUI/res/layout/sysui_overlay_window.xml
index 1b0a211..067e359 100644
--- a/packages/CarSystemUI/res/layout/sysui_overlay_window.xml
+++ b/packages/CarSystemUI/res/layout/sysui_overlay_window.xml
@@ -22,10 +22,12 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent">
 
+    <!-- TODO(b/151617493): replace marginBottom with insets. -->
     <ViewStub android:id="@+id/notification_panel_stub"
               android:layout_width="match_parent"
               android:layout_height="match_parent"
-              android:layout="@layout/notification_panel_container"/>
+              android:layout="@layout/notification_panel_container"
+              android:layout_marginBottom="@dimen/navigation_bar_height"/>
 
     <ViewStub android:id="@+id/fullscreen_user_switcher_stub"
               android:layout_width="match_parent"
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/CarDeviceProvisionedController.java b/packages/CarSystemUI/src/com/android/systemui/car/CarDeviceProvisionedController.java
index b057198..44e43fe 100644
--- a/packages/CarSystemUI/src/com/android/systemui/car/CarDeviceProvisionedController.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/CarDeviceProvisionedController.java
@@ -24,7 +24,7 @@
  */
 public interface CarDeviceProvisionedController extends DeviceProvisionedController {
     /**
-     * Returns {@code true} then SUW is in progress for the given user.
+     * Returns {@code true} when SUW is in progress for the given user.
      */
     boolean isUserSetupInProgress(int user);
 
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewController.java b/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewController.java
index 1901a2d..d8a894c 100644
--- a/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewController.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewController.java
@@ -16,9 +16,6 @@
 
 package com.android.systemui.car.notification;
 
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
 import android.app.ActivityManager;
 import android.car.Car;
 import android.car.drivingstate.CarUxRestrictionsManager;
@@ -33,7 +30,6 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
 
 import androidx.annotation.NonNull;
 import androidx.recyclerview.widget.RecyclerView;
@@ -54,7 +50,7 @@
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.FlingAnimationUtils;
 import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.window.OverlayViewController;
+import com.android.systemui.window.OverlayPanelViewController;
 import com.android.systemui.window.OverlayViewGlobalStateController;
 
 import javax.inject.Inject;
@@ -62,39 +58,22 @@
 
 /** View controller for the notification panel. */
 @Singleton
-public class NotificationPanelViewController extends OverlayViewController {
+public class NotificationPanelViewController extends OverlayPanelViewController {
 
-    // used to calculate how fast to open or close the window
-    private static final float DEFAULT_FLING_VELOCITY = 0;
-    // max time a fling animation takes
-    private static final float FLING_ANIMATION_MAX_TIME = 0.5f;
-    // acceleration rate for the fling animation
-    private static final float FLING_SPEED_UP_FACTOR = 0.6f;
-
-    private static final int SWIPE_DOWN_MIN_DISTANCE = 25;
-    private static final int SWIPE_MAX_OFF_PATH = 75;
-    private static final int SWIPE_THRESHOLD_VELOCITY = 200;
     private static final boolean DEBUG = true;
     private static final String TAG = "NotificationPanelViewController";
 
     private final Context mContext;
     private final Resources mResources;
     private final CarServiceProvider mCarServiceProvider;
-    private final CarDeviceProvisionedController mCarDeviceProvisionedController;
     private final IStatusBarService mBarService;
     private final CommandQueue mCommandQueue;
     private final NotificationDataManager mNotificationDataManager;
     private final CarUxRestrictionManagerWrapper mCarUxRestrictionManagerWrapper;
     private final CarNotificationListener mCarNotificationListener;
     private final NotificationClickHandlerFactory mNotificationClickHandlerFactory;
-    private final FlingAnimationUtils mFlingAnimationUtils;
     private final StatusBarStateController mStatusBarStateController;
 
-    private final int mSettleClosePercentage;
-
-    private float mOpeningVelocity = DEFAULT_FLING_VELOCITY;
-    private float mClosingVelocity = DEFAULT_FLING_VELOCITY;
-
     private float mInitialBackgroundAlpha;
     private float mBackgroundAlphaDiff;
 
@@ -108,13 +87,7 @@
     private float mFirstTouchDownOnGlassPane;
     private boolean mNotificationListAtBottomAtTimeOfTouch;
     private boolean mIsSwipingVerticallyToClose;
-    private int mPercentageFromBottom;
-    private boolean mIsNotificationAnimating;
     private boolean mIsNotificationCardSwiping;
-    private boolean mPanelExpanded = false;
-
-    private View.OnTouchListener mTopNavBarNotificationTouchListener;
-    private View.OnTouchListener mNavBarNotificationTouchListener;
 
     private OnUnseenCountUpdateListener mUnseenCountUpdateListener;
 
@@ -123,6 +96,7 @@
             Context context,
             @Main Resources resources,
             OverlayViewGlobalStateController overlayViewGlobalStateController,
+            FlingAnimationUtils.Builder flingAnimationUtilsBuilder,
 
             /* Other things */
             CarServiceProvider carServiceProvider,
@@ -135,26 +109,21 @@
             CarUxRestrictionManagerWrapper carUxRestrictionManagerWrapper,
             CarNotificationListener carNotificationListener,
             NotificationClickHandlerFactory notificationClickHandlerFactory,
-            FlingAnimationUtils.Builder flingAnimationUtilsBuilder,
 
             /* Things that need to be replaced */
             StatusBarStateController statusBarStateController
     ) {
-        super(R.id.notification_panel_stub, overlayViewGlobalStateController);
+        super(context, resources, R.id.notification_panel_stub, overlayViewGlobalStateController,
+                flingAnimationUtilsBuilder, carDeviceProvisionedController);
         mContext = context;
         mResources = resources;
         mCarServiceProvider = carServiceProvider;
-        mCarDeviceProvisionedController = carDeviceProvisionedController;
         mBarService = barService;
         mCommandQueue = commandQueue;
         mNotificationDataManager = notificationDataManager;
         mCarUxRestrictionManagerWrapper = carUxRestrictionManagerWrapper;
         mCarNotificationListener = carNotificationListener;
         mNotificationClickHandlerFactory = notificationClickHandlerFactory;
-        mFlingAnimationUtils = flingAnimationUtilsBuilder
-                .setMaxLengthSeconds(FLING_ANIMATION_MAX_TIME)
-                .setSpeedUpFactor(FLING_SPEED_UP_FACTOR)
-                .build();
         mStatusBarStateController = statusBarStateController;
 
         // Notification background setup.
@@ -175,60 +144,6 @@
                             + " percentage");
         }
         mBackgroundAlphaDiff = finalBackgroundAlpha - mInitialBackgroundAlpha;
-
-        // Notification Panel param setup
-        mSettleClosePercentage = mResources.getInteger(
-                R.integer.notification_settle_close_percentage);
-
-        // Attached to the top navigation bar (i.e. status bar) to detect pull down of the
-        // notification shade.
-        GestureDetector openGestureDetector = new GestureDetector(mContext,
-                new OpenNotificationGestureListener() {
-                    @Override
-                    protected void openNotification() {
-                        animateExpandNotificationsPanel();
-                    }
-                });
-
-        // Attached to the NavBars to close the notification shade
-        GestureDetector navBarCloseNotificationGestureDetector = new GestureDetector(mContext,
-                new NavBarCloseNotificationGestureListener() {
-                    @Override
-                    protected void close() {
-                        if (mPanelExpanded) {
-                            animateCollapsePanels();
-                        }
-                    }
-                });
-
-        mTopNavBarNotificationTouchListener = (v, event) -> {
-            if (!isInflated()) {
-                getOverlayViewGlobalStateController().inflateView(this);
-            }
-            if (!mCarDeviceProvisionedController.isCurrentUserFullySetup()) {
-                return true;
-            }
-
-            boolean consumed = openGestureDetector.onTouchEvent(event);
-            if (consumed) {
-                return true;
-            }
-            maybeCompleteAnimation(event);
-            return true;
-        };
-
-        mNavBarNotificationTouchListener =
-                (v, event) -> {
-                    if (!isInflated()) {
-                        return true;
-                    }
-                    boolean consumed = navBarCloseNotificationGestureDetector.onTouchEvent(event);
-                    if (consumed) {
-                        return true;
-                    }
-                    maybeCompleteAnimation(event);
-                    return true;
-                };
     }
 
     @Override
@@ -252,14 +167,13 @@
     private void onNotificationViewInflated() {
         // Find views.
         mNotificationView = getLayout().findViewById(R.id.notification_view);
-        View glassPane = mNotificationView.findViewById(R.id.glass_pane);
-        mHandleBar = mNotificationView.findViewById(R.id.handle_bar);
-        mNotificationList = mNotificationView.findViewById(R.id.notifications);
+        setupHandleBar();
+        setupNotificationPanel();
 
         mNotificationClickHandlerFactory.registerClickListener((launchResult, alertEntry) -> {
             if (launchResult == ActivityManager.START_TASK_TO_FRONT
                     || launchResult == ActivityManager.START_SUCCESS) {
-                animateCollapsePanels();
+                animateCollapsePanel();
             }
         });
 
@@ -269,39 +183,52 @@
                         mNotificationDataManager.getUnseenNotificationCount());
             }
         });
+
         mNotificationClickHandlerFactory.setNotificationDataManager(mNotificationDataManager);
         mNotificationView.setClickHandlerFactory(mNotificationClickHandlerFactory);
         mNotificationView.setNotificationDataManager(mNotificationDataManager);
 
-        mNotificationList.addOnScrollListener(new RecyclerView.OnScrollListener() {
-            @Override
-            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
-                super.onScrolled(recyclerView, dx, dy);
-                if (!mNotificationList.canScrollVertically(1)) {
-                    mNotificationListAtBottom = true;
-                    return;
-                }
-                mNotificationListAtBottom = false;
-                mIsSwipingVerticallyToClose = false;
-                mNotificationListAtBottomAtTimeOfTouch = false;
-            }
-        });
+        mCarServiceProvider.addListener(car -> {
+            CarUxRestrictionsManager carUxRestrictionsManager =
+                    (CarUxRestrictionsManager)
+                            car.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE);
+            mCarUxRestrictionManagerWrapper.setCarUxRestrictionsManager(
+                    carUxRestrictionsManager);
 
-        // Attached to the notification ui to detect close request of the notification shade.
+            mNotificationViewController = new NotificationViewController(
+                    mNotificationView,
+                    PreprocessingManager.getInstance(mContext),
+                    mCarNotificationListener,
+                    mCarUxRestrictionManagerWrapper,
+                    mNotificationDataManager);
+            mNotificationViewController.enable();
+        });
+    }
+
+    private void setupHandleBar() {
+        mHandleBar = mNotificationView.findViewById(R.id.handle_bar);
+        GestureDetector handleBarCloseNotificationGestureDetector = new GestureDetector(mContext,
+                new HandleBarCloseGestureListener());
+        mHandleBar.setOnTouchListener((v, event) -> {
+            handleBarCloseNotificationGestureDetector.onTouchEvent(event);
+            maybeCompleteAnimation(event);
+            return true;
+        });
+    }
+
+    private void setupNotificationPanel() {
+        View glassPane = mNotificationView.findViewById(R.id.glass_pane);
+        mNotificationList = mNotificationView.findViewById(R.id.notifications);
         GestureDetector closeGestureDetector = new GestureDetector(mContext,
-                new CloseNotificationGestureListener() {
+                new CloseGestureListener() {
                     @Override
                     protected void close() {
-                        if (mPanelExpanded) {
-                            animateCollapsePanels();
+                        if (isPanelExpanded()) {
+                            animateCollapsePanel();
                         }
                     }
                 });
 
-        // Attached to the Handle bar to close the notification shade
-        GestureDetector handleBarCloseNotificationGestureDetector = new GestureDetector(mContext,
-                new HandleBarCloseNotificationGestureListener());
-
         // The glass pane is used to view touch events before passed to the notification list.
         // This allows us to initialize gesture listeners and detect when to close the notifications
         glassPane.setOnTouchListener((v, event) -> {
@@ -320,6 +247,21 @@
             return false;
         });
 
+        mNotificationList.addOnScrollListener(new RecyclerView.OnScrollListener() {
+            @Override
+            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+                super.onScrolled(recyclerView, dx, dy);
+                // Check if we can scroll vertically downwards.
+                if (!mNotificationList.canScrollVertically(/* direction= */ 1)) {
+                    mNotificationListAtBottom = true;
+                    return;
+                }
+                mNotificationListAtBottom = false;
+                mIsSwipingVerticallyToClose = false;
+                mNotificationListAtBottomAtTimeOfTouch = false;
+            }
+        });
+
         mNotificationList.setOnTouchListener((v, event) -> {
             mIsNotificationCardSwiping = Math.abs(mFirstTouchDownOnGlassPane - event.getRawX())
                     > SWIPE_MAX_OFF_PATH;
@@ -341,19 +283,19 @@
 
             boolean handled = closeGestureDetector.onTouchEvent(event);
             boolean isTracking = mIsTracking;
-            Rect rect = mNotificationView.getClipBounds();
+            Rect rect = getLayout().getClipBounds();
             float clippedHeight = 0;
             if (rect != null) {
                 clippedHeight = rect.bottom;
             }
             if (!handled && event.getActionMasked() == MotionEvent.ACTION_UP
                     && mIsSwipingVerticallyToClose) {
-                if (mSettleClosePercentage < mPercentageFromBottom && isTracking) {
-                    animateNotificationPanel(DEFAULT_FLING_VELOCITY, false);
-                } else if (clippedHeight != mNotificationView.getHeight() && isTracking) {
+                if (getSettleClosePercentage() < getPercentageFromBottom() && isTracking) {
+                    animatePanel(DEFAULT_FLING_VELOCITY, false);
+                } else if (clippedHeight != getLayout().getHeight() && isTracking) {
                     // this can be caused when user is at the end of the list and trying to
                     // fling to top of the list by scrolling down.
-                    animateNotificationPanel(DEFAULT_FLING_VELOCITY, true);
+                    animatePanel(DEFAULT_FLING_VELOCITY, true);
                 }
             }
 
@@ -365,28 +307,6 @@
             }
             return handled || isTracking;
         });
-
-        mCarServiceProvider.addListener(car -> {
-            CarUxRestrictionsManager carUxRestrictionsManager =
-                    (CarUxRestrictionsManager)
-                            car.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE);
-            mCarUxRestrictionManagerWrapper.setCarUxRestrictionsManager(
-                    carUxRestrictionsManager);
-
-            mNotificationViewController = new NotificationViewController(
-                    mNotificationView,
-                    PreprocessingManager.getInstance(mContext),
-                    mCarNotificationListener,
-                    mCarUxRestrictionManagerWrapper,
-                    mNotificationDataManager);
-            mNotificationViewController.enable();
-        });
-
-        mHandleBar.setOnTouchListener((v, event) -> {
-            handleBarCloseNotificationGestureDetector.onTouchEvent(event);
-            maybeCompleteAnimation(event);
-            return true;
-        });
     }
 
     /** Called when the car power state is changed to ON. */
@@ -397,139 +317,40 @@
         mNotificationDataManager.clearAll();
     }
 
-    View.OnTouchListener getTopNavBarNotificationTouchListener() {
-        return mTopNavBarNotificationTouchListener;
+    @Override
+    protected boolean shouldAnimateCollapsePanel() {
+        return true;
     }
 
-    View.OnTouchListener getNavBarNotificationTouchListener() {
-        return mNavBarNotificationTouchListener;
+    @Override
+    protected void onAnimateCollapsePanel() {
+        // No op.
     }
 
-    private void maybeCompleteAnimation(MotionEvent event) {
-        if (event.getActionMasked() == MotionEvent.ACTION_UP
-                && mNotificationView.getVisibility() == View.VISIBLE) {
-            if (mSettleClosePercentage < mPercentageFromBottom) {
-                animateNotificationPanel(DEFAULT_FLING_VELOCITY, false);
-            } else {
-                animateNotificationPanel(DEFAULT_FLING_VELOCITY, true);
-            }
-        }
+    @Override
+    protected boolean shouldAnimateExpandPanel() {
+        return mCommandQueue.panelsEnabled();
     }
 
-    /**
-     * Animates the notification shade from one position to other. This is used to either open or
-     * close the notification shade completely with a velocity. If the animation is to close the
-     * notification shade this method also makes the view invisible after animation ends.
-     */
-    private void animateNotificationPanel(float velocity, boolean isClosing) {
-        float to = 0;
-        if (!isClosing) {
-            to = mNotificationView.getHeight();
-        }
-
-        Rect rect = mNotificationView.getClipBounds();
-        if (rect != null && rect.bottom != to) {
-            float from = rect.bottom;
-            animate(from, to, velocity, isClosing);
-            return;
-        }
-
-        // We will only be here if the shade is being opened programmatically or via button when
-        // height of the layout was not calculated.
-        ViewTreeObserver notificationTreeObserver = mNotificationView.getViewTreeObserver();
-        notificationTreeObserver.addOnGlobalLayoutListener(
-                new ViewTreeObserver.OnGlobalLayoutListener() {
-                    @Override
-                    public void onGlobalLayout() {
-                        ViewTreeObserver obs = mNotificationView.getViewTreeObserver();
-                        obs.removeOnGlobalLayoutListener(this);
-                        float to = mNotificationView.getHeight();
-                        animate(/* from= */ 0, to, velocity, isClosing);
-                    }
-                });
-    }
-
-    private void animateCollapsePanels() {
-        if (!mPanelExpanded || mNotificationView.getVisibility() == View.INVISIBLE) {
-            return;
-        }
-        getOverlayViewGlobalStateController().setWindowFocusable(false);
-        animateNotificationPanel(mClosingVelocity, true);
-    }
-
-    private void animateExpandNotificationsPanel() {
-        if (!mCommandQueue.panelsEnabled()
-                || !mCarDeviceProvisionedController.isCurrentUserFullySetup()) {
-            return;
-        }
-        // scroll to top
+    @Override
+    protected void onAnimateExpandPanel() {
         mNotificationList.scrollToPosition(0);
-        setPanelVisible(true);
-        mNotificationView.setVisibility(View.VISIBLE);
-        animateNotificationPanel(mOpeningVelocity, false);
-
-        setPanelExpanded(true);
     }
 
-    private void animate(float from, float to, float velocity, boolean isClosing) {
-        if (mIsNotificationAnimating) {
-            return;
-        }
-        mIsNotificationAnimating = true;
-        mIsTracking = true;
-        ValueAnimator animator = ValueAnimator.ofFloat(from, to);
-        animator.addUpdateListener(
-                animation -> {
-                    float animatedValue = (Float) animation.getAnimatedValue();
-                    setNotificationViewClipBounds((int) animatedValue);
-                });
-        animator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                super.onAnimationEnd(animation);
-                mIsNotificationAnimating = false;
-                mIsTracking = false;
-                mOpeningVelocity = DEFAULT_FLING_VELOCITY;
-                mClosingVelocity = DEFAULT_FLING_VELOCITY;
-                if (isClosing) {
-                    setPanelVisible(false);
-                    mNotificationView.setVisibility(View.INVISIBLE);
-                    mNotificationView.setClipBounds(null);
-                    mNotificationViewController.onVisibilityChanged(false);
-                    // let the status bar know that the panel is closed
-                    setPanelExpanded(false);
-                } else {
-                    mNotificationViewController.onVisibilityChanged(true);
-                    // let the status bar know that the panel is open
-                    mNotificationView.setVisibleNotificationsAsSeen();
-                    setPanelExpanded(true);
-                }
-            }
-        });
-        mFlingAnimationUtils.apply(animator, from, to, Math.abs(velocity));
-        animator.start();
+    @Override
+    protected void onCollapseAnimationEnd() {
+        mNotificationViewController.onVisibilityChanged(false);
     }
 
-    /**
-     * Set the panel view to be visible.
-     */
-    public void setPanelVisible(boolean visible) {
-        if (visible && !getOverlayViewGlobalStateController().isWindowVisible()) {
-            getOverlayViewGlobalStateController().setWindowVisible(true);
-        }
-        if (!visible && getOverlayViewGlobalStateController().isWindowVisible()) {
-            getOverlayViewGlobalStateController().setWindowVisible(false);
-        }
-        getLayout().setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
-        getOverlayViewGlobalStateController().setWindowFocusable(visible);
+    @Override
+    protected void onExpandAnimationEnd() {
+        mNotificationViewController.onVisibilityChanged(true);
+        mNotificationView.setVisibleNotificationsAsSeen();
     }
 
-    /**
-     * Set the panel state to expanded. This will expand or collapse the overlay window if
-     * necessary.
-     */
-    public void setPanelExpanded(boolean expand) {
-        mPanelExpanded = expand;
+    @Override
+    protected void onPanelExpanded(boolean expand) {
+        super.onPanelExpanded(expand);
 
         if (expand && mStatusBarStateController.getState() != StatusBarState.KEYGUARD) {
             if (DEBUG) {
@@ -550,19 +371,19 @@
         }
     }
 
-    private void setNotificationViewClipBounds(int height) {
-        if (height > mNotificationView.getHeight()) {
-            height = mNotificationView.getHeight();
-        }
-        Rect clipBounds = new Rect();
-        clipBounds.set(0, 0, mNotificationView.getWidth(), height);
-        // Sets the clip region on the notification list view.
-        mNotificationView.setClipBounds(clipBounds);
+    @Override
+    protected void onOpenScrollStart() {
+        mNotificationList.scrollToPosition(0);
+    }
+
+    @Override
+    protected void onScroll(int height) {
         if (mHandleBar != null) {
             ViewGroup.MarginLayoutParams lp =
                     (ViewGroup.MarginLayoutParams) mHandleBar.getLayoutParams();
             mHandleBar.setTranslationY(height - mHandleBar.getHeight() - lp.bottomMargin);
         }
+
         if (mNotificationView.getHeight() > 0) {
             Drawable background = mNotificationView.getBackground().mutate();
             background.setAlpha((int) (getBackgroundAlpha(height) * 255));
@@ -570,6 +391,13 @@
         }
     }
 
+    @Override
+    protected boolean shouldAllowClosingScroll() {
+        // Unless the notification list is at the bottom, the panel shouldn't be allowed to
+        // collapse on scroll.
+        return mNotificationListAtBottomAtTimeOfTouch;
+    }
+
     /**
      * Calculates the alpha value for the background based on how much of the notification
      * shade is visible to the user. When the notification shade is completely open then
@@ -580,30 +408,6 @@
                 + ((float) height / mNotificationView.getHeight() * mBackgroundAlphaDiff);
     }
 
-    private void calculatePercentageFromBottom(float height) {
-        if (mNotificationView.getHeight() > 0) {
-            mPercentageFromBottom = (int) Math.abs(
-                    height / mNotificationView.getHeight() * 100);
-        }
-    }
-
-    /** Toggles the visibility of the notification panel. */
-    public void toggle() {
-        if (!isInflated()) {
-            getOverlayViewGlobalStateController().inflateView(this);
-        }
-        if (mPanelExpanded) {
-            animateCollapsePanels();
-        } else {
-            animateExpandNotificationsPanel();
-        }
-    }
-
-    /** Returns {@code true} if the notification panel is expanded. */
-    public boolean isPanelExpanded() {
-        return mPanelExpanded;
-    }
-
     /** Sets the unseen count listener. */
     public void setOnUnseenCountUpdateListener(OnUnseenCountUpdateListener listener) {
         mUnseenCountUpdateListener = listener;
@@ -619,154 +423,9 @@
     }
 
     /**
-     * Only responsible for open hooks. Since once the panel opens it covers all elements
-     * there is no need to merge with close.
-     */
-    private abstract class OpenNotificationGestureListener extends
-            GestureDetector.SimpleOnGestureListener {
-
-        @Override
-        public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
-                float distanceY) {
-
-            if (mNotificationView.getVisibility() == View.INVISIBLE) {
-                // when the on-scroll is called for the first time to open.
-                mNotificationList.scrollToPosition(0);
-            }
-            setPanelVisible(true);
-            mNotificationView.setVisibility(View.VISIBLE);
-
-            // clips the view for the notification shade when the user scrolls to open.
-            setNotificationViewClipBounds((int) event2.getRawY());
-
-            // Initially the scroll starts with height being zero. This checks protects from divide
-            // by zero error.
-            calculatePercentageFromBottom(event2.getRawY());
-
-            mIsTracking = true;
-            return true;
-        }
-
-
-        @Override
-        public boolean onFling(MotionEvent event1, MotionEvent event2,
-                float velocityX, float velocityY) {
-            if (velocityY > SWIPE_THRESHOLD_VELOCITY) {
-                mOpeningVelocity = velocityY;
-                openNotification();
-                return true;
-            }
-            animateNotificationPanel(DEFAULT_FLING_VELOCITY, true);
-
-            return false;
-        }
-
-        protected abstract void openNotification();
-    }
-
-    /**
-     * To be installed on the open panel notification panel
-     */
-    private abstract class CloseNotificationGestureListener extends
-            GestureDetector.SimpleOnGestureListener {
-
-        @Override
-        public boolean onSingleTapUp(MotionEvent motionEvent) {
-            if (mPanelExpanded) {
-                animateNotificationPanel(DEFAULT_FLING_VELOCITY, true);
-            }
-            return true;
-        }
-
-        @Override
-        public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
-                float distanceY) {
-            // should not clip while scroll to the bottom of the list.
-            if (!mNotificationListAtBottomAtTimeOfTouch) {
-                return false;
-            }
-            float actualNotificationHeight =
-                    mNotificationView.getHeight() - (event1.getRawY() - event2.getRawY());
-            if (actualNotificationHeight > mNotificationView.getHeight()) {
-                actualNotificationHeight = mNotificationView.getHeight();
-            }
-            if (mNotificationView.getHeight() > 0) {
-                mPercentageFromBottom = (int) Math.abs(
-                        actualNotificationHeight / mNotificationView.getHeight() * 100);
-                boolean isUp = distanceY > 0;
-
-                // This check is to figure out if onScroll was called while swiping the card at
-                // bottom of the list. At that time we should not allow notification shade to
-                // close. We are also checking for the upwards swipe gesture here because it is
-                // possible if a user is closing the notification shade and while swiping starts
-                // to open again but does not fling. At that time we should allow the
-                // notification shade to close fully or else it would stuck in between.
-                if (Math.abs(mNotificationView.getHeight() - actualNotificationHeight)
-                        > SWIPE_DOWN_MIN_DISTANCE && isUp) {
-                    setNotificationViewClipBounds((int) actualNotificationHeight);
-                    mIsTracking = true;
-                } else if (!isUp) {
-                    setNotificationViewClipBounds((int) actualNotificationHeight);
-                }
-            }
-            // if we return true the items in RV won't be scrollable.
-            return false;
-        }
-
-
-        @Override
-        public boolean onFling(MotionEvent event1, MotionEvent event2,
-                float velocityX, float velocityY) {
-            // should not fling if the touch does not start when view is at the bottom of the list.
-            if (!mNotificationListAtBottomAtTimeOfTouch) {
-                return false;
-            }
-            if (Math.abs(event1.getX() - event2.getX()) > SWIPE_MAX_OFF_PATH
-                    || Math.abs(velocityY) < SWIPE_THRESHOLD_VELOCITY) {
-                // swipe was not vertical or was not fast enough
-                return false;
-            }
-            boolean isUp = velocityY < 0;
-            if (isUp) {
-                close();
-                return true;
-            } else {
-                // we should close the shade
-                animateNotificationPanel(velocityY, false);
-            }
-            return false;
-        }
-
-        protected abstract void close();
-    }
-
-    /**
-     * To be installed on the nav bars.
-     */
-    private abstract class NavBarCloseNotificationGestureListener extends
-            CloseNotificationGestureListener {
-        @Override
-        public boolean onSingleTapUp(MotionEvent e) {
-            mClosingVelocity = DEFAULT_FLING_VELOCITY;
-            if (mPanelExpanded) {
-                close();
-            }
-            return super.onSingleTapUp(e);
-        }
-
-        @Override
-        public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
-                float distanceY) {
-            calculatePercentageFromBottom(event2.getRawY());
-            setNotificationViewClipBounds((int) event2.getRawY());
-            return true;
-        }
-    }
-
-    /**
      * To be installed on the handle bar.
      */
-    private class HandleBarCloseNotificationGestureListener extends
+    private class HandleBarCloseGestureListener extends
             GestureDetector.SimpleOnGestureListener {
 
         @Override
@@ -777,9 +436,8 @@
             // the handle bar we should calculate the height using the diff of event1 and event2.
             // This will help the notification shade to clip smoothly as the event2 value changes
             // as event1 value will be fixed.
-            int clipHeight =
-                    mNotificationView.getHeight() - (int) (event1.getRawY() - event2.getRawY());
-            setNotificationViewClipBounds(clipHeight);
+            int clipHeight = getLayout().getHeight() - (int) (event1.getRawY() - event2.getRawY());
+            setViewClipBounds(clipHeight);
             return true;
         }
     }
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewMediator.java b/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewMediator.java
index 110c2ee..9d71797 100644
--- a/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewMediator.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewMediator.java
@@ -59,13 +59,13 @@
     @Override
     public void registerListeners() {
         mCarNavigationBarController.registerTopBarTouchListener(
-                mNotificationPanelViewController.getTopNavBarNotificationTouchListener());
+                mNotificationPanelViewController.getDragOpenTouchListener());
         mCarNavigationBarController.registerBottomBarTouchListener(
-                mNotificationPanelViewController.getNavBarNotificationTouchListener());
+                mNotificationPanelViewController.getDragCloseTouchListener());
         mCarNavigationBarController.registerLeftBarTouchListener(
-                mNotificationPanelViewController.getNavBarNotificationTouchListener());
+                mNotificationPanelViewController.getDragCloseTouchListener());
         mCarNavigationBarController.registerRightBarTouchListener(
-                mNotificationPanelViewController.getNavBarNotificationTouchListener());
+                mNotificationPanelViewController.getDragCloseTouchListener());
 
         mCarNavigationBarController.registerNotificationController(
                 new CarNavigationBarController.NotificationsShadeController() {
diff --git a/packages/CarSystemUI/src/com/android/systemui/window/OverlayPanelViewController.java b/packages/CarSystemUI/src/com/android/systemui/window/OverlayPanelViewController.java
new file mode 100644
index 0000000..58022f1
--- /dev/null
+++ b/packages/CarSystemUI/src/com/android/systemui/window/OverlayPanelViewController.java
@@ -0,0 +1,563 @@
+/*
+ * 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.window;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewTreeObserver;
+
+import androidx.annotation.CallSuper;
+
+import com.android.systemui.R;
+import com.android.systemui.car.CarDeviceProvisionedController;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.statusbar.FlingAnimationUtils;
+
+/**
+ * The {@link OverlayPanelViewController} provides additional dragging animation capabilities to
+ * {@link OverlayViewController}.
+ */
+public abstract class OverlayPanelViewController extends OverlayViewController {
+
+    private static final boolean DEBUG = true;
+    private static final String TAG = "OverlayPanelViewController";
+
+    // used to calculate how fast to open or close the window
+    protected static final float DEFAULT_FLING_VELOCITY = 0;
+    // max time a fling animation takes
+    protected static final float FLING_ANIMATION_MAX_TIME = 0.5f;
+    // acceleration rate for the fling animation
+    protected static final float FLING_SPEED_UP_FACTOR = 0.6f;
+
+    protected static final int SWIPE_DOWN_MIN_DISTANCE = 25;
+    protected static final int SWIPE_MAX_OFF_PATH = 75;
+    protected static final int SWIPE_THRESHOLD_VELOCITY = 200;
+
+    private final FlingAnimationUtils mFlingAnimationUtils;
+    private final CarDeviceProvisionedController mCarDeviceProvisionedController;
+    private final View.OnTouchListener mDragOpenTouchListener;
+    private final View.OnTouchListener mDragCloseTouchListener;
+
+    private final int mSettleClosePercentage;
+    private int mPercentageFromBottom;
+
+    private boolean mPanelVisible;
+    private boolean mPanelExpanded;
+
+    private float mOpeningVelocity = DEFAULT_FLING_VELOCITY;
+    private float mClosingVelocity = DEFAULT_FLING_VELOCITY;
+
+    private boolean mIsAnimating;
+    private boolean mIsTracking;
+
+    public OverlayPanelViewController(
+            Context context,
+            @Main Resources resources,
+            int stubId,
+            OverlayViewGlobalStateController overlayViewGlobalStateController,
+            FlingAnimationUtils.Builder flingAnimationUtilsBuilder,
+            CarDeviceProvisionedController carDeviceProvisionedController
+    ) {
+        super(stubId, overlayViewGlobalStateController);
+
+        mFlingAnimationUtils = flingAnimationUtilsBuilder
+                .setMaxLengthSeconds(FLING_ANIMATION_MAX_TIME)
+                .setSpeedUpFactor(FLING_SPEED_UP_FACTOR)
+                .build();
+        mCarDeviceProvisionedController = carDeviceProvisionedController;
+
+        mSettleClosePercentage = resources.getInteger(
+                R.integer.notification_settle_close_percentage);
+
+        // Attached to the top navigation bar (i.e. status bar) to detect pull down of the
+        // notification shade.
+        GestureDetector openGestureDetector = new GestureDetector(context,
+                new OpenGestureListener() {
+                    @Override
+                    protected void open() {
+                        animateExpandPanel();
+                    }
+                });
+
+        // Attached to the NavBars to close the notification shade
+        GestureDetector navBarCloseNotificationGestureDetector = new GestureDetector(context,
+                new SystemBarCloseGestureListener() {
+                    @Override
+                    protected void close() {
+                        if (isPanelExpanded()) {
+                            animateCollapsePanel();
+                        }
+                    }
+                });
+
+        mDragOpenTouchListener = (v, event) -> {
+            if (!mCarDeviceProvisionedController.isCurrentUserFullySetup()) {
+                return true;
+            }
+            if (!isInflated()) {
+                getOverlayViewGlobalStateController().inflateView(this);
+            }
+
+            boolean consumed = openGestureDetector.onTouchEvent(event);
+            if (consumed) {
+                return true;
+            }
+            maybeCompleteAnimation(event);
+            return true;
+        };
+
+        mDragCloseTouchListener = (v, event) -> {
+            if (!isInflated()) {
+                return true;
+            }
+            boolean consumed = navBarCloseNotificationGestureDetector.onTouchEvent(event);
+            if (consumed) {
+                return true;
+            }
+            maybeCompleteAnimation(event);
+            return true;
+        };
+    }
+
+    /** Toggles the visibility of the panel. */
+    public void toggle() {
+        if (!isInflated()) {
+            getOverlayViewGlobalStateController().inflateView(this);
+        }
+        if (isPanelExpanded()) {
+            animateCollapsePanel();
+        } else {
+            animateExpandPanel();
+        }
+    }
+
+    /* ***************************************************************************************** *
+     * Panel Animation
+     * ***************************************************************************************** */
+
+    /** Animates the closing of the panel. */
+    protected void animateCollapsePanel() {
+        if (!shouldAnimateCollapsePanel()) {
+            return;
+        }
+
+        if (!isPanelExpanded() || !isPanelVisible()) {
+            return;
+        }
+
+        onAnimateCollapsePanel();
+        getOverlayViewGlobalStateController().setWindowFocusable(false);
+        animatePanel(mClosingVelocity, /* isClosing= */ true);
+    }
+
+    /** Determines whether {@link #animateCollapsePanel()} should collapse the panel. */
+    protected abstract boolean shouldAnimateCollapsePanel();
+
+    /** Called when the panel is beginning to collapse. */
+    protected abstract void onAnimateCollapsePanel();
+
+    /** Animates the expansion of the panel. */
+    protected void animateExpandPanel() {
+        if (!shouldAnimateExpandPanel()) {
+            return;
+        }
+
+        if (!mCarDeviceProvisionedController.isCurrentUserFullySetup()) {
+            return;
+        }
+
+        onAnimateExpandPanel();
+        setPanelVisible(true);
+        animatePanel(mOpeningVelocity, /* isClosing= */ false);
+
+        setPanelExpanded(true);
+    }
+
+    /** Determines whether {@link #animateExpandPanel()}} should expand the panel. */
+    protected abstract boolean shouldAnimateExpandPanel();
+
+    /** Called when the panel is beginning to expand. */
+    protected abstract void onAnimateExpandPanel();
+
+    /**
+     * Depending on certain conditions, determines whether to fully expand or collapse the panel.
+     */
+    protected void maybeCompleteAnimation(MotionEvent event) {
+        if (event.getActionMasked() == MotionEvent.ACTION_UP
+                && isPanelVisible()) {
+            if (mSettleClosePercentage < mPercentageFromBottom) {
+                animatePanel(DEFAULT_FLING_VELOCITY, false);
+            } else {
+                animatePanel(DEFAULT_FLING_VELOCITY, true);
+            }
+        }
+    }
+
+    /**
+     * Animates the panel from one position to other. This is used to either open or
+     * close the panel completely with a velocity. If the animation is to close the
+     * panel this method also makes the view invisible after animation ends.
+     */
+    protected void animatePanel(float velocity, boolean isClosing) {
+        float to = 0;
+        if (!isClosing) {
+            to = getLayout().getHeight();
+        }
+
+        Rect rect = getLayout().getClipBounds();
+        if (rect != null && rect.bottom != to) {
+            float from = rect.bottom;
+            animate(from, to, velocity, isClosing);
+            return;
+        }
+
+        // We will only be here if the shade is being opened programmatically or via button when
+        // height of the layout was not calculated.
+        ViewTreeObserver notificationTreeObserver = getLayout().getViewTreeObserver();
+        notificationTreeObserver.addOnGlobalLayoutListener(
+                new ViewTreeObserver.OnGlobalLayoutListener() {
+                    @Override
+                    public void onGlobalLayout() {
+                        ViewTreeObserver obs = getLayout().getViewTreeObserver();
+                        obs.removeOnGlobalLayoutListener(this);
+                        float to = getLayout().getHeight();
+                        animate(/* from= */ 0, to, velocity, isClosing);
+                    }
+                });
+    }
+
+    private void animate(float from, float to, float velocity, boolean isClosing) {
+        if (mIsAnimating) {
+            return;
+        }
+        mIsAnimating = true;
+        mIsTracking = true;
+        ValueAnimator animator = ValueAnimator.ofFloat(from, to);
+        animator.addUpdateListener(
+                animation -> {
+                    float animatedValue = (Float) animation.getAnimatedValue();
+                    setViewClipBounds((int) animatedValue);
+                });
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                super.onAnimationEnd(animation);
+                mIsAnimating = false;
+                mIsTracking = false;
+                mOpeningVelocity = DEFAULT_FLING_VELOCITY;
+                mClosingVelocity = DEFAULT_FLING_VELOCITY;
+                if (isClosing) {
+                    setPanelVisible(false);
+                    getLayout().setClipBounds(null);
+                    onCollapseAnimationEnd();
+                    setPanelExpanded(false);
+                } else {
+                    onExpandAnimationEnd();
+                    setPanelExpanded(true);
+                }
+            }
+        });
+        getFlingAnimationUtils().apply(animator, from, to, Math.abs(velocity));
+        animator.start();
+    }
+
+    /**
+     * Called in {@link Animator.AnimatorListener#onAnimationEnd(Animator)} when the panel is
+     * closing.
+     */
+    protected abstract void onCollapseAnimationEnd();
+
+    /**
+     * Called in {@link Animator.AnimatorListener#onAnimationEnd(Animator)} when the panel is
+     * opening.
+     */
+    protected abstract void onExpandAnimationEnd();
+
+    /* ***************************************************************************************** *
+     * Panel Visibility
+     * ***************************************************************************************** */
+
+    /** Set the panel view to be visible. */
+    protected final void setPanelVisible(boolean visible) {
+        mPanelVisible = visible;
+        onPanelVisible(visible);
+    }
+
+    /** Returns {@code true} if panel is visible. */
+    public final boolean isPanelVisible() {
+        return mPanelVisible;
+    }
+
+    /** Business logic run when panel visibility is set. */
+    @CallSuper
+    protected void onPanelVisible(boolean visible) {
+        if (DEBUG) {
+            Log.e(TAG, "onPanelVisible: " + visible);
+        }
+
+        if (visible && !getOverlayViewGlobalStateController().isWindowVisible()) {
+            getOverlayViewGlobalStateController().setWindowVisible(true);
+        }
+        if (!visible && getOverlayViewGlobalStateController().isWindowVisible()) {
+            getOverlayViewGlobalStateController().setWindowVisible(false);
+        }
+        getLayout().setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+        getOverlayViewGlobalStateController().setWindowFocusable(visible);
+    }
+
+    /* ***************************************************************************************** *
+     * Panel Expansion
+     * ***************************************************************************************** */
+
+    /**
+     * Set the panel state to expanded. This will expand or collapse the overlay window if
+     * necessary.
+     */
+    protected final void setPanelExpanded(boolean expand) {
+        mPanelExpanded = expand;
+        onPanelExpanded(expand);
+    }
+
+    /** Returns {@code true} if panel is expanded. */
+    public final boolean isPanelExpanded() {
+        return mPanelExpanded;
+    }
+
+    @CallSuper
+    protected void onPanelExpanded(boolean expand) {
+        if (DEBUG) {
+            Log.e(TAG, "onPanelExpanded: " + expand);
+        }
+    }
+
+    /* ***************************************************************************************** *
+     * Misc
+     * ***************************************************************************************** */
+
+    protected void calculatePercentageFromBottom(float height) {
+        if (getLayout().getHeight() > 0) {
+            mPercentageFromBottom = (int) Math.abs(
+                    height / getLayout().getHeight() * 100);
+        }
+    }
+
+    protected void setViewClipBounds(int height) {
+        if (height > getLayout().getHeight()) {
+            height = getLayout().getHeight();
+        }
+        Rect clipBounds = new Rect();
+        clipBounds.set(0, 0, getLayout().getWidth(), height);
+        getLayout().setClipBounds(clipBounds);
+        onScroll(height);
+    }
+
+    /** Called while scrolling. */
+    protected abstract void onScroll(int height);
+
+    /* ***************************************************************************************** *
+     * Getters
+     * ***************************************************************************************** */
+
+    /** Returns the open touch listener. */
+    public final View.OnTouchListener getDragOpenTouchListener() {
+        return mDragOpenTouchListener;
+    }
+
+    /** Returns the close touch listener. */
+    public final View.OnTouchListener getDragCloseTouchListener() {
+        return mDragCloseTouchListener;
+    }
+
+    /** Gets the fling animation utils used for animating this panel. */
+    protected final FlingAnimationUtils getFlingAnimationUtils() {
+        return mFlingAnimationUtils;
+    }
+
+    /** Returns {@code true} if the panel is currently tracking. */
+    protected final boolean isTracking() {
+        return mIsTracking;
+    }
+
+    /** Returns {@code true} if the panel is currently animating. */
+    protected final boolean isAnimating() {
+        return mIsAnimating;
+    }
+
+    /** Returns the percentage of the panel that is open from the bottom. */
+    protected final int getPercentageFromBottom() {
+        return mPercentageFromBottom;
+    }
+
+    /** Returns the percentage at which we've determined whether to open or close the panel. */
+    protected final int getSettleClosePercentage() {
+        return mSettleClosePercentage;
+    }
+
+    /* ***************************************************************************************** *
+     * Gesture Listeners
+     * ***************************************************************************************** */
+
+    /** Called when the user is beginning to scroll down the panel. */
+    protected abstract void onOpenScrollStart();
+
+    /**
+     * Only responsible for open hooks. Since once the panel opens it covers all elements
+     * there is no need to merge with close.
+     */
+    protected abstract class OpenGestureListener extends
+            GestureDetector.SimpleOnGestureListener {
+
+        @Override
+        public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
+                float distanceY) {
+
+            if (!isPanelVisible()) {
+                onOpenScrollStart();
+            }
+            setPanelVisible(true);
+
+            // clips the view for the notification shade when the user scrolls to open.
+            setViewClipBounds((int) event2.getRawY());
+
+            // Initially the scroll starts with height being zero. This checks protects from divide
+            // by zero error.
+            calculatePercentageFromBottom(event2.getRawY());
+
+            mIsTracking = true;
+            return true;
+        }
+
+
+        @Override
+        public boolean onFling(MotionEvent event1, MotionEvent event2,
+                float velocityX, float velocityY) {
+            if (velocityY > SWIPE_THRESHOLD_VELOCITY) {
+                mOpeningVelocity = velocityY;
+                open();
+                return true;
+            }
+            animatePanel(DEFAULT_FLING_VELOCITY, true);
+
+            return false;
+        }
+
+        protected abstract void open();
+    }
+
+    /** Determines whether the scroll event should allow closing of the panel. */
+    protected abstract boolean shouldAllowClosingScroll();
+
+    protected abstract class CloseGestureListener extends
+            GestureDetector.SimpleOnGestureListener {
+
+        @Override
+        public boolean onSingleTapUp(MotionEvent motionEvent) {
+            if (isPanelExpanded()) {
+                animatePanel(DEFAULT_FLING_VELOCITY, true);
+            }
+            return true;
+        }
+
+        @Override
+        public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
+                float distanceY) {
+            // should not clip while scroll to the bottom of the list.
+            if (!shouldAllowClosingScroll()) {
+                return false;
+            }
+            float actualNotificationHeight =
+                    getLayout().getHeight() - (event1.getRawY() - event2.getRawY());
+            if (actualNotificationHeight > getLayout().getHeight()) {
+                actualNotificationHeight = getLayout().getHeight();
+            }
+            if (getLayout().getHeight() > 0) {
+                mPercentageFromBottom = (int) Math.abs(
+                        actualNotificationHeight / getLayout().getHeight() * 100);
+                boolean isUp = distanceY > 0;
+
+                // This check is to figure out if onScroll was called while swiping the card at
+                // bottom of the list. At that time we should not allow notification shade to
+                // close. We are also checking for the upwards swipe gesture here because it is
+                // possible if a user is closing the notification shade and while swiping starts
+                // to open again but does not fling. At that time we should allow the
+                // notification shade to close fully or else it would stuck in between.
+                if (Math.abs(getLayout().getHeight() - actualNotificationHeight)
+                        > SWIPE_DOWN_MIN_DISTANCE && isUp) {
+                    setViewClipBounds((int) actualNotificationHeight);
+                    mIsTracking = true;
+                } else if (!isUp) {
+                    setViewClipBounds((int) actualNotificationHeight);
+                }
+            }
+            // if we return true the items in RV won't be scrollable.
+            return false;
+        }
+
+
+        @Override
+        public boolean onFling(MotionEvent event1, MotionEvent event2,
+                float velocityX, float velocityY) {
+            // should not fling if the touch does not start when view is at the bottom of the list.
+            if (!shouldAllowClosingScroll()) {
+                return false;
+            }
+            if (Math.abs(event1.getX() - event2.getX()) > SWIPE_MAX_OFF_PATH
+                    || Math.abs(velocityY) < SWIPE_THRESHOLD_VELOCITY) {
+                // swipe was not vertical or was not fast enough
+                return false;
+            }
+            boolean isUp = velocityY < 0;
+            if (isUp) {
+                close();
+                return true;
+            } else {
+                // we should close the shade
+                animatePanel(velocityY, false);
+            }
+            return false;
+        }
+
+        protected abstract void close();
+    }
+
+    protected abstract class SystemBarCloseGestureListener extends CloseGestureListener {
+        @Override
+        public boolean onSingleTapUp(MotionEvent e) {
+            mClosingVelocity = DEFAULT_FLING_VELOCITY;
+            if (isPanelExpanded()) {
+                close();
+            }
+            return super.onSingleTapUp(e);
+        }
+
+        @Override
+        public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
+                float distanceY) {
+            calculatePercentageFromBottom(event2.getRawY());
+            setViewClipBounds((int) event2.getRawY());
+            return true;
+        }
+    }
+}
diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/CarNavigationBarTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/CarNavigationBarTest.java
index f2748b8..76557fd 100644
--- a/packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/CarNavigationBarTest.java
+++ b/packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/CarNavigationBarTest.java
@@ -20,13 +20,14 @@
 import static org.mockito.Mockito.when;
 
 import android.os.Handler;
-import android.test.suitebuilder.annotation.SmallTest;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.testing.TestableResources;
 import android.view.LayoutInflater;
 import android.view.WindowManager;
 
+import androidx.test.filters.SmallTest;
+
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.car.CarDeviceProvisionedController;
diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/window/OverlayPanelViewControllerTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/window/OverlayPanelViewControllerTest.java
new file mode 100644
index 0000000..04f2d06
--- /dev/null
+++ b/packages/CarSystemUI/tests/src/com/android/systemui/window/OverlayPanelViewControllerTest.java
@@ -0,0 +1,431 @@
+/*
+ * 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.window;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.car.CarDeviceProvisionedController;
+import com.android.systemui.statusbar.FlingAnimationUtils;
+import com.android.systemui.tests.R;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+@SmallTest
+public class OverlayPanelViewControllerTest extends SysuiTestCase {
+    private TestOverlayPanelViewController mOverlayPanelViewController;
+    private ViewGroup mBaseLayout;
+
+    @Mock
+    private OverlayViewGlobalStateController mOverlayViewGlobalStateController;
+    @Mock
+    private FlingAnimationUtils.Builder mFlingAnimationUtilsBuilder;
+    @Mock
+    private FlingAnimationUtils mFlingAnimationUtils;
+    @Mock
+    private CarDeviceProvisionedController mCarDeviceProvisionedController;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mBaseLayout = (ViewGroup) LayoutInflater.from(mContext).inflate(
+                R.layout.overlay_view_controller_test, /* root= */ null);
+
+        when(mFlingAnimationUtilsBuilder.setMaxLengthSeconds(anyFloat())).thenReturn(
+                mFlingAnimationUtilsBuilder);
+        when(mFlingAnimationUtilsBuilder.setSpeedUpFactor(anyFloat())).thenReturn(
+                mFlingAnimationUtilsBuilder);
+        when(mFlingAnimationUtilsBuilder.build()).thenReturn(mFlingAnimationUtils);
+        mOverlayPanelViewController = new TestOverlayPanelViewController(
+                getContext(),
+                getContext().getOrCreateTestableResources().getResources(),
+                R.id.overlay_view_controller_stub,
+                mOverlayViewGlobalStateController,
+                mFlingAnimationUtilsBuilder,
+                mCarDeviceProvisionedController);
+    }
+
+    @Test
+    public void toggle_notInflated_inflates() {
+        assertThat(mOverlayPanelViewController.isInflated()).isFalse();
+
+        mOverlayPanelViewController.toggle();
+
+        verify(mOverlayViewGlobalStateController).inflateView(mOverlayPanelViewController);
+    }
+
+    @Test
+    public void toggle_inflated_doesNotInflate() {
+        mOverlayPanelViewController.inflate(mBaseLayout);
+        assertThat(mOverlayPanelViewController.isInflated()).isTrue();
+
+        mOverlayPanelViewController.toggle();
+
+        verify(mOverlayViewGlobalStateController, never()).inflateView(mOverlayPanelViewController);
+    }
+
+    @Test
+    public void toggle_notExpanded_panelExpands() {
+        mOverlayPanelViewController.inflate(mBaseLayout);
+        mOverlayPanelViewController.setPanelExpanded(false);
+
+        mOverlayPanelViewController.toggle();
+
+        assertThat(mOverlayPanelViewController.mAnimateExpandPanelCalled).isTrue();
+    }
+
+    @Test
+    public void toggle_expanded_panelCollapses() {
+        mOverlayPanelViewController.inflate(mBaseLayout);
+        mOverlayPanelViewController.setPanelExpanded(true);
+
+        mOverlayPanelViewController.toggle();
+
+        assertThat(mOverlayPanelViewController.mAnimateCollapsePanelCalled).isTrue();
+    }
+
+    @Test
+    public void animateCollapsePanel_shouldNotAnimateCollapsePanel_doesNotCollapse() {
+        mOverlayPanelViewController.inflate(mBaseLayout);
+        mOverlayPanelViewController.setShouldAnimateCollapsePanel(false);
+
+        mOverlayPanelViewController.animateCollapsePanel();
+
+        assertThat(mOverlayPanelViewController.mAnimateCollapsePanelCalled).isTrue();
+        assertThat(mOverlayPanelViewController.mOnAnimateCollapsePanelCalled).isFalse();
+    }
+
+    @Test
+    public void animateCollapsePanel_isNotExpanded_doesNotCollapse() {
+        mOverlayPanelViewController.inflate(mBaseLayout);
+        mOverlayPanelViewController.setShouldAnimateCollapsePanel(true);
+        mOverlayPanelViewController.setPanelExpanded(false);
+
+        mOverlayPanelViewController.animateCollapsePanel();
+
+        assertThat(mOverlayPanelViewController.mAnimateCollapsePanelCalled).isTrue();
+        assertThat(mOverlayPanelViewController.mOnAnimateCollapsePanelCalled).isFalse();
+    }
+
+    @Test
+    public void animateCollapsePanel_isNotVisible_doesNotCollapse() {
+        mOverlayPanelViewController.inflate(mBaseLayout);
+        mOverlayPanelViewController.setShouldAnimateCollapsePanel(true);
+        mOverlayPanelViewController.setPanelExpanded(true);
+        mOverlayPanelViewController.setPanelVisible(false);
+
+        mOverlayPanelViewController.animateCollapsePanel();
+
+        assertThat(mOverlayPanelViewController.mAnimateCollapsePanelCalled).isTrue();
+        assertThat(mOverlayPanelViewController.mOnAnimateCollapsePanelCalled).isFalse();
+    }
+
+    @Test
+    public void animateCollapsePanel_collapses() {
+        mOverlayPanelViewController.inflate(mBaseLayout);
+        mOverlayPanelViewController.setShouldAnimateCollapsePanel(true);
+        mOverlayPanelViewController.setPanelExpanded(true);
+        mOverlayPanelViewController.setPanelVisible(true);
+
+        mOverlayPanelViewController.animateCollapsePanel();
+
+        assertThat(mOverlayPanelViewController.mOnAnimateCollapsePanelCalled).isTrue();
+    }
+
+    @Test
+    public void animateCollapsePanel_removesWindowFocus() {
+        mOverlayPanelViewController.inflate(mBaseLayout);
+        mOverlayPanelViewController.setShouldAnimateCollapsePanel(true);
+        mOverlayPanelViewController.setPanelExpanded(true);
+        mOverlayPanelViewController.setPanelVisible(true);
+
+        mOverlayPanelViewController.animateCollapsePanel();
+
+        verify(mOverlayViewGlobalStateController).setWindowFocusable(false);
+    }
+
+    @Test
+    public void animateExpandPanel_shouldNotAnimateExpandPanel_doesNotExpand() {
+        mOverlayPanelViewController.inflate(mBaseLayout);
+        mOverlayPanelViewController.setShouldAnimateExpandPanel(false);
+
+        mOverlayPanelViewController.animateExpandPanel();
+
+        assertThat(mOverlayPanelViewController.mAnimateExpandPanelCalled).isTrue();
+        assertThat(mOverlayPanelViewController.mOnAnimateExpandPanelCalled).isFalse();
+    }
+
+    @Test
+    public void animateExpandPanel_userNotSetup_doesNotExpand() {
+        mOverlayPanelViewController.inflate(mBaseLayout);
+        mOverlayPanelViewController.setShouldAnimateExpandPanel(true);
+        when(mCarDeviceProvisionedController.isCurrentUserFullySetup()).thenReturn(false);
+
+        mOverlayPanelViewController.animateExpandPanel();
+
+        assertThat(mOverlayPanelViewController.mAnimateExpandPanelCalled).isTrue();
+        assertThat(mOverlayPanelViewController.mOnAnimateExpandPanelCalled).isFalse();
+    }
+
+    @Test
+    public void animateExpandPanel_expands() {
+        mOverlayPanelViewController.inflate(mBaseLayout);
+        mOverlayPanelViewController.setShouldAnimateExpandPanel(true);
+        when(mCarDeviceProvisionedController.isCurrentUserFullySetup()).thenReturn(true);
+
+        mOverlayPanelViewController.animateExpandPanel();
+
+        assertThat(mOverlayPanelViewController.mOnAnimateExpandPanelCalled).isTrue();
+    }
+
+    @Test
+    public void animateExpandPanel_setsPanelVisible() {
+        mOverlayPanelViewController.inflate(mBaseLayout);
+        mOverlayPanelViewController.setShouldAnimateExpandPanel(true);
+        when(mCarDeviceProvisionedController.isCurrentUserFullySetup()).thenReturn(true);
+
+        mOverlayPanelViewController.animateExpandPanel();
+
+        assertThat(mOverlayPanelViewController.isPanelVisible()).isTrue();
+    }
+
+    @Test
+    public void animateExpandPanel_setsPanelExpanded() {
+        mOverlayPanelViewController.inflate(mBaseLayout);
+        mOverlayPanelViewController.setShouldAnimateExpandPanel(true);
+        when(mCarDeviceProvisionedController.isCurrentUserFullySetup()).thenReturn(true);
+
+        mOverlayPanelViewController.animateExpandPanel();
+
+        assertThat(mOverlayPanelViewController.isPanelExpanded()).isTrue();
+    }
+
+    @Test
+    public void setPanelVisible_setTrue_windowNotVisible_setsWindowVisible() {
+        mOverlayPanelViewController.inflate(mBaseLayout);
+        when(mOverlayViewGlobalStateController.isWindowVisible()).thenReturn(false);
+
+        mOverlayPanelViewController.setPanelVisible(true);
+
+        verify(mOverlayViewGlobalStateController).setWindowVisible(true);
+    }
+
+    @Test
+    public void setPanelVisible_setTrue_windowVisible_doesNotSetWindowVisible() {
+        mOverlayPanelViewController.inflate(mBaseLayout);
+        when(mOverlayViewGlobalStateController.isWindowVisible()).thenReturn(true);
+
+        mOverlayPanelViewController.setPanelVisible(true);
+
+        verify(mOverlayViewGlobalStateController, never()).setWindowVisible(true);
+    }
+
+    @Test
+    public void setPanelVisible_setTrue_setLayoutVisible() {
+        mOverlayPanelViewController.inflate(mBaseLayout);
+        mOverlayPanelViewController.getLayout().setVisibility(View.INVISIBLE);
+
+        mOverlayPanelViewController.setPanelVisible(true);
+
+        assertThat(mOverlayPanelViewController.getLayout().getVisibility()).isEqualTo(View.VISIBLE);
+    }
+
+    @Test
+    public void setPanelVisible_setTrue_setWindowFocusable() {
+        mOverlayPanelViewController.inflate(mBaseLayout);
+        mOverlayPanelViewController.setPanelVisible(true);
+
+        verify(mOverlayViewGlobalStateController).setWindowFocusable(true);
+    }
+
+    @Test
+    public void setPanelVisible_setFalse_windowVisible_setsWindowNotVisible() {
+        mOverlayPanelViewController.inflate(mBaseLayout);
+        when(mOverlayViewGlobalStateController.isWindowVisible()).thenReturn(true);
+
+        mOverlayPanelViewController.setPanelVisible(false);
+
+        verify(mOverlayViewGlobalStateController).setWindowVisible(false);
+    }
+
+    @Test
+    public void setPanelVisible_setFalse_windowNotVisible_doesNotSetWindowNotVisible() {
+        mOverlayPanelViewController.inflate(mBaseLayout);
+        when(mOverlayViewGlobalStateController.isWindowVisible()).thenReturn(false);
+
+        mOverlayPanelViewController.setPanelVisible(false);
+
+        verify(mOverlayViewGlobalStateController, never()).setWindowVisible(false);
+    }
+
+    @Test
+    public void setPanelVisible_setFalse_setLayoutInvisible() {
+        mOverlayPanelViewController.inflate(mBaseLayout);
+        mOverlayPanelViewController.getLayout().setVisibility(View.VISIBLE);
+
+        mOverlayPanelViewController.setPanelVisible(false);
+
+        assertThat(mOverlayPanelViewController.getLayout().getVisibility()).isEqualTo(
+                View.INVISIBLE);
+    }
+
+    @Test
+    public void setPanelVisible_setFalse_setWindowNotFocusable() {
+        mOverlayPanelViewController.inflate(mBaseLayout);
+
+        mOverlayPanelViewController.setPanelVisible(false);
+
+        verify(mOverlayViewGlobalStateController).setWindowFocusable(false);
+    }
+
+    @Test
+    public void dragOpenTouchListener_isNotInflated_inflatesView() {
+        when(mCarDeviceProvisionedController.isCurrentUserFullySetup()).thenReturn(true);
+        assertThat(mOverlayPanelViewController.isInflated()).isFalse();
+
+        mOverlayPanelViewController.getDragOpenTouchListener().onTouch(/* v= */ null,
+                MotionEvent.obtain(/* downTime= */ 200, /* eventTime= */ 300,
+                        MotionEvent.ACTION_MOVE, /* x= */ 0, /* y= */ 0, /* metaState= */ 0));
+
+        verify(mOverlayViewGlobalStateController).inflateView(mOverlayPanelViewController);
+    }
+
+    private static class TestOverlayPanelViewController extends OverlayPanelViewController {
+
+        private boolean mShouldAnimateCollapsePanel;
+        private boolean mShouldAnimateExpandPanel;
+        private boolean mShouldAllowClosingScroll;
+
+        boolean mOnAnimateCollapsePanelCalled;
+        boolean mAnimateCollapsePanelCalled;
+        boolean mOnAnimateExpandPanelCalled;
+        boolean mAnimateExpandPanelCalled;
+        boolean mOnCollapseAnimationEndCalled;
+        boolean mOnExpandAnimationEndCalled;
+        boolean mOnOpenScrollStartEnd;
+        List<Integer> mOnScrollHeights;
+
+        TestOverlayPanelViewController(
+                Context context,
+                Resources resources,
+                int stubId,
+                OverlayViewGlobalStateController overlayViewGlobalStateController,
+                FlingAnimationUtils.Builder flingAnimationUtilsBuilder,
+                CarDeviceProvisionedController carDeviceProvisionedController) {
+            super(context, resources, stubId, overlayViewGlobalStateController,
+                    flingAnimationUtilsBuilder,
+                    carDeviceProvisionedController);
+
+            mOnScrollHeights = new ArrayList<>();
+        }
+
+        public void setShouldAnimateCollapsePanel(boolean shouldAnimate) {
+            mShouldAnimateCollapsePanel = shouldAnimate;
+        }
+
+        @Override
+        protected boolean shouldAnimateCollapsePanel() {
+            return mShouldAnimateCollapsePanel;
+        }
+
+        @Override
+        protected void animateCollapsePanel() {
+            super.animateCollapsePanel();
+            mAnimateCollapsePanelCalled = true;
+        }
+
+        @Override
+        protected void onAnimateCollapsePanel() {
+            mOnAnimateCollapsePanelCalled = true;
+        }
+
+        public void setShouldAnimateExpandPanel(boolean shouldAnimate) {
+            mShouldAnimateExpandPanel = shouldAnimate;
+        }
+
+        @Override
+        protected boolean shouldAnimateExpandPanel() {
+            return mShouldAnimateExpandPanel;
+        }
+
+        @Override
+        protected void animateExpandPanel() {
+            super.animateExpandPanel();
+            mAnimateExpandPanelCalled = true;
+        }
+
+        @Override
+        protected void onAnimateExpandPanel() {
+            mOnAnimateExpandPanelCalled = true;
+        }
+
+        @Override
+        protected void onCollapseAnimationEnd() {
+            mOnCollapseAnimationEndCalled = true;
+        }
+
+        @Override
+        protected void onExpandAnimationEnd() {
+            mOnExpandAnimationEndCalled = true;
+        }
+
+        @Override
+        protected void onScroll(int height) {
+            mOnScrollHeights.add(height);
+        }
+
+        @Override
+        protected void onOpenScrollStart() {
+            mOnOpenScrollStartEnd = true;
+        }
+
+        public void setShouldAllowClosingScroll(boolean shouldAllow) {
+            mShouldAllowClosingScroll = shouldAllow;
+        }
+
+        @Override
+        protected boolean shouldAllowClosingScroll() {
+            return mShouldAllowClosingScroll;
+        }
+    }
+}
diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/window/OverlayViewControllerTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/window/OverlayViewControllerTest.java
index 3be9626..3313261 100644
--- a/packages/CarSystemUI/tests/src/com/android/systemui/window/OverlayViewControllerTest.java
+++ b/packages/CarSystemUI/tests/src/com/android/systemui/window/OverlayViewControllerTest.java
@@ -43,7 +43,7 @@
 @TestableLooper.RunWithLooper
 @SmallTest
 public class OverlayViewControllerTest extends SysuiTestCase {
-    private MockOverlayViewController mOverlayViewController;
+    private TestOverlayViewController mOverlayViewController;
     private ViewGroup mBaseLayout;
 
     @Mock
@@ -56,7 +56,7 @@
     public void setUp() {
         MockitoAnnotations.initMocks(/* testClass= */ this);
 
-        mOverlayViewController = new MockOverlayViewController(R.id.overlay_view_controller_stub,
+        mOverlayViewController = new TestOverlayViewController(R.id.overlay_view_controller_stub,
                 mOverlayViewGlobalStateController);
 
         mBaseLayout = (ViewGroup) LayoutInflater.from(mContext).inflate(
@@ -130,12 +130,12 @@
         assertThat(mOverlayViewController.mHideInternalCalled).isFalse();
     }
 
-    private static class MockOverlayViewController extends OverlayViewController {
+    private static class TestOverlayViewController extends OverlayViewController {
         boolean mOnFinishInflateCalled = false;
         boolean mShowInternalCalled = false;
         boolean mHideInternalCalled = false;
 
-        MockOverlayViewController(int stubId,
+        TestOverlayViewController(int stubId,
                 OverlayViewGlobalStateController overlayViewGlobalStateController) {
             super(stubId, overlayViewGlobalStateController);
         }
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiEntryPreference.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiEntryPreference.java
index 8aa0aec..a53bc9f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiEntryPreference.java
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiEntryPreference.java
@@ -102,12 +102,11 @@
         // Turn off divider
         view.findViewById(R.id.two_target_divider).setVisibility(View.INVISIBLE);
 
-        // Enable the icon button when this Entry is a canManageSubscription entry.
+        // Enable the icon button when the help string in this WifiEntry is not null.
         final ImageButton imageButton = (ImageButton) view.findViewById(R.id.icon_button);
         final ImageView frictionImageView = (ImageView) view.findViewById(
                 R.id.friction_icon);
-        if (mWifiEntry.canManageSubscription() && !mWifiEntry.isSaved()
-                && !mWifiEntry.isSubscription()
+        if (mWifiEntry.getHelpUriString() != null
                 && mWifiEntry.getConnectedState() == WifiEntry.CONNECTED_STATE_DISCONNECTED) {
             final Drawable drawablehelp = getDrawable(R.drawable.ic_help);
             drawablehelp.setTintList(
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiEntryPreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiEntryPreferenceTest.java
index a9f31ce..46e699d 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiEntryPreferenceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiEntryPreferenceTest.java
@@ -64,7 +64,7 @@
 
     private static final String MOCK_TITLE = "title";
     private static final String MOCK_SUMMARY = "summary";
-
+    private static final String FAKE_URI_STRING = "fakeuri";
 
     @Before
     public void setUp() {
@@ -155,22 +155,23 @@
     }
 
     @Test
-    public void canManageSubscription_shouldSetImageButtonVisible() {
-        when(mMockWifiEntry.canManageSubscription()).thenReturn(true);
+    public void notNull_whenGetHelpUriString_shouldSetImageButtonVisible() {
+        when(mMockWifiEntry.getHelpUriString()).thenReturn(FAKE_URI_STRING);
         final WifiEntryPreference pref =
                 new WifiEntryPreference(mContext, mMockWifiEntry, mMockIconInjector);
         final LayoutInflater inflater = LayoutInflater.from(mContext);
         final View view = inflater.inflate(pref.getLayoutResource(), new LinearLayout(mContext),
                 false);
         final PreferenceViewHolder holder = PreferenceViewHolder.createInstanceForTests(view);
+
         pref.onBindViewHolder(holder);
 
         assertThat(view.findViewById(R.id.icon_button).getVisibility()).isEqualTo(View.VISIBLE);
     }
 
     @Test
-    public void helpButton_whenCanManageSubscription_shouldSetCorrectContentDescription() {
-        when(mMockWifiEntry.canManageSubscription()).thenReturn(true);
+    public void helpButton_whenGetHelpUriStringNotNull_shouldSetCorrectContentDescription() {
+        when(mMockWifiEntry.getHelpUriString()).thenReturn(FAKE_URI_STRING);
         final WifiEntryPreference pref =
                 new WifiEntryPreference(mContext, mMockWifiEntry, mMockIconInjector);
         final LayoutInflater inflater = LayoutInflater.from(mContext);
diff --git a/packages/SystemUI/res/layout/qs_media_panel.xml b/packages/SystemUI/res/layout/qs_media_panel.xml
index fc3bf94..e5ac5f8 100644
--- a/packages/SystemUI/res/layout/qs_media_panel.xml
+++ b/packages/SystemUI/res/layout/qs_media_panel.xml
@@ -23,7 +23,8 @@
     android:layout_height="match_parent"
     android:orientation="vertical"
     android:gravity="center_horizontal|fill_vertical"
-    android:padding="16dp"
+    android:paddingTop="@dimen/qs_media_panel_outer_padding"
+    android:paddingBottom="@dimen/qs_media_panel_outer_padding"
     android:background="@drawable/qs_media_background"
     >
 
@@ -42,7 +43,9 @@
             android:orientation="horizontal"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:layout_marginBottom="16dp"
+            android:layout_marginBottom="@dimen/qs_media_panel_outer_padding"
+            android:paddingStart="@dimen/qs_media_panel_outer_padding"
+            android:paddingEnd="16dp"
         >
 
             <ImageView
@@ -139,6 +142,7 @@
         <!-- Seek Bar -->
         <SeekBar
             android:id="@+id/media_progress_bar"
+            style="@android:style/Widget.ProgressBar.Horizontal"
             android:clickable="true"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
@@ -154,6 +158,9 @@
             android:id="@+id/notification_media_progress_time"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
+            android:paddingStart="@dimen/qs_media_panel_outer_padding"
+            android:paddingEnd="@dimen/qs_media_panel_outer_padding"
+            android:layout_marginBottom="10dp"
             android:layout_gravity="center"
             >
             <!-- width is set to "match_parent" to avoid extra layout calls -->
@@ -184,6 +191,8 @@
             android:layoutDirection="ltr"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
+            android:paddingStart="@dimen/qs_media_panel_outer_padding"
+            android:paddingEnd="@dimen/qs_media_panel_outer_padding"
             android:gravity="center"
             >
             <ImageButton
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index bce5fac..344479f 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1211,6 +1211,7 @@
     <!-- Size of media cards in the QSPanel carousel -->
     <dimen name="qs_media_width">350dp</dimen>
     <dimen name="qs_media_padding">8dp</dimen>
+    <dimen name="qs_media_panel_outer_padding">16dp</dimen>
     <dimen name="qs_media_corner_radius">10dp</dimen>
     <dimen name="qs_media_album_size">72dp</dimen>
     <dimen name="qs_seamless_icon_size">20dp</dimen>
diff --git a/packages/SystemUI/src/com/android/keyguard/AdminSecondaryLockScreenController.java b/packages/SystemUI/src/com/android/keyguard/AdminSecondaryLockScreenController.java
index 7eb5a8f..e99245f 100644
--- a/packages/SystemUI/src/com/android/keyguard/AdminSecondaryLockScreenController.java
+++ b/packages/SystemUI/src/com/android/keyguard/AdminSecondaryLockScreenController.java
@@ -34,6 +34,8 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 
+import java.util.NoSuchElementException;
+
 /**
  * Encapsulates all logic for secondary lockscreen state management.
  */
@@ -79,7 +81,9 @@
     private final IKeyguardCallback mCallback = new IKeyguardCallback.Stub() {
         @Override
         public void onDismiss() {
-            dismiss(UserHandle.getCallingUserId());
+            mHandler.post(() -> {
+                dismiss(UserHandle.getCallingUserId());
+            });
         }
 
         @Override
@@ -91,7 +95,9 @@
             if (surfacePackage != null) {
                 mView.setChildSurfacePackage(surfacePackage);
             } else {
-                dismiss(KeyguardUpdateMonitor.getCurrentUser());
+                mHandler.post(() -> {
+                    dismiss(KeyguardUpdateMonitor.getCurrentUser());
+                });
             }
         }
     };
@@ -122,6 +128,7 @@
                         // If the remote content is not readied within the timeout period,
                         // move on without the secondary lockscreen.
                         dismiss(userId);
+                        Log.w(TAG, "Timed out waiting for secondary lockscreen content.");
                     },
                     REMOTE_CONTENT_READY_TIMEOUT_MILLIS);
         }
@@ -150,8 +157,12 @@
      * Displays the Admin security Surface view.
      */
     public void show(Intent serviceIntent) {
-        mContext.bindService(serviceIntent, mConnection, Context.BIND_AUTO_CREATE);
-        mParent.addView(mView);
+        if (mClient == null) {
+            mContext.bindService(serviceIntent, mConnection, Context.BIND_AUTO_CREATE);
+        }
+        if (!mView.isAttachedToWindow()) {
+            mParent.addView(mView);
+        }
     }
 
     /**
@@ -162,7 +173,11 @@
             mParent.removeView(mView);
         }
         if (mClient != null) {
-            mClient.asBinder().unlinkToDeath(mKeyguardClientDeathRecipient, 0);
+            try {
+                mClient.asBinder().unlinkToDeath(mKeyguardClientDeathRecipient, 0);
+            } catch (NoSuchElementException e) {
+                Log.w(TAG, "IKeyguardClient death recipient already released");
+            }
             mContext.unbindService(mConnection);
             mClient = null;
         }
@@ -185,10 +200,12 @@
 
     private void dismiss(int userId) {
         mHandler.removeCallbacksAndMessages(null);
-        if (mView != null && mView.isAttachedToWindow()
-                && userId == KeyguardUpdateMonitor.getCurrentUser()) {
+        if (mView.isAttachedToWindow() && userId == KeyguardUpdateMonitor.getCurrentUser()) {
             hide();
-            mKeyguardCallback.dismiss(true, userId);
+            if (mKeyguardCallback != null) {
+                mKeyguardCallback.dismiss(/* securityVerified= */ true, userId,
+                        /* bypassSecondaryLockScreen= */true);
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java
index d5a08dd..aa2fe3c 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java
@@ -85,7 +85,8 @@
                         // the user proved presence via some other way to the trust agent.
                         Log.i(TAG, "TrustAgent dismissed Keyguard.");
                     }
-                    dismiss(false /* authenticated */, userId);
+                    dismiss(false /* authenticated */, userId,
+                            /* bypassSecondaryLockScreen */ false);
                 } else {
                     mViewMediatorCallback.playTrustedSound();
                 }
@@ -190,7 +191,7 @@
      * @return True if the keyguard is done.
      */
     public boolean dismiss(int targetUserId) {
-        return dismiss(false, targetUserId);
+        return dismiss(false, targetUserId, false);
     }
 
     public boolean handleBackKey() {
@@ -206,8 +207,10 @@
     }
 
     @Override
-    public boolean dismiss(boolean authenticated, int targetUserId) {
-        return mSecurityContainer.showNextSecurityScreenOrFinish(authenticated, targetUserId);
+    public boolean dismiss(boolean authenticated, int targetUserId,
+            boolean bypassSecondaryLockScreen) {
+        return mSecurityContainer.showNextSecurityScreenOrFinish(authenticated, targetUserId,
+                bypassSecondaryLockScreen);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityCallback.java
index 49dcfff..e384727 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityCallback.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityCallback.java
@@ -25,6 +25,15 @@
     void dismiss(boolean securityVerified, int targetUserId);
 
     /**
+     * Dismiss the given security screen.
+     * @param securityVerified true if the user correctly entered credentials for the given screen.
+     * @param targetUserId a user that needs to be the foreground user at the dismissal completion.
+     * @param bypassSecondaryLockScreen true if the user can bypass the secondary lock screen,
+     *                                  if any, during this dismissal.
+     */
+    void dismiss(boolean securityVerified, int targetUserId, boolean bypassSecondaryLockScreen);
+
+    /**
      * Manually report user activity to keep the device awake.
      */
     void userActivity();
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index ba8a1a9..1e1ce4e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -115,7 +115,8 @@
 
     // Used to notify the container when something interesting happens.
     public interface SecurityCallback {
-        public boolean dismiss(boolean authenticated, int targetUserId);
+        public boolean dismiss(boolean authenticated, int targetUserId,
+                boolean bypassSecondaryLockScreen);
         public void userActivity();
         public void onSecurityModeChanged(SecurityMode securityMode, boolean needsInput);
 
@@ -504,9 +505,12 @@
      * @param authenticated true if the user entered the correct authentication
      * @param targetUserId a user that needs to be the foreground user at the finish (if called)
      *     completion.
+     * @param bypassSecondaryLockScreen true if the user is allowed to bypass the secondary
+     *     secondary lock screen requirement, if any.
      * @return true if keyguard is done
      */
-    boolean showNextSecurityScreenOrFinish(boolean authenticated, int targetUserId) {
+    boolean showNextSecurityScreenOrFinish(boolean authenticated, int targetUserId,
+            boolean bypassSecondaryLockScreen) {
         if (DEBUG) Log.d(TAG, "showNextSecurityScreenOrFinish(" + authenticated + ")");
         boolean finish = false;
         boolean strongAuth = false;
@@ -555,7 +559,7 @@
             }
         }
         // Check for device admin specified additional security measures.
-        if (finish) {
+        if (finish && !bypassSecondaryLockScreen) {
             Intent secondaryLockscreenIntent =
                     mUpdateMonitor.getSecondaryLockscreenRequirement(targetUserId);
             if (secondaryLockscreenIntent != null) {
@@ -636,8 +640,15 @@
             mUpdateMonitor.cancelFaceAuth();
         }
 
+        @Override
         public void dismiss(boolean authenticated, int targetId) {
-            mSecurityCallback.dismiss(authenticated, targetId);
+            dismiss(authenticated, targetId, /* bypassSecondaryLockScreen */ false);
+        }
+
+        @Override
+        public void dismiss(boolean authenticated, int targetId,
+                boolean bypassSecondaryLockScreen) {
+            mSecurityCallback.dismiss(authenticated, targetId, bypassSecondaryLockScreen);
         }
 
         public boolean isVerifyUnlockOnly() {
@@ -689,6 +700,9 @@
         @Override
         public void dismiss(boolean securityVerified, int targetUserId) { }
         @Override
+        public void dismiss(boolean authenticated, int targetId,
+                boolean bypassSecondaryLockScreen) { }
+        @Override
         public void onUserInput() { }
         @Override
         public void reset() {}
diff --git a/packages/SystemUI/src/com/android/systemui/ForegroundServiceLifetimeExtender.java b/packages/SystemUI/src/com/android/systemui/ForegroundServiceLifetimeExtender.java
index 362014f..e17d4e6 100644
--- a/packages/SystemUI/src/com/android/systemui/ForegroundServiceLifetimeExtender.java
+++ b/packages/SystemUI/src/com/android/systemui/ForegroundServiceLifetimeExtender.java
@@ -25,6 +25,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.statusbar.NotificationLifetimeExtender;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.util.time.SystemClock;
 
 /**
  * Extends the lifetime of foreground notification services such that they show for at least
@@ -39,8 +40,10 @@
     private NotificationSafeToRemoveCallback mNotificationSafeToRemoveCallback;
     private ArraySet<NotificationEntry> mManagedEntries = new ArraySet<>();
     private Handler mHandler = new Handler(Looper.getMainLooper());
+    private final SystemClock mSystemClock;
 
-    public ForegroundServiceLifetimeExtender() {
+    public ForegroundServiceLifetimeExtender(SystemClock systemClock) {
+        mSystemClock = systemClock;
     }
 
     @Override
@@ -55,8 +58,8 @@
             return false;
         }
 
-        long currentTime = System.currentTimeMillis();
-        return currentTime - entry.getSbn().getPostTime() < MIN_FGS_TIME_MS;
+        long currentTime = mSystemClock.uptimeMillis();
+        return currentTime - entry.getCreationTime() < MIN_FGS_TIME_MS;
     }
 
     @Override
@@ -84,7 +87,7 @@
             }
         };
         long delayAmt = MIN_FGS_TIME_MS
-                - (System.currentTimeMillis() - entry.getSbn().getPostTime());
+                - (mSystemClock.uptimeMillis() - entry.getCreationTime());
         mHandler.postDelayed(r, delayAmt);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/ForegroundServiceNotificationListener.java b/packages/SystemUI/src/com/android/systemui/ForegroundServiceNotificationListener.java
index f01fa81..ef1f4e0 100644
--- a/packages/SystemUI/src/com/android/systemui/ForegroundServiceNotificationListener.java
+++ b/packages/SystemUI/src/com/android/systemui/ForegroundServiceNotificationListener.java
@@ -30,6 +30,7 @@
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
+import com.android.systemui.util.time.SystemClock;
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
@@ -49,7 +50,8 @@
     public ForegroundServiceNotificationListener(Context context,
             ForegroundServiceController foregroundServiceController,
             NotificationEntryManager notificationEntryManager,
-            NotifPipeline notifPipeline) {
+            NotifPipeline notifPipeline,
+            SystemClock systemClock) {
         mContext = context;
         mForegroundServiceController = foregroundServiceController;
 
@@ -76,7 +78,8 @@
                 removeNotification(entry.getSbn());
             }
         });
-        mEntryManager.addNotificationLifetimeExtender(new ForegroundServiceLifetimeExtender());
+        mEntryManager.addNotificationLifetimeExtender(
+                new ForegroundServiceLifetimeExtender(systemClock));
 
         notifPipeline.addCollectionListener(new NotifCollectionListener() {
             @Override
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index 9d885fd..99e5eb6 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -719,13 +719,6 @@
     }
 
     /**
-     * Tell the stack of bubbles to expand.
-     */
-    public void expandStack() {
-        mBubbleData.setExpanded(true);
-    }
-
-    /**
      * Tell the stack of bubbles to collapse.
      */
     public void collapseStack() {
@@ -753,12 +746,6 @@
         return (isSummary && isSuppressedSummary) || isBubbleAndSuppressed;
     }
 
-    @VisibleForTesting
-    void selectBubble(String key) {
-        Bubble bubble = mBubbleData.getBubbleWithKey(key);
-        mBubbleData.setSelectedBubble(bubble);
-    }
-
     void promoteBubbleFromOverflow(Bubble bubble) {
         bubble.setInflateSynchronously(mInflateSynchronously);
         mBubbleData.promoteBubbleFromOverflow(bubble, mStackView, mBubbleIconFactory);
@@ -778,13 +765,6 @@
     }
 
     /**
-     * Tell the stack of bubbles to be dismissed, this will remove all of the bubbles in the stack.
-     */
-    void dismissStack(@DismissReason int reason) {
-        mBubbleData.dismissAll(reason);
-    }
-
-    /**
      * Directs a back gesture at the bubble stack. When opened, the current expanded bubble
      * is forwarded a back key down/up pair.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
index be9cd5f..4c149dd 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
@@ -28,6 +28,7 @@
 import android.service.notification.NotificationListenerService;
 import android.util.Log;
 import android.util.Pair;
+import android.view.View;
 
 import androidx.annotation.Nullable;
 
@@ -751,6 +752,7 @@
     }
 
     @VisibleForTesting(visibility = PRIVATE)
+    @Nullable
     Bubble getBubbleWithKey(String key) {
         for (int i = 0; i < mBubbles.size(); i++) {
             Bubble bubble = mBubbles.get(i);
@@ -761,6 +763,17 @@
         return null;
     }
 
+    @Nullable
+    Bubble getBubbleWithView(View view) {
+        for (int i = 0; i < mBubbles.size(); i++) {
+            Bubble bubble = mBubbles.get(i);
+            if (bubble.getIconView() != null && bubble.getIconView().equals(view)) {
+                return bubble;
+            }
+        }
+        return null;
+    }
+
     @VisibleForTesting(visibility = PRIVATE)
     Bubble getOverflowBubbleWithKey(String key) {
         for (int i = 0; i < mOverflowBubbles.size(); i++) {
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
index 496456d..93fb697 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.bubbles;
 
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
 import static android.view.Display.INVALID_DISPLAY;
@@ -126,6 +127,7 @@
                     ActivityOptions options = ActivityOptions.makeCustomAnimation(getContext(),
                             0 /* enterResId */, 0 /* exitResId */);
                     options.setTaskAlwaysOnTop(true);
+                    options.setLaunchWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
                     // Post to keep the lifecycle normal
                     post(() -> {
                         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index 044feaa..644e54f 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -30,6 +30,7 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
+import android.annotation.SuppressLint;
 import android.app.Notification;
 import android.content.Context;
 import android.content.res.Configuration;
@@ -43,6 +44,7 @@
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.RectF;
+import android.graphics.Region;
 import android.os.Bundle;
 import android.os.Vibrator;
 import android.util.Log;
@@ -83,6 +85,7 @@
 import com.android.systemui.shared.system.SysUiStatsLog;
 import com.android.systemui.util.DismissCircleView;
 import com.android.systemui.util.FloatingContentCoordinator;
+import com.android.systemui.util.RelativeTouchListener;
 import com.android.systemui.util.animation.PhysicsAnimator;
 import com.android.systemui.util.magnetictarget.MagnetizedObject;
 
@@ -239,7 +242,6 @@
         mExpandedAnimationController.dump(fd, pw, args);
     }
 
-    private BubbleTouchHandler mTouchHandler;
     private BubbleController.BubbleExpandListener mExpandListener;
     private SysUiState mSysUiState;
 
@@ -296,7 +298,7 @@
 
                 @Override
                 public void setValue(Object o, float v) {
-                    onFlyoutDragged(v);
+                    setFlyoutStateForDragLength(v);
                 }
             };
 
@@ -337,13 +339,6 @@
     private MagnetizedObject<?> mMagnetizedObject;
 
     /**
-     * The action to run when the magnetized object is released in the dismiss target.
-     *
-     * This will actually perform the dismissal of either the stack or an individual bubble.
-     */
-    private Runnable mReleasedInDismissTargetAction;
-
-    /**
      * The MagneticTarget instance for our circular dismiss view. This is added to the
      * MagnetizedObject instances for the stack and any dragged-out bubbles.
      */
@@ -377,7 +372,7 @@
                 public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
                     mExpandedAnimationController.dismissDraggedOutBubble(
                             mExpandedAnimationController.getDraggedOutBubble(),
-                            mReleasedInDismissTargetAction);
+                            BubbleStackView.this::dismissMagnetizedObject);
                     hideDismissTarget();
                 }
             };
@@ -410,7 +405,7 @@
                     mStackAnimationController.implodeStack(
                             () -> {
                                 resetDesaturationAndDarken();
-                                mReleasedInDismissTargetAction.run();
+                                dismissMagnetizedObject();
                             }
                     );
 
@@ -418,6 +413,197 @@
                 }
             };
 
+    /**
+     * Click listener set on each bubble view. When collapsed, clicking a bubble expands the stack.
+     * When expanded, clicking a bubble either expands that bubble, or collapses the stack.
+     */
+    private OnClickListener mBubbleClickListener = new OnClickListener() {
+        @Override
+        public void onClick(View view) {
+            final Bubble clickedBubble = mBubbleData.getBubbleWithView(view);
+
+            // If the bubble has since left us, ignore the click.
+            if (clickedBubble == null) {
+                return;
+            }
+
+            final boolean clickedBubbleIsCurrentlyExpandedBubble =
+                    clickedBubble.getKey().equals(mExpandedBubble.getKey());
+
+            if (isExpanded() && !clickedBubbleIsCurrentlyExpandedBubble) {
+                if (clickedBubble != mBubbleData.getSelectedBubble()) {
+                    // Select the clicked bubble.
+                    mBubbleData.setSelectedBubble(clickedBubble);
+                } else {
+                    // If the clicked bubble is the selected bubble (but not the expanded bubble),
+                    // that means overflow was previously expanded. Set the selected bubble
+                    // internally without going through BubbleData (which would ignore it since it's
+                    // already selected).
+                    setSelectedBubble(clickedBubble);
+
+                }
+            } else {
+                // Otherwise, we either tapped the stack (which means we're collapsed
+                // and should expand) or the currently selected bubble (we're expanded
+                // and should collapse).
+                if (!maybeShowStackUserEducation()) {
+                    mBubbleData.setExpanded(!mBubbleData.isExpanded());
+                }
+            }
+        }
+    };
+
+    /**
+     * Touch listener set on each bubble view. This enables dragging and dismissing the stack (when
+     * collapsed), or individual bubbles (when expanded).
+     */
+    private RelativeTouchListener mBubbleTouchListener = new RelativeTouchListener() {
+
+        @Override
+        public boolean onDown(@NonNull View v, @NonNull MotionEvent ev) {
+            // If we're expanding or collapsing, consume but ignore all touch events.
+            if (mIsExpansionAnimating) {
+                return true;
+            }
+
+            if (mBubbleData.isExpanded()) {
+                maybeShowManageEducation(false /* show */);
+
+                // If we're expanded, tell the animation controller to prepare to drag this bubble,
+                // dispatching to the individual bubble magnet listener.
+                mExpandedAnimationController.prepareForBubbleDrag(
+                        v /* bubble */,
+                        mMagneticTarget,
+                        mIndividualBubbleMagnetListener);
+
+                // Save the magnetized individual bubble so we can dispatch touch events to it.
+                mMagnetizedObject = mExpandedAnimationController.getMagnetizedBubbleDraggingOut();
+            } else {
+                // If we're collapsed, prepare to drag the stack. Cancel active animations, set the
+                // animation controller, and hide the flyout.
+                mStackAnimationController.cancelStackPositionAnimations();
+                mBubbleContainer.setActiveController(mStackAnimationController);
+                hideFlyoutImmediate();
+
+                // Also, save the magnetized stack so we can dispatch touch events to it.
+                mMagnetizedObject = mStackAnimationController.getMagnetizedStack(mMagneticTarget);
+                mMagnetizedObject.setMagnetListener(mStackMagnetListener);
+            }
+
+            passEventToMagnetizedObject(ev);
+
+            // Bubbles are always interested in all touch events!
+            return true;
+        }
+
+        @Override
+        public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
+                float viewInitialY, float dx, float dy) {
+            // If we're expanding or collapsing, ignore all touch events.
+            if (mIsExpansionAnimating) {
+                return;
+            }
+
+            // Show the dismiss target, if we haven't already.
+            springInDismissTargetMaybe();
+
+            // First, see if the magnetized object consumes the event - if so, we shouldn't move the
+            // bubble since it's stuck to the target.
+            if (!passEventToMagnetizedObject(ev)) {
+                if (mBubbleData.isExpanded()) {
+                    mExpandedAnimationController.dragBubbleOut(
+                            v, viewInitialX + dx, viewInitialY + dy);
+                } else {
+                    hideStackUserEducation(false /* fromExpansion */);
+                    mStackAnimationController.moveStackFromTouch(
+                            viewInitialX + dx, viewInitialY + dy);
+                }
+            }
+        }
+
+        @Override
+        public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
+                float viewInitialY, float dx, float dy, float velX, float velY) {
+            // If we're expanding or collapsing, ignore all touch events.
+            if (mIsExpansionAnimating) {
+                return;
+            }
+
+            // First, see if the magnetized object consumes the event - if so, the bubble was
+            // released in the target or flung out of it, and we should ignore the event.
+            if (!passEventToMagnetizedObject(ev)) {
+                if (mBubbleData.isExpanded()) {
+                    mExpandedAnimationController.snapBubbleBack(v, velX, velY);
+                } else {
+                    // Fling the stack to the edge, and save whether or not it's going to end up on
+                    // the left side of the screen.
+                    mStackOnLeftOrWillBe =
+                            mStackAnimationController.flingStackThenSpringToEdge(
+                                    viewInitialX + dx, velX, velY) <= 0;
+
+                    updateBubbleZOrdersAndDotPosition(true /* animate */);
+
+                    logBubbleEvent(null /* no bubble associated with bubble stack move */,
+                            SysUiStatsLog.BUBBLE_UICHANGED__ACTION__STACK_MOVED);
+                }
+
+                hideDismissTarget();
+            }
+        }
+    };
+
+    /** Click listener set on the flyout, which expands the stack when the flyout is tapped. */
+    private OnClickListener mFlyoutClickListener = new OnClickListener() {
+        @Override
+        public void onClick(View view) {
+            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();
+        }
+    };
+
+    /** Touch listener for the flyout. This enables the drag-to-dismiss gesture on the flyout. */
+    private RelativeTouchListener mFlyoutTouchListener = new RelativeTouchListener() {
+
+        @Override
+        public boolean onDown(@NonNull View v, @NonNull MotionEvent ev) {
+            mFlyout.removeCallbacks(mHideFlyout);
+            return true;
+        }
+
+        @Override
+        public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
+                float viewInitialY, float dx, float dy) {
+            setFlyoutStateForDragLength(dx);
+        }
+
+        @Override
+        public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
+                float viewInitialY, float dx, float dy, float velX, float velY) {
+            final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
+            final boolean metRequiredVelocity =
+                    onLeft ? velX < -FLYOUT_DISMISS_VELOCITY : velX > FLYOUT_DISMISS_VELOCITY;
+            final boolean metRequiredDeltaX =
+                    onLeft
+                            ? dx < -mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS
+                            : dx > mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS;
+            final boolean isCancelFling = onLeft ? velX > 0 : velX < 0;
+            final boolean shouldDismiss = metRequiredVelocity
+                    || (metRequiredDeltaX && !isCancelFling);
+
+            mFlyout.removeCallbacks(mHideFlyout);
+            animateFlyoutCollapsed(shouldDismiss, velX);
+
+            maybeShowStackUserEducation();
+        }
+    };
+
     private ViewGroup mDismissTargetContainer;
     private PhysicsAnimator<View> mDismissTargetAnimator;
     private PhysicsAnimator.SpringConfig mDismissTargetSpring = new PhysicsAnimator.SpringConfig(
@@ -436,6 +622,7 @@
     private BubbleManageEducationView mManageEducationView;
     private boolean mAnimatingManageEducationAway;
 
+    @SuppressLint("ClickableViewAccessibility")
     public BubbleStackView(Context context, BubbleData data,
             @Nullable SurfaceSynchronizer synchronizer,
             FloatingContentCoordinator floatingContentCoordinator,
@@ -444,8 +631,6 @@
 
         mBubbleData = data;
         mInflater = LayoutInflater.from(context);
-        mTouchHandler = new BubbleTouchHandler(this, data, context);
-        setOnTouchListener(mTouchHandler);
 
         mSysUiState = sysUiState;
 
@@ -641,6 +826,18 @@
             mDesaturateAndDarkenPaint.setColorFilter(new ColorMatrixColorFilter(animatedMatrix));
             mDesaturateAndDarkenTargetView.setLayerPaint(mDesaturateAndDarkenPaint);
         });
+
+        // If the stack itself is touched, it means none of its touchable views (bubbles, flyouts,
+        // ActivityViews, etc.) were touched. Collapse the stack if it's expanded.
+        setOnTouchListener((view, ev) -> {
+            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+                if (mBubbleData.isExpanded()) {
+                    mBubbleData.setExpanded(false);
+                }
+            }
+
+            return false;
+        });
     }
 
     private void setUpUserEducation() {
@@ -690,6 +887,7 @@
         }
     }
 
+    @SuppressLint("ClickableViewAccessibility")
     private void setUpFlyout() {
         if (mFlyout != null) {
             removeView(mFlyout);
@@ -699,6 +897,8 @@
         mFlyout.animate()
                 .setDuration(FLYOUT_ALPHA_ANIMATION_DURATION)
                 .setInterpolator(new AccelerateDecelerateInterpolator());
+        mFlyout.setOnClickListener(mFlyoutClickListener);
+        mFlyout.setOnTouchListener(mFlyoutTouchListener);
         addView(mFlyout, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
     }
 
@@ -718,6 +918,7 @@
         mBubbleContainer.addView(mBubbleOverflow.getBtn(), overflowBtnIndex,
                 new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
 
+        mBubbleOverflow.getBtn().setOnClickListener(v -> setSelectedBubble(mBubbleOverflow));
     }
     /**
      * Handle theme changes.
@@ -920,6 +1121,7 @@
     }
 
     // via BubbleData.Listener
+    @SuppressLint("ClickableViewAccessibility")
     void addBubble(Bubble bubble) {
         if (DEBUG_BUBBLE_STACK_VIEW) {
             Log.d(TAG, "addBubble: " + bubble);
@@ -944,6 +1146,9 @@
         bubble.getIconView().setDotPositionOnLeft(
                 !mStackOnLeftOrWillBe /* onLeft */, false /* animate */);
 
+        bubble.getIconView().setOnClickListener(mBubbleClickListener);
+        bubble.getIconView().setOnTouchListener(mBubbleTouchListener);
+
         mBubbleContainer.addView(bubble.getIconView(), 0,
                 new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
         ViewClippingUtil.setClippingDeactivated(bubble.getIconView(), true, mClippingParameters);
@@ -1009,10 +1214,6 @@
         updatePointerPosition();
     }
 
-    void showOverflow() {
-        setSelectedBubble(mBubbleOverflow);
-    }
-
     /**
      * Changes the currently selected bubble. If the stack is already expanded, the newly selected
      * bubble will be shown immediately. This does not change the expanded state or change the
@@ -1177,14 +1378,6 @@
         }
     }
 
-    /*
-     * Sets the action to run to dismiss the currently dragging object (either the stack or an
-     * individual bubble).
-     */
-    public void setReleasedInDismissTargetAction(Runnable action) {
-        mReleasedInDismissTargetAction = action;
-    }
-
     /**
      * Dismiss the stack of bubbles.
      *
@@ -1201,54 +1394,6 @@
     }
 
     /**
-     * @return the view the touch event is on
-     */
-    @Nullable
-    public View getTargetView(MotionEvent event) {
-        float x = event.getRawX();
-        float y = event.getRawY();
-        if (mIsExpanded) {
-            if (isIntersecting(mBubbleContainer, x, y)) {
-                if (BubbleExperimentConfig.allowBubbleOverflow(mContext)
-                        && isIntersecting(mBubbleOverflow.getBtn(), x, y)) {
-                    return mBubbleOverflow.getBtn();
-                }
-                // Could be tapping or dragging a bubble while expanded
-                for (int i = 0; i < getBubbleCount(); i++) {
-                    BadgedImageView view = (BadgedImageView) mBubbleContainer.getChildAt(i);
-                    if (isIntersecting(view, x, y)) {
-                        return view;
-                    }
-                }
-            }
-            BubbleExpandedView bev = (BubbleExpandedView) mExpandedViewContainer.getChildAt(0);
-            if (bev.intersectingTouchableContent((int) x, (int) y)) {
-                return bev;
-            }
-            // Outside of the parts we care about.
-            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;
-    }
-
-    View getFlyoutView() {
-        return mFlyout;
-    }
-
-    /**
      * @deprecated use {@link #setExpanded(boolean)} and
      * {@link BubbleData#setSelectedBubble(Bubble)}
      */
@@ -1385,124 +1530,70 @@
         }
     }
 
-    /** Called when the collapsed stack is tapped on. */
-    void onStackTapped() {
-        if (!maybeShowStackUserEducation()) {
-            mBubbleData.setExpanded(true);
-        }
+    /**
+     * This method is called by {@link android.app.ActivityView} because the BubbleStackView has a
+     * higher Z-index than the ActivityView (so that dragged-out bubbles are visible over the AV).
+     * ActivityView is asking BubbleStackView to subtract the stack's bounds from the provided
+     * touchable region, so that the ActivityView doesn't consume events meant for the stack. Due to
+     * the special nature of ActivityView, it does not respect the standard
+     * {@link #dispatchTouchEvent} and {@link #onInterceptTouchEvent} methods typically used for
+     * this purpose.
+     *
+     * BubbleStackView is MATCH_PARENT, so that bubbles can be positioned via their translation
+     * properties for performance reasons. This means that the default implementation of this method
+     * subtracts the entirety of the screen from the ActivityView's touchable region, resulting in
+     * it not receiving any touch events. This was previously addressed by returning false in the
+     * stack's {@link View#canReceivePointerEvents()} method, but this precluded the use of any
+     * touch handlers in the stack or its child views.
+     *
+     * To support touch handlers, we're overriding this method to leave the ActivityView's touchable
+     * region alone. The only touchable part of the stack that can ever overlap the AV is a
+     * dragged-out bubble that is animating back into the row of bubbles. It's not worth continually
+     * updating the touchable region to allow users to grab a bubble while it completes its ~50ms
+     * animation back to the bubble row.
+     *
+     * NOTE: Any future additions to the stack that obscure the ActivityView region will need their
+     * bounds subtracted here in order to receive touch events.
+     */
+    @Override
+    public void subtractObscuredTouchableRegion(Region touchableRegion, View view) {
+
     }
 
-    /** 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=" + ((BadgedImageView) bubble).getKey());
-        }
-
-        if (mBubbleOverflow != null && bubble.equals(mBubbleOverflow.getIconView())) {
-            return;
-        }
-
-        mExpandedAnimationController.prepareForBubbleDrag(bubble, mMagneticTarget);
-
-        // We're dragging an individual bubble, so set the magnetized object to the magnetized
-        // bubble.
-        mMagnetizedObject = mExpandedAnimationController.getMagnetizedBubbleDraggingOut();
-        mMagnetizedObject.setMagnetListener(mIndividualBubbleMagnetListener);
-
-        maybeShowManageEducation(false);
+    /**
+     * If you're here because you're not receiving touch events on a view that is a descendant of
+     * BubbleStackView, and you think BSV is intercepting them - it's not! You need to subtract the
+     * bounds of the view in question in {@link #subtractObscuredTouchableRegion}. The ActivityView
+     * consumes all touch events within its bounds, even for views like the BubbleStackView that are
+     * above it. It ignores typical view touch handling methods like this one and
+     * dispatchTouchEvent.
+     */
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        return super.onInterceptTouchEvent(ev);
     }
 
-    /** Called with the coordinates to which an individual bubble has been dragged. */
-    public void onBubbleDragged(View bubble, float x, float y) {
-        if (!mIsExpanded || mIsExpansionAnimating
-                || (mBubbleOverflow != null && bubble.equals(mBubbleOverflow.getIconView()))) {
-            return;
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent ev) {
+        boolean dispatched = super.dispatchTouchEvent(ev);
+
+        // If a new bubble arrives while the collapsed stack is being dragged, it will be positioned
+        // at the front of the stack (under the touch position). Subsequent ACTION_MOVE events will
+        // then be passed to the new bubble, which will not consume them since it hasn't received an
+        // ACTION_DOWN yet. Work around this by passing MotionEvents directly to the touch handler
+        // until the current gesture ends with an ACTION_UP event.
+        if (!dispatched && !mIsExpanded && mIsGestureInProgress) {
+            dispatched = mBubbleTouchListener.onTouch(this /* view */, ev);
         }
 
-        mExpandedAnimationController.dragBubbleOut(bubble, x, y);
-        springInDismissTarget();
+        mIsGestureInProgress =
+                ev.getAction() != MotionEvent.ACTION_UP
+                        && ev.getAction() != MotionEvent.ACTION_CANCEL;
+
+        return dispatched;
     }
 
-    /** Called when a drag operation on an individual bubble has finished. */
-    public void onBubbleDragFinish(
-            View bubble, float x, float y, float velX, float velY) {
-        if (DEBUG_BUBBLE_STACK_VIEW) {
-            Log.d(TAG, "onBubbleDragFinish: bubble=" + bubble);
-        }
-
-        if (!mIsExpanded || mIsExpansionAnimating
-                || (mBubbleOverflow != null && bubble.equals(mBubbleOverflow.getIconView()))) {
-            return;
-        }
-
-        mExpandedAnimationController.snapBubbleBack(bubble, velX, velY);
-        hideDismissTarget();
-    }
-
-    /** Expands the clicked bubble. */
-    public void expandBubble(Bubble bubble) {
-        if (bubble != null && bubble.equals(mBubbleData.getSelectedBubble())) {
-            // If the bubble we're supposed to expand is the selected bubble, that means the
-            // overflow bubble is currently expanded. Don't tell BubbleData to set this bubble as
-            // selected, since it already is. Just call the stack's setSelectedBubble to expand it.
-            setSelectedBubble(bubble);
-        } else {
-            mBubbleData.setSelectedBubble(bubble);
-        }
-    }
-
-    void onDragStart() {
-        if (DEBUG_BUBBLE_STACK_VIEW) {
-            Log.d(TAG, "onDragStart()");
-        }
-        if (mIsExpanded || mIsExpansionAnimating) {
-            if (DEBUG_BUBBLE_STACK_VIEW) {
-                Log.d(TAG, "mIsExpanded or mIsExpansionAnimating");
-            }
-            return;
-        }
-        mStackAnimationController.cancelStackPositionAnimations();
-        mBubbleContainer.setActiveController(mStackAnimationController);
-        hideFlyoutImmediate();
-
-        // Since we're dragging the stack, set the magnetized object to the magnetized stack.
-        mMagnetizedObject = mStackAnimationController.getMagnetizedStack(mMagneticTarget);
-        mMagnetizedObject.setMagnetListener(mStackMagnetListener);
-    }
-
-    void onDragged(float x, float y) {
-        if (mIsExpanded || mIsExpansionAnimating) {
-            return;
-        }
-
-        hideStackUserEducation(false /* fromExpansion */);
-        springInDismissTarget();
-        mStackAnimationController.moveStackFromTouch(x, y);
-    }
-
-    void onDragFinish(float x, float y, float velX, float velY) {
-        if (DEBUG_BUBBLE_STACK_VIEW) {
-            Log.d(TAG, "onDragFinish");
-        }
-
-        if (mIsExpanded || mIsExpansionAnimating) {
-            return;
-        }
-
-        final float newStackX = mStackAnimationController.flingStackThenSpringToEdge(x, velX, velY);
-        logBubbleEvent(null /* no bubble associated with bubble stack move */,
-                SysUiStatsLog.BUBBLE_UICHANGED__ACTION__STACK_MOVED);
-
-        mStackOnLeftOrWillBe = newStackX <= 0;
-        updateBubbleZOrdersAndDotPosition(true /* animate */);
-        hideDismissTarget();
-    }
-
-    void onFlyoutDragStart() {
-        mFlyout.removeCallbacks(mHideFlyout);
-    }
-
-    void onFlyoutDragged(float deltaX) {
+    void setFlyoutStateForDragLength(float deltaX) {
         // This shouldn't happen, but if it does, just wait until the flyout lays out. This method
         // is continually called.
         if (mFlyout.getWidth() <= 0) {
@@ -1538,61 +1629,29 @@
         mFlyout.setTranslationX(mFlyout.getRestingTranslationX() + overscrollTranslation);
     }
 
-    void onFlyoutTapped() {
-        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();
-    }
-
-    /**
-     * Called when the flyout drag has finished, and returns true if the gesture successfully
-     * dismissed the flyout.
-     */
-    void onFlyoutDragFinished(float deltaX, float velX) {
-        final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
-        final boolean metRequiredVelocity =
-                onLeft ? velX < -FLYOUT_DISMISS_VELOCITY : velX > FLYOUT_DISMISS_VELOCITY;
-        final boolean metRequiredDeltaX =
-                onLeft
-                        ? deltaX < -mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS
-                        : deltaX > mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS;
-        final boolean isCancelFling = onLeft ? velX > 0 : velX < 0;
-        final boolean shouldDismiss = metRequiredVelocity || (metRequiredDeltaX && !isCancelFling);
-
-        mFlyout.removeCallbacks(mHideFlyout);
-        animateFlyoutCollapsed(shouldDismiss, velX);
-
-        maybeShowStackUserEducation();
-    }
-
-    /**
-     * Called when the first touch event of a gesture (stack drag, bubble drag, flyout drag, etc.)
-     * is received.
-     */
-    void onGestureStart() {
-        mIsGestureInProgress = true;
-    }
-
-    /** Called when a gesture is completed or cancelled. */
-    void onGestureFinished() {
-        mIsGestureInProgress = false;
-
-        if (mIsExpanded) {
-            mExpandedAnimationController.onGestureFinished();
-        }
-    }
-
     /** Passes the MotionEvent to the magnetized object and returns true if it was consumed. */
-    boolean passEventToMagnetizedObject(MotionEvent event) {
+    private boolean passEventToMagnetizedObject(MotionEvent event) {
         return mMagnetizedObject != null && mMagnetizedObject.maybeConsumeMotionEvent(event);
     }
 
+    /**
+     * Dismisses the magnetized object - either an individual bubble, if we're expanded, or the
+     * stack, if we're collapsed.
+     */
+    private void dismissMagnetizedObject() {
+        if (mIsExpanded) {
+            final View draggedOutBubbleView = (View) mMagnetizedObject.getUnderlyingObject();
+            final Bubble draggedOutBubble = mBubbleData.getBubbleWithView(draggedOutBubbleView);
+
+            if (mBubbleData.hasBubbleWithKey(draggedOutBubble.getKey())) {
+                mBubbleData.notificationEntryRemoved(
+                        draggedOutBubble.getEntry(), BubbleController.DISMISS_USER_GESTURE);
+            }
+        } else {
+            mBubbleData.dismissAll(BubbleController.DISMISS_USER_GESTURE);
+        }
+    }
+
     /** Prepares and starts the desaturate/darken animation on the bubble stack. */
     private void animateDesaturateAndDarken(View targetView, boolean desaturateAndDarken) {
         mDesaturateAndDarkenTargetView = targetView;
@@ -1624,7 +1683,7 @@
     }
 
     /** Animates in the dismiss target. */
-    private void springInDismissTarget() {
+    private void springInDismissTargetMaybe() {
         if (mShowingDismiss) {
             return;
         }
@@ -1827,13 +1886,6 @@
         return 0;
     }
 
-    private boolean isIntersecting(View view, float x, float y) {
-        mTempLoc = view.getLocationOnScreen();
-        mTempRect.set(mTempLoc[0], mTempLoc[1], mTempLoc[0] + view.getWidth(),
-                mTempLoc[1] + view.getHeight());
-        return mTempRect.contains(x, y);
-    }
-
     private void requestUpdate() {
         if (mViewUpdatedRequested || mIsExpansionAnimating) {
             return;
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
deleted file mode 100644
index 132c45f..0000000
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
+++ /dev/null
@@ -1,221 +0,0 @@
-/*
- * Copyright (C) 2012 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.graphics.PointF;
-import android.view.MotionEvent;
-import android.view.VelocityTracker;
-import android.view.View;
-import android.view.ViewConfiguration;
-
-import com.android.systemui.Dependency;
-
-/**
- * Handles interpreting touches on a {@link BubbleStackView}. This includes expanding, collapsing,
- * dismissing, and flings.
- */
-class BubbleTouchHandler implements View.OnTouchListener {
-
-    private final PointF mTouchDown = new PointF();
-    private final PointF mViewPositionOnTouchDown = new PointF();
-    private final BubbleStackView mStack;
-    private final BubbleData mBubbleData;
-
-    private BubbleController mController = Dependency.get(BubbleController.class);
-
-    private boolean mMovedEnough;
-    private int mTouchSlopSquared;
-    private VelocityTracker mVelocityTracker;
-
-    /** View that was initially touched, when we received the first ACTION_DOWN event. */
-    private View mTouchedView;
-
-    BubbleTouchHandler(BubbleStackView stackView,
-            BubbleData bubbleData, Context context) {
-        final int touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
-        mTouchSlopSquared = touchSlop * touchSlop;
-        mBubbleData = bubbleData;
-        mStack = stackView;
-    }
-
-    @Override
-    public boolean onTouch(View v, MotionEvent event) {
-        final int action = event.getActionMasked();
-
-        // If we aren't currently in the process of touching a view, figure out what we're touching.
-        // It'll be the stack, an individual bubble, or nothing.
-        if (mTouchedView == null) {
-            mTouchedView = mStack.getTargetView(event);
-        }
-
-        // If this is an ACTION_OUTSIDE event, or the stack reported that we aren't touching
-        // anything, collapse the stack.
-        if (action == MotionEvent.ACTION_OUTSIDE || mTouchedView == null) {
-            mBubbleData.setExpanded(false);
-            mStack.hideStackUserEducation(false /* fromExpansion */);
-            resetForNextGesture();
-            return false;
-        }
-
-        if (!(mTouchedView instanceof BadgedImageView)
-                && !(mTouchedView instanceof BubbleStackView)
-                && !(mTouchedView instanceof BubbleFlyoutView)) {
-
-            // Not touching anything touchable, but we shouldn't collapse (e.g. touching edge
-            // of expanded view).
-            mStack.maybeShowManageEducation(false);
-            resetForNextGesture();
-            return false;
-        }
-
-        final boolean isStack = mStack.equals(mTouchedView);
-        final boolean isFlyout = mStack.getFlyoutView().equals(mTouchedView);
-        final float rawX = event.getRawX();
-        final float rawY = event.getRawY();
-
-        // The coordinates of the touch event, in terms of the touched view's position.
-        final float viewX = mViewPositionOnTouchDown.x + rawX - mTouchDown.x;
-        final float viewY = mViewPositionOnTouchDown.y + rawY - mTouchDown.y;
-        switch (action) {
-            case MotionEvent.ACTION_DOWN:
-                trackMovement(event);
-
-                mTouchDown.set(rawX, rawY);
-                mStack.onGestureStart();
-
-                if (isStack) {
-                    mViewPositionOnTouchDown.set(mStack.getStackPosition());
-
-                    // Dismiss the entire stack if it's released in the dismiss target.
-                    mStack.setReleasedInDismissTargetAction(
-                            () -> mController.dismissStack(BubbleController.DISMISS_USER_GESTURE));
-                    mStack.onDragStart();
-                    mStack.passEventToMagnetizedObject(event);
-                } else if (isFlyout) {
-                    mStack.onFlyoutDragStart();
-                } else {
-                    mViewPositionOnTouchDown.set(
-                            mTouchedView.getTranslationX(), mTouchedView.getTranslationY());
-
-                    // Dismiss only the dragged-out bubble if it's released in the target.
-                    final String individualBubbleKey = ((BadgedImageView) mTouchedView).getKey();
-                    mStack.setReleasedInDismissTargetAction(() -> {
-                        final Bubble bubble =
-                                mBubbleData.getBubbleWithKey(individualBubbleKey);
-                        // bubble can be null if the user is in the middle of
-                        // dismissing the bubble, but the app also sent a cancel
-                        if (bubble != null) {
-                            mController.removeBubble(bubble.getEntry(),
-                                    BubbleController.DISMISS_USER_GESTURE);
-                        }
-                    });
-
-                    mStack.onBubbleDragStart(mTouchedView);
-                    mStack.passEventToMagnetizedObject(event);
-                }
-
-                break;
-            case MotionEvent.ACTION_MOVE:
-                trackMovement(event);
-                final float deltaX = rawX - mTouchDown.x;
-                final float deltaY = rawY - mTouchDown.y;
-
-                if ((deltaX * deltaX) + (deltaY * deltaY) > mTouchSlopSquared && !mMovedEnough) {
-                    mMovedEnough = true;
-                }
-
-                if (mMovedEnough) {
-                    if (isFlyout) {
-                        mStack.onFlyoutDragged(deltaX);
-                    } else if (!mStack.passEventToMagnetizedObject(event)) {
-                        // If the magnetic target doesn't consume the event, drag the stack or
-                        // bubble.
-                        if (isStack) {
-                            mStack.onDragged(viewX, viewY);
-                        } else {
-                            mStack.onBubbleDragged(mTouchedView, viewX, viewY);
-                        }
-                    }
-                }
-                break;
-
-            case MotionEvent.ACTION_CANCEL:
-                resetForNextGesture();
-                break;
-
-            case MotionEvent.ACTION_UP:
-                trackMovement(event);
-                mVelocityTracker.computeCurrentVelocity(/* maxVelocity */ 1000);
-                final float velX = mVelocityTracker.getXVelocity();
-                final float velY = mVelocityTracker.getYVelocity();
-
-                if (isFlyout && mMovedEnough) {
-                    mStack.onFlyoutDragFinished(rawX - mTouchDown.x /* deltaX */, velX);
-                } else if (isFlyout) {
-                    if (!mBubbleData.isExpanded() && !mMovedEnough) {
-                        mStack.onFlyoutTapped();
-                    }
-                } else if (mMovedEnough) {
-                    if (!mStack.passEventToMagnetizedObject(event)) {
-                        // If the magnetic target didn't consume the event, tell the stack to finish
-                        // the drag.
-                        if (isStack) {
-                            mStack.onDragFinish(viewX, viewY, velX, velY);
-                        } else {
-                            mStack.onBubbleDragFinish(mTouchedView, viewX, viewY, velX, velY);
-                        }
-                    }
-                } else if (mTouchedView == mStack.getExpandedBubbleView()) {
-                    mBubbleData.setExpanded(false);
-                } else if (isStack) {
-                    mStack.onStackTapped();
-                } else {
-                    final String key = ((BadgedImageView) mTouchedView).getKey();
-                    if (key == BubbleOverflow.KEY) {
-                        mStack.showOverflow();
-                    } else {
-                        mStack.expandBubble(mBubbleData.getBubbleWithKey(key));
-                    }
-                }
-                resetForNextGesture();
-                break;
-        }
-
-        return true;
-    }
-
-    /** Clears all touch-related state. */
-    private void resetForNextGesture() {
-        if (mVelocityTracker != null) {
-            mVelocityTracker.recycle();
-            mVelocityTracker = null;
-        }
-
-        mTouchedView = null;
-        mMovedEnough = false;
-
-        mStack.onGestureFinished();
-    }
-
-    private void trackMovement(MotionEvent event) {
-        if (mVelocityTracker == null) {
-            mVelocityTracker = VelocityTracker.obtain();
-        }
-        mVelocityTracker.addMovement(event);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java
index a0b4938..d974adc 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java
@@ -252,8 +252,14 @@
         mSpringToTouchOnNextMotionEvent = true;
     }
 
-    /** Prepares the given bubble to be dragged out. */
-    public void prepareForBubbleDrag(View bubble, MagnetizedObject.MagneticTarget target) {
+    /**
+     * Prepares the given bubble view to be dragged out, using the provided magnetic target and
+     * listener.
+     */
+    public void prepareForBubbleDrag(
+            View bubble,
+            MagnetizedObject.MagneticTarget target,
+            MagnetizedObject.MagnetListener listener) {
         mLayout.cancelAnimationsOnView(bubble);
 
         bubble.setTranslationZ(Short.MAX_VALUE);
@@ -277,6 +283,7 @@
             }
         };
         mMagnetizedBubbleDraggingOut.addTarget(target);
+        mMagnetizedBubbleDraggingOut.setMagnetListener(listener);
         mMagnetizedBubbleDraggingOut.setHapticsEnabled(true);
         mMagnetizedBubbleDraggingOut.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java
index c292769..b1bbafc 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java
@@ -1117,9 +1117,4 @@
             mAssociatedController = controller;
         }
     }
-
-    @Override
-    protected boolean canReceivePointerEvents() {
-        return false;
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt
index 5d03fc5..7e8fec7 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt
@@ -45,7 +45,7 @@
     companion object {
         private const val TAG = "ControlsBindingControllerImpl"
         private const val MAX_CONTROLS_REQUEST = 100000L
-        private const val SUGGESTED_CONTROLS_REQUEST = 4L
+        private const val SUGGESTED_CONTROLS_REQUEST = 6L
     }
 
     private var currentUser = UserHandle.of(ActivityManager.getCurrentUser())
@@ -61,6 +61,11 @@
      */
     private var statefulControlSubscriber: StatefulControlSubscriber? = null
 
+    /*
+     * Will track any active load subscriber. Only one can be active at any time.
+     */
+    private var loadSubscriber: LoadSubscriber? = null
+
     private val actionCallbackService = object : IControlsActionCallback.Stub() {
         override fun accept(
             token: IBinder,
@@ -99,17 +104,24 @@
         component: ComponentName,
         callback: ControlsBindingController.LoadCallback
     ): Runnable {
-        val subscriber = LoadSubscriber(callback, MAX_CONTROLS_REQUEST)
-        retrieveLifecycleManager(component).maybeBindAndLoad(subscriber)
-        return subscriber.loadCancel()
+        loadSubscriber?.loadCancel()
+
+        val ls = LoadSubscriber(callback, MAX_CONTROLS_REQUEST)
+        loadSubscriber = ls
+
+        retrieveLifecycleManager(component).maybeBindAndLoad(ls)
+        return ls.loadCancel()
     }
 
     override fun bindAndLoadSuggested(
         component: ComponentName,
         callback: ControlsBindingController.LoadCallback
     ) {
-        val subscriber = LoadSubscriber(callback, SUGGESTED_CONTROLS_REQUEST)
-        retrieveLifecycleManager(component).maybeBindAndLoadSuggested(subscriber)
+        loadSubscriber?.loadCancel()
+        val ls = LoadSubscriber(callback, SUGGESTED_CONTROLS_REQUEST)
+        loadSubscriber = ls
+
+        retrieveLifecycleManager(component).maybeBindAndLoadSuggested(ls)
     }
 
     override fun subscribe(structureInfo: StructureInfo) {
@@ -152,13 +164,16 @@
     override fun changeUser(newUser: UserHandle) {
         if (newUser == currentUser) return
 
-        unsubscribe()
         unbind()
-        currentProvider = null
         currentUser = newUser
     }
 
     private fun unbind() {
+        unsubscribe()
+
+        loadSubscriber?.loadCancel()
+        loadSubscriber = null
+
         currentProvider?.unbindService()
         currentProvider = null
     }
@@ -210,6 +225,20 @@
         val callback: ControlsBindingController.LoadCallback
     ) : CallbackRunnable(token) {
         override fun doRun() {
+            Log.d(TAG, "LoadSubscription: Complete and loading controls")
+            callback.accept(list)
+        }
+    }
+
+    private inner class OnCancelAndLoadRunnable(
+        token: IBinder,
+        val list: List<Control>,
+        val subscription: IControlsSubscription,
+        val callback: ControlsBindingController.LoadCallback
+    ) : CallbackRunnable(token) {
+        override fun doRun() {
+            Log.d(TAG, "LoadSubscription: Canceling and loading controls")
+            provider?.cancelSubscription(subscription)
             callback.accept(list)
         }
     }
@@ -220,6 +249,7 @@
         val requestLimit: Long
     ) : CallbackRunnable(token) {
         override fun doRun() {
+            Log.d(TAG, "LoadSubscription: Starting subscription")
             provider?.startSubscription(subscription, requestLimit)
         }
     }
@@ -254,34 +284,54 @@
         val requestLimit: Long
     ) : IControlsSubscriber.Stub() {
         val loadedControls = ArrayList<Control>()
-        var hasError = false
+        private var isTerminated = false
         private var _loadCancelInternal: (() -> Unit)? = null
+        private lateinit var subscription: IControlsSubscription
+
         fun loadCancel() = Runnable {
-                Log.d(TAG, "Cancel load requested")
-                _loadCancelInternal?.invoke()
-            }
+            Log.d(TAG, "Cancel load requested")
+            _loadCancelInternal?.invoke()
+        }
 
         override fun onSubscribe(token: IBinder, subs: IControlsSubscription) {
-            _loadCancelInternal = subs::cancel
+            subscription = subs
+            _loadCancelInternal = { currentProvider?.cancelSubscription(subscription) }
             backgroundExecutor.execute(OnSubscribeRunnable(token, subs, requestLimit))
         }
 
         override fun onNext(token: IBinder, c: Control) {
-            backgroundExecutor.execute { loadedControls.add(c) }
+            backgroundExecutor.execute {
+                if (isTerminated) return@execute
+
+                loadedControls.add(c)
+
+                // Once we have reached our requestLimit, send a request to cancel, and immediately
+                // load the results. Calls to onError() and onComplete() are not required after
+                // cancel.
+                if (loadedControls.size >= requestLimit) {
+                    maybeTerminateAndRun(
+                        OnCancelAndLoadRunnable(token, loadedControls, subscription, callback)
+                    )
+                }
+            }
         }
+
         override fun onError(token: IBinder, s: String) {
-            hasError = true
-            _loadCancelInternal = {}
-            currentProvider?.cancelLoadTimeout()
-            backgroundExecutor.execute(OnLoadErrorRunnable(token, s, callback))
+            maybeTerminateAndRun(OnLoadErrorRunnable(token, s, callback))
         }
 
         override fun onComplete(token: IBinder) {
+            maybeTerminateAndRun(OnLoadRunnable(token, loadedControls, callback))
+        }
+
+        private fun maybeTerminateAndRun(postTerminateFn: Runnable) {
+            if (isTerminated) return
+
+            isTerminated = true
             _loadCancelInternal = {}
-            if (!hasError) {
-                currentProvider?.cancelLoadTimeout()
-                backgroundExecutor.execute(OnLoadRunnable(token, loadedControls, callback))
-            }
+            currentProvider?.cancelLoadTimeout()
+
+            backgroundExecutor.execute(postTerminateFn)
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
index ae75dd4..568fb28 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
@@ -180,6 +180,11 @@
     fun countFavoritesForComponent(componentName: ComponentName): Int
 
     /**
+     * TEMPORARY for testing
+     */
+    fun resetFavorites()
+
+    /**
      * Interface for structure to pass data to [ControlsFavoritingActivity].
      */
     interface LoadData {
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
index 3483339..8805694 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
@@ -365,6 +365,8 @@
         componentName: ComponentName,
         callback: Consumer<Boolean>
     ) {
+        if (seedingInProgress) return
+
         Log.i(TAG, "Beginning request to seed favorites for: $componentName")
         if (!confirmAvailability()) {
             if (userChanging) {
@@ -495,6 +497,13 @@
         }
     }
 
+    override fun resetFavorites() {
+        executor.execute {
+            Favorites.clear()
+            persistenceWrapper.storeFavorites(Favorites.getAllStructures())
+        }
+    }
+
     override fun refreshStatus(componentName: ComponentName, control: Control) {
         if (!confirmAvailability()) {
             Log.d(TAG, "Controls not available")
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt
index 895f1d2..a6af6a1 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt
@@ -63,8 +63,6 @@
 ) : IBinder.DeathRecipient {
 
     val token: IBinder = Binder()
-    @GuardedBy("subscriptions")
-    private val subscriptions = mutableListOf<IControlsSubscription>()
     private var requiresBound = false
     @GuardedBy("queuedServiceMethods")
     private val queuedServiceMethods: MutableSet<ServiceMethod> = ArraySet()
@@ -194,7 +192,7 @@
      * Request a call to [IControlsProvider.loadSuggested].
      *
      * If the service is not bound, the call will be queued and the service will be bound first.
-     * The service will be unbound after the controls are returned or the call times out.
+     * The service will be unbound if the call times out.
      *
      * @param subscriber the subscriber that manages coordination for loading controls
      */
@@ -245,9 +243,7 @@
         if (DEBUG) {
             Log.d(TAG, "startSubscription: $subscription")
         }
-        synchronized(subscriptions) {
-            subscriptions.add(subscription)
-        }
+
         wrapper?.request(subscription, requestLimit)
     }
 
@@ -261,9 +257,7 @@
         if (DEBUG) {
             Log.d(TAG, "cancelSubscription: $subscription")
         }
-        synchronized(subscriptions) {
-            subscriptions.remove(subscription)
-        }
+
         wrapper?.cancel(subscription)
     }
 
@@ -281,17 +275,6 @@
         onLoadCanceller?.run()
         onLoadCanceller = null
 
-        // be sure to cancel all subscriptions
-        val subs = synchronized(subscriptions) {
-            ArrayList(subscriptions).also {
-                subscriptions.clear()
-            }
-        }
-
-        subs.forEach {
-            wrapper?.cancel(it)
-        }
-
         bindService(false)
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
index 208d911..02c6ff6 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
@@ -16,18 +16,26 @@
 
 package com.android.systemui.controls.ui
 
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ObjectAnimator
+import android.app.AlertDialog
 import android.app.Dialog
 import android.content.ComponentName
 import android.content.Context
+import android.content.DialogInterface
 import android.content.Intent
 import android.content.SharedPreferences
 import android.content.res.Configuration
 import android.graphics.drawable.Drawable
 import android.graphics.drawable.LayerDrawable
+import android.os.Process
 import android.service.controls.Control
 import android.service.controls.actions.ControlAction
 import android.util.TypedValue
 import android.util.Log
+import android.view.animation.AccelerateInterpolator
+import android.view.animation.DecelerateInterpolator
 import android.view.ContextThemeWrapper
 import android.view.LayoutInflater
 import android.view.View
@@ -77,6 +85,8 @@
         private const val PREF_COMPONENT = "controls_component"
         private const val PREF_STRUCTURE = "controls_structure"
 
+        private const val FADE_IN_MILLIS = 225L
+
         private val EMPTY_COMPONENT = ComponentName("", "")
         private val EMPTY_STRUCTURE = StructureInfo(
             EMPTY_COMPONENT,
@@ -153,7 +163,20 @@
 
     private fun reload(parent: ViewGroup) {
         if (hidden) return
-        show(parent)
+
+        val fadeAnim = ObjectAnimator.ofFloat(parent, "alpha", 1.0f, 0.0f)
+        fadeAnim.setInterpolator(AccelerateInterpolator(1.0f))
+        fadeAnim.setDuration(FADE_IN_MILLIS)
+        fadeAnim.addListener(object : AnimatorListenerAdapter() {
+            override fun onAnimationEnd(animation: Animator) {
+                show(parent)
+                val showAnim = ObjectAnimator.ofFloat(parent, "alpha", 0.0f, 1.0f)
+                showAnim.setInterpolator(DecelerateInterpolator(1.0f))
+                showAnim.setDuration(FADE_IN_MILLIS)
+                showAnim.start()
+            }
+        })
+        fadeAnim.start()
     }
 
     private fun showSeedingView(items: List<SelectionItem>) {
@@ -229,7 +252,8 @@
 
     private fun createMenu() {
         val items = arrayOf(
-            context.resources.getString(R.string.controls_menu_add)
+            context.resources.getString(R.string.controls_menu_add),
+            "Reset"
         )
         var adapter = ArrayAdapter<String>(context, R.layout.controls_more_item, items)
 
@@ -249,6 +273,8 @@
                             when (pos) {
                                 // 0: Add Control
                                 0 -> startFavoritingActivity(view.context, selectedStructure)
+                                // 1: TEMPORARY for reset controls
+                                1 -> showResetConfirmation()
                                 else -> Log.w(ControlsUiController.TAG,
                                     "Unsupported index ($pos) on 'more' menu selection")
                             }
@@ -275,6 +301,39 @@
         })
     }
 
+    private fun showResetConfirmation() {
+        val builder = AlertDialog.Builder(
+            context,
+            android.R.style.Theme_DeviceDefault_Dialog_Alert
+        ).apply {
+            setMessage("For testing purposes: Would you like to " +
+                "reset your favorited device controls?")
+            setPositiveButton(
+                android.R.string.ok,
+                DialogInterface.OnClickListener { dialog, _ ->
+                    val userHandle = Process.myUserHandle()
+                    val userContext = context.createContextAsUser(userHandle, 0)
+                    val prefs = userContext.getSharedPreferences(
+                        "controls_prefs", Context.MODE_PRIVATE)
+                    prefs.edit().putBoolean("ControlsSeedingCompleted", false).apply()
+                    controlsController.get().resetFavorites()
+                    dialog.dismiss()
+                    context.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
+            })
+            setNegativeButton(
+                android.R.string.cancel,
+                DialogInterface.OnClickListener {
+                    dialog, _ -> dialog.cancel()
+                }
+            )
+        }
+        builder.create().apply {
+            getWindow().apply {
+                setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY)
+            }
+        }.show()
+    }
+
     private fun createDropDown(items: List<SelectionItem>) {
         items.forEach {
             RenderInfo.registerComponentIcon(it.componentName, it.icon)
@@ -370,7 +429,7 @@
         }
 
         // add spacers if necessary to keep control size consistent
-        var spacersToAdd = selectedStructure.controls.size % maxColumns
+        var spacersToAdd = maxColumns - (selectedStructure.controls.size % maxColumns)
         while (spacersToAdd > 0) {
             lastRow.addView(Space(context), LinearLayout.LayoutParams(0, 0, 1f))
             spacersToAdd--
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
index 4dd5e87..79def1d 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
@@ -205,7 +205,9 @@
     private final IWindowManager mIWindowManager;
     private final Executor mBackgroundExecutor;
     private final ControlsListingController mControlsListingController;
-    private boolean mAnyControlsProviders = false;
+    private List<ControlsServiceInfo> mControlsServiceInfos = new ArrayList<>();
+    private ControlsController mControlsController;
+    private SharedPreferences mControlsPreferences;
 
     @VisibleForTesting
     public enum GlobalActionsEvent implements UiEventLogger.UiEventEnum {
@@ -271,6 +273,7 @@
         mBackgroundExecutor = backgroundExecutor;
         mControlsListingController = controlsListingController;
         mBlurUtils = blurUtils;
+        mControlsController = controlsController;
 
         // receive broadcasts
         IntentFilter filter = new IntentFilter();
@@ -309,45 +312,54 @@
             }
         });
 
-        String preferredControlsPackage = mContext.getResources()
-                .getString(com.android.systemui.R.string.config_controlsPreferredPackage);
         mControlsListingController.addCallback(list -> {
-            mAnyControlsProviders = !list.isEmpty();
-
-            /*
-             * See if any service providers match the preferred component. If they do,
-             * and there are no current favorites, and we haven't successfully loaded favorites to
-             * date, query the preferred component for a limited number of suggested controls.
-             */
-            ComponentName preferredComponent = null;
-            for (ControlsServiceInfo info : list) {
-                if (info.componentName.getPackageName().equals(preferredControlsPackage)) {
-                    preferredComponent = info.componentName;
-                    break;
-                }
-            }
-
-            if (preferredComponent == null) return;
-
-            SharedPreferences prefs = context.getSharedPreferences(PREFS_CONTROLS_FILE,
-                    Context.MODE_PRIVATE);
-            boolean isSeeded = prefs.getBoolean(PREFS_CONTROLS_SEEDING_COMPLETED, false);
-            boolean hasFavorites = controlsController.getFavorites().size() > 0;
-            if (!isSeeded && !hasFavorites) {
-                controlsController.seedFavoritesForComponent(
-                        preferredComponent,
-                        (accepted) -> {
-                            Log.i(TAG, "Controls seeded: " + accepted);
-                            prefs.edit().putBoolean(PREFS_CONTROLS_SEEDING_COMPLETED,
-                                    accepted).apply();
-                        }
-                );
-            }
+            mControlsServiceInfos = list;
         });
+
+        // Need to be user-specific with the context to make sure we read the correct prefs
+        Context userContext = context.createContextAsUser(
+                new UserHandle(mUserManager.getUserHandle()), 0);
+        mControlsPreferences = userContext.getSharedPreferences(PREFS_CONTROLS_FILE,
+            Context.MODE_PRIVATE);
+
     }
 
+    private void seedFavorites() {
+        if (mControlsServiceInfos.isEmpty()
+                || mControlsController.getFavorites().size() > 0
+                || mControlsPreferences.getBoolean(PREFS_CONTROLS_SEEDING_COMPLETED, false)) {
+            return;
+        }
 
+        /*
+         * See if any service providers match the preferred component. If they do,
+         * and there are no current favorites, and we haven't successfully loaded favorites to
+         * date, query the preferred component for a limited number of suggested controls.
+         */
+        String preferredControlsPackage = mContext.getResources()
+                .getString(com.android.systemui.R.string.config_controlsPreferredPackage);
 
+        ComponentName preferredComponent = null;
+        for (ControlsServiceInfo info : mControlsServiceInfos) {
+            if (info.componentName.getPackageName().equals(preferredControlsPackage)) {
+                preferredComponent = info.componentName;
+                break;
+            }
+        }
+
+        if (preferredComponent == null) {
+            Log.i(TAG, "Controls seeding: No preferred component has been set, will not seed");
+            mControlsPreferences.edit().putBoolean(PREFS_CONTROLS_SEEDING_COMPLETED, true).apply();
+        }
+
+        mControlsController.seedFavoritesForComponent(
+                preferredComponent,
+                (accepted) -> {
+                    Log.i(TAG, "Controls seeded: " + accepted);
+                    mControlsPreferences.edit().putBoolean(PREFS_CONTROLS_SEEDING_COMPLETED,
+                        accepted).apply();
+                });
+    }
 
     /**
      * Show the global actions dialog (creating if necessary)
@@ -393,6 +405,7 @@
         awakenIfNecessary();
         mDialog = createDialog();
         prepareDialog();
+        seedFavorites();
 
         // If we only have 1 item and it's a simple press action, just do this action.
         if (mAdapter.getCount() == 1
@@ -2017,6 +2030,6 @@
     private boolean shouldShowControls() {
         return mKeyguardStateController.isUnlocked()
                 && mControlsUiController.getAvailable()
-                && mAnyControlsProviders;
+                && !mControlsServiceInfos.isEmpty();
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt b/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt
index aa5ebaa..b7658a9 100644
--- a/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt
@@ -46,19 +46,6 @@
     /** Updates seek bar views when the data model changes. */
     @UiThread
     override fun onChanged(data: SeekBarViewModel.Progress) {
-        if (data.enabled && seekBarView.visibility == View.GONE) {
-            seekBarView.visibility = View.VISIBLE
-            elapsedTimeView.visibility = View.VISIBLE
-            totalTimeView.visibility = View.VISIBLE
-        } else if (!data.enabled && seekBarView.visibility == View.VISIBLE) {
-            seekBarView.visibility = View.GONE
-            elapsedTimeView.visibility = View.GONE
-            totalTimeView.visibility = View.GONE
-            return
-        }
-
-        // TODO: update the style of the disabled progress bar
-        seekBarView.setEnabled(data.seekAvailable)
 
         data.color?.let {
             var tintList = ColorStateList.valueOf(it)
@@ -71,6 +58,17 @@
             totalTimeView.setTextColor(it)
         }
 
+        if (!data.enabled) {
+            seekBarView.setEnabled(false)
+            seekBarView.getThumb().setAlpha(0)
+            elapsedTimeView.setText("")
+            totalTimeView.setText("")
+            return
+        }
+
+        seekBarView.getThumb().setAlpha(if (data.seekAvailable) 255 else 0)
+        seekBarView.setEnabled(data.seekAvailable)
+
         data.elapsedTime?.let {
             seekBarView.setProgress(it)
             elapsedTimeView.setText(DateUtils.formatElapsedTime(
diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java b/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java
index b10dd93..d2994ae 100644
--- a/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java
+++ b/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java
@@ -103,6 +103,7 @@
         @Override
         public void onPipAnimationEnd(SurfaceControl.Transaction tx,
                 PipAnimationController.PipTransitionAnimator animator) {
+            finishResize(tx, animator.getDestinationBounds(), animator.getTransitionDirection());
             mMainHandler.post(() -> {
                 for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) {
                     final PipTransitionCallback callback = mPipTransitionCallbacks.get(i);
@@ -110,7 +111,6 @@
                             animator.getTransitionDirection());
                 }
             });
-            finishResize(tx, animator.getDestinationBounds(), animator.getTransitionDirection());
         }
 
         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMotionHelper.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMotionHelper.java
index a192afc..a8a5d89 100644
--- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMotionHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMotionHelper.java
@@ -16,12 +16,10 @@
 
 package com.android.systemui.pip.phone;
 
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.app.ActivityManager.StackInfo;
 import android.app.IActivityTaskManager;
 import android.content.Context;
 import android.graphics.Rect;
@@ -169,15 +167,7 @@
      */
     void synchronizePinnedStackBounds() {
         cancelAnimations();
-        try {
-            StackInfo stackInfo = mActivityTaskManager.getStackInfo(
-                    WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED);
-            if (stackInfo != null) {
-                mBounds.set(stackInfo.bounds);
-            }
-        } catch (RemoteException e) {
-            Log.w(TAG, "Failed to get pinned stack bounds");
-        }
+        mBounds.set(mPipTaskOrganizer.getLastReportedBounds());
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/pip/tv/PipManager.java b/packages/SystemUI/src/com/android/systemui/pip/tv/PipManager.java
index 52c8960..99a01d3 100644
--- a/packages/SystemUI/src/com/android/systemui/pip/tv/PipManager.java
+++ b/packages/SystemUI/src/com/android/systemui/pip/tv/PipManager.java
@@ -289,7 +289,6 @@
 
         mMediaSessionManager =
                 (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE);
-        mPipTaskOrganizer.registerPipTransitionCallback(this);
 
         try {
             WindowManagerWrapper.getInstance().addPinnedStackListener(mPinnedStackListener);
@@ -334,6 +333,8 @@
      * Shows the picture-in-picture menu if an activity is in picture-in-picture mode.
      */
     public void showPictureInPictureMenu() {
+        if (DEBUG) Log.d(TAG, "showPictureInPictureMenu(), current state=" + getStateDescription());
+
         if (getState() == STATE_PIP) {
             resizePinnedStack(STATE_PIP_MENU);
         }
@@ -343,10 +344,18 @@
      * Closes PIP (PIPed activity and PIP system UI).
      */
     public void closePip() {
+        if (DEBUG) Log.d(TAG, "closePip(), current state=" + getStateDescription());
+
         closePipInternal(true);
     }
 
     private void closePipInternal(boolean removePipStack) {
+        if (DEBUG) {
+            Log.d(TAG,
+                    "closePipInternal() removePipStack=" + removePipStack + ", current state="
+                            + getStateDescription());
+        }
+
         mState = STATE_NO_PIP;
         mPipTaskId = TASK_ID_NO_PIP;
         mPipMediaController = null;
@@ -371,6 +380,8 @@
      * Moves the PIPed activity to the fullscreen and closes PIP system UI.
      */
     void movePipToFullscreen() {
+        if (DEBUG) Log.d(TAG, "movePipToFullscreen(), current state=" + getStateDescription());
+
         mPipTaskId = TASK_ID_NO_PIP;
         for (int i = mListeners.size() - 1; i >= 0; --i) {
             mListeners.get(i).onMoveToFullscreen();
@@ -386,6 +397,7 @@
     public void suspendPipResizing(int reason) {
         if (DEBUG) Log.d(TAG,
                 "suspendPipResizing() reason=" + reason + " callers=" + Debug.getCallers(2));
+
         mSuspendPipResizingReason |= reason;
     }
 
@@ -408,7 +420,11 @@
      * @param state In Pip state also used to determine the new size for the Pip.
      */
     void resizePinnedStack(int state) {
-        if (DEBUG) Log.d(TAG, "resizePinnedStack() state=" + state, new Exception());
+        if (DEBUG) {
+            Log.d(TAG, "resizePinnedStack() state=" + stateToName(state) + ", current state="
+                    + getStateDescription(), new Exception());
+        }
+
         boolean wasStateNoPip = (mState == STATE_NO_PIP);
         for (int i = mListeners.size() - 1; i >= 0; --i) {
             mListeners.get(i).onPipResizeAboutToStart();
@@ -418,7 +434,7 @@
             if (DEBUG) Log.d(TAG, "resizePinnedStack() deferring"
                     + " mSuspendPipResizingReason=" + mSuspendPipResizingReason
                     + " mResumeResizePinnedStackRunnableState="
-                    + mResumeResizePinnedStackRunnableState);
+                    + stateToName(mResumeResizePinnedStackRunnableState));
             return;
         }
         mState = state;
@@ -458,7 +474,8 @@
      * stack to the centered PIP bound {@link R.config_centeredPictureInPictureBounds}.
      */
     private void showPipMenu() {
-        if (DEBUG) Log.d(TAG, "showPipMenu()");
+        if (DEBUG) Log.d(TAG, "showPipMenu(), current state=" + getStateDescription());
+
         mState = STATE_PIP_MENU;
         for (int i = mListeners.size() - 1; i >= 0; --i) {
             mListeners.get(i).onShowPipMenu();
@@ -712,6 +729,7 @@
 
     private void onPipTransitionFinishedOrCanceled() {
         if (DEBUG) Log.d(TAG, "onPipTransitionFinishedOrCanceled()");
+
         if (getState() == STATE_PIP_MENU) {
             showPipMenu();
         }
@@ -753,4 +771,28 @@
             WindowManagerWrapper.getInstance().setPipVisibility(visible);
         });
     }
+
+    private String getStateDescription() {
+        if (mSuspendPipResizingReason == 0) {
+            return stateToName(mState);
+        }
+        return stateToName(mResumeResizePinnedStackRunnableState) + " (while " + stateToName(mState)
+                + " is suspended)";
+    }
+
+    private static String stateToName(int state) {
+        switch (state) {
+            case STATE_NO_PIP:
+                return "NO_PIP";
+
+            case STATE_PIP:
+                return "PIP";
+
+            case STATE_PIP_MENU:
+                return "PIP_MENU";
+
+            default:
+                return "UNKNOWN(" + state + ")";
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/pip/tv/PipMenuActivity.java b/packages/SystemUI/src/com/android/systemui/pip/tv/PipMenuActivity.java
index f43f8e7..c7e77cc 100644
--- a/packages/SystemUI/src/com/android/systemui/pip/tv/PipMenuActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/pip/tv/PipMenuActivity.java
@@ -22,6 +22,7 @@
 import android.content.Intent;
 import android.content.pm.ParceledListSlice;
 import android.os.Bundle;
+import android.util.Log;
 
 import com.android.systemui.R;
 import com.android.systemui.pip.tv.dagger.TvPipComponent;
@@ -34,6 +35,7 @@
  * Activity to show the PIP menu to control PIP.
  */
 public class PipMenuActivity extends Activity implements PipManager.Listener {
+    private static final boolean DEBUG = false;
     private static final String TAG = "PipMenuActivity";
 
     static final String EXTRA_CUSTOM_ACTIONS = "custom_actions";
@@ -47,7 +49,6 @@
     private boolean mRestorePipSizeWhenClose;
     private PipControlsViewController mPipControlsViewController;
 
-
     @Inject
     public PipMenuActivity(TvPipComponent.Builder pipComponentBuilder, PipManager pipManager) {
         super();
@@ -57,6 +58,8 @@
 
     @Override
     protected void onCreate(Bundle bundle) {
+        if (DEBUG) Log.d(TAG, "onCreate()");
+
         super.onCreate(bundle);
         if (!mPipManager.isPipShown()) {
             finish();
@@ -81,13 +84,18 @@
 
     @Override
     protected void onNewIntent(Intent intent) {
+        if (DEBUG) Log.d(TAG, "onNewIntent(), intent=" + intent);
         super.onNewIntent(intent);
 
         onPipMenuActionsChanged(getIntent().getParcelableExtra(EXTRA_CUSTOM_ACTIONS));
     }
 
     private void restorePipAndFinish() {
+        if (DEBUG) Log.d(TAG, "restorePipAndFinish()");
+
         if (mRestorePipSizeWhenClose) {
+            if (DEBUG) Log.d(TAG, "   > restoring to the default position");
+
             // When PIP menu activity is closed, restore to the default position.
             mPipManager.resizePinnedStack(PipManager.STATE_PIP);
         }
@@ -96,12 +104,16 @@
 
     @Override
     public void onResume() {
+        if (DEBUG) Log.d(TAG, "onResume()");
+
         super.onResume();
         mFadeInAnimation.start();
     }
 
     @Override
     public void onPause() {
+        if (DEBUG) Log.d(TAG, "onPause()");
+
         super.onPause();
         mFadeOutAnimation.start();
         restorePipAndFinish();
@@ -109,6 +121,8 @@
 
     @Override
     protected void onDestroy() {
+        if (DEBUG) Log.d(TAG, "onDestroy()");
+
         super.onDestroy();
         mPipManager.removeListener(this);
         mPipManager.resumePipResizing(
@@ -117,29 +131,41 @@
 
     @Override
     public void onBackPressed() {
+        if (DEBUG) Log.d(TAG, "onBackPressed()");
+
         restorePipAndFinish();
     }
 
     @Override
-    public void onPipEntered() { }
+    public void onPipEntered() {
+        if (DEBUG) Log.d(TAG, "onPipEntered()");
+    }
 
     @Override
     public void onPipActivityClosed() {
+        if (DEBUG) Log.d(TAG, "onPipActivityClosed()");
+
         finish();
     }
 
     @Override
     public void onPipMenuActionsChanged(ParceledListSlice actions) {
+        if (DEBUG) Log.d(TAG, "onPipMenuActionsChanged()");
+
         boolean hasCustomActions = actions != null && !actions.getList().isEmpty();
         mPipControlsViewController.setActions(
                 hasCustomActions ? actions.getList() : Collections.EMPTY_LIST);
     }
 
     @Override
-    public void onShowPipMenu() { }
+    public void onShowPipMenu() {
+        if (DEBUG) Log.d(TAG, "onShowPipMenu()");
+    }
 
     @Override
     public void onMoveToFullscreen() {
+        if (DEBUG) Log.d(TAG, "onMoveToFullscreen()");
+
         // Moving PIP to fullscreen is implemented by resizing PINNED_STACK with null bounds.
         // This conflicts with restoring PIP position, so disable it.
         mRestorePipSizeWhenClose = false;
@@ -148,8 +174,17 @@
 
     @Override
     public void onPipResizeAboutToStart() {
+        if (DEBUG) Log.d(TAG, "onPipResizeAboutToStart()");
+
         finish();
         mPipManager.suspendPipResizing(
                 PipManager.SUSPEND_PIP_RESIZE_REASON_WAITING_FOR_MENU_ACTIVITY_FINISH);
     }
+
+    @Override
+    public void finish() {
+        if (DEBUG) Log.d(TAG, "finish()", new RuntimeException());
+
+        super.finish();
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
index 295adae..9324b14 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
@@ -24,6 +24,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.Notification;
+import android.os.SystemClock;
 import android.service.notification.NotificationListenerService;
 import android.service.notification.NotificationListenerService.Ranking;
 import android.service.notification.NotificationListenerService.RankingMap;
@@ -555,8 +556,8 @@
         NotificationEntry entry = new NotificationEntry(
                 notification,
                 ranking,
-                mFgsFeatureController.isForegroundServiceDismissalEnabled());
-        mAllNotifications.add(entry);
+                mFgsFeatureController.isForegroundServiceDismissalEnabled(),
+                SystemClock.uptimeMillis());
 
         mLeakDetector.trackInstance(entry);
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
index 9c2cac7..365862b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
@@ -47,6 +47,7 @@
 import android.annotation.UserIdInt;
 import android.app.Notification;
 import android.os.RemoteException;
+import android.os.SystemClock;
 import android.os.UserHandle;
 import android.service.notification.NotificationListenerService;
 import android.service.notification.NotificationListenerService.Ranking;
@@ -387,7 +388,7 @@
 
         if (entry == null) {
             // A new notification!
-            entry = new NotificationEntry(sbn, ranking);
+            entry = new NotificationEntry(sbn, ranking, SystemClock.uptimeMillis());
             mNotificationSet.put(sbn.getKey(), entry);
 
             mLogger.logNotifPosted(sbn.getKey());
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index c1ba26d..68ec34e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -35,6 +35,7 @@
 
 import static java.util.Objects.requireNonNull;
 
+import android.annotation.CurrentTimeMillisLong;
 import android.app.Notification;
 import android.app.Notification.MessagingStyle.Message;
 import android.app.NotificationChannel;
@@ -93,6 +94,7 @@
     private final String mKey;
     private StatusBarNotification mSbn;
     private Ranking mRanking;
+    private long mCreationTime;
 
     /*
      * Bookkeeping members
@@ -171,21 +173,29 @@
     private boolean mAllowFgsDismissal;
     private int mBucket = BUCKET_ALERTING;
 
+    /**
+     * @param sbn the StatusBarNotification from system server
+     * @param ranking also from system server
+     * @param creationTime SystemClock.uptimeMillis of when we were created
+     */
     public NotificationEntry(
             @NonNull StatusBarNotification sbn,
-            @NonNull Ranking ranking) {
-        this(sbn, ranking, false);
+            @NonNull Ranking ranking,
+            long creationTime) {
+        this(sbn, ranking, false, creationTime);
     }
 
     public NotificationEntry(
             @NonNull StatusBarNotification sbn,
             @NonNull Ranking ranking,
-            boolean allowFgsDismissal
+            boolean allowFgsDismissal,
+            long creationTime
     ) {
         super(requireNonNull(Objects.requireNonNull(sbn).getKey()));
 
         requireNonNull(ranking);
 
+        mCreationTime = creationTime;
         mKey = sbn.getKey();
         setSbn(sbn);
         setRanking(ranking);
@@ -238,6 +248,21 @@
     }
 
     /**
+     * A timestamp of SystemClock.uptimeMillis() of when this entry was first created, regardless
+     * of any changes to the data presented. It is set once on creation and will never change, and
+     * allows us to know exactly how long this notification has been alive for in our listener
+     * service. It is entirely unrelated to the information inside of the notification.
+     *
+     * This is different to Notification#when because it persists throughout updates, whereas
+     * system server treats every single call to notify() as a new notification and we handle
+     * updates to NotificationEntry locally.
+     */
+    @CurrentTimeMillisLong
+    public long getCreationTime() {
+        return mCreationTime;
+    }
+
+    /**
      * Should only be called by NotificationEntryManager and friends.
      * TODO: Make this package-private
      */
diff --git a/packages/SystemUI/src/com/android/systemui/util/RelativeTouchListener.kt b/packages/SystemUI/src/com/android/systemui/util/RelativeTouchListener.kt
new file mode 100644
index 0000000..d65b285
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/RelativeTouchListener.kt
@@ -0,0 +1,162 @@
+/*
+ * 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.util
+
+import android.graphics.PointF
+import android.os.Handler
+import android.view.MotionEvent
+import android.view.VelocityTracker
+import android.view.View
+import android.view.ViewConfiguration
+import kotlin.math.hypot
+
+/**
+ * Listener which receives [onDown], [onMove], and [onUp] events, with relevant information about
+ * the coordinates of the touch and the view relative to the initial ACTION_DOWN event and the
+ * view's initial position.
+ */
+abstract class RelativeTouchListener : View.OnTouchListener {
+
+    /**
+     * Called when an ACTION_DOWN event is received for the given view.
+     *
+     * @return False if the object is not interested in MotionEvents at this time, or true if we
+     * should consume this event and subsequent events, and begin calling [onMove].
+     */
+    abstract fun onDown(v: View, ev: MotionEvent): Boolean
+
+    /**
+     * Called when an ACTION_MOVE event is received for the given view. This signals that the view
+     * is being dragged.
+     *
+     * @param viewInitialX The view's translationX value when this touch gesture started.
+     * @param viewInitialY The view's translationY value when this touch gesture started.
+     * @param dx Horizontal distance covered since the initial ACTION_DOWN event, in pixels.
+     * @param dy Vertical distance covered since the initial ACTION_DOWN event, in pixels.
+     */
+    abstract fun onMove(
+        v: View,
+        ev: MotionEvent,
+        viewInitialX: Float,
+        viewInitialY: Float,
+        dx: Float,
+        dy: Float
+    )
+
+    /**
+     * Called when an ACTION_UP event is received for the given view. This signals that a drag or
+     * fling gesture has completed.
+     *
+     * @param viewInitialX The view's translationX value when this touch gesture started.
+     * @param viewInitialY The view's translationY value when this touch gesture started.
+     * @param dx Horizontal distance covered, in pixels.
+     * @param dy Vertical distance covered, in pixels.
+     * @param velX The final horizontal velocity of the gesture, in pixels/second.
+     * @param velY The final vertical velocity of the gesture, in pixels/second.
+     */
+    abstract fun onUp(
+        v: View,
+        ev: MotionEvent,
+        viewInitialX: Float,
+        viewInitialY: Float,
+        dx: Float,
+        dy: Float,
+        velX: Float,
+        velY: Float
+    )
+
+    /** The raw coordinates of the last ACTION_DOWN event. */
+    private val touchDown = PointF()
+
+    /** The coordinates of the view, at the time of the last ACTION_DOWN event. */
+    private val viewPositionOnTouchDown = PointF()
+
+    private val velocityTracker = VelocityTracker.obtain()
+
+    private var touchSlop: Int = -1
+    private var movedEnough = false
+
+    private val handler = Handler()
+    private var performedLongClick = false
+
+    @Suppress("UNCHECKED_CAST")
+    override fun onTouch(v: View, ev: MotionEvent): Boolean {
+        addMovement(ev)
+
+        val dx = ev.rawX - touchDown.x
+        val dy = ev.rawY - touchDown.y
+
+        when (ev.action) {
+            MotionEvent.ACTION_DOWN -> {
+                if (!onDown(v, ev)) {
+                    return false
+                }
+
+                // Grab the touch slop, it might have changed if the config changed since the
+                // last gesture.
+                touchSlop = ViewConfiguration.get(v.context).scaledTouchSlop
+
+                touchDown.set(ev.rawX, ev.rawY)
+                viewPositionOnTouchDown.set(v.translationX, v.translationY)
+
+                performedLongClick = false
+                handler.postDelayed({
+                    performedLongClick = v.performLongClick()
+                }, ViewConfiguration.getLongPressTimeout().toLong())
+            }
+
+            MotionEvent.ACTION_MOVE -> {
+                if (!movedEnough && hypot(dx, dy) > touchSlop && !performedLongClick) {
+                    movedEnough = true
+                    handler.removeCallbacksAndMessages(null)
+                }
+
+                if (movedEnough) {
+                    onMove(v, ev, viewPositionOnTouchDown.x, viewPositionOnTouchDown.y, dx, dy)
+                }
+            }
+
+            MotionEvent.ACTION_UP -> {
+                if (movedEnough) {
+                    velocityTracker.computeCurrentVelocity(1000 /* units */)
+                    onUp(v, ev, viewPositionOnTouchDown.x, viewPositionOnTouchDown.y, dx, dy,
+                            velocityTracker.xVelocity, velocityTracker.yVelocity)
+                } else if (!performedLongClick) {
+                    v.performClick()
+                } else {
+                    handler.removeCallbacksAndMessages(null)
+                }
+
+                velocityTracker.clear()
+                movedEnough = false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Adds a movement to the velocity tracker using raw screen coordinates.
+     */
+    private fun addMovement(event: MotionEvent) {
+        val deltaX = event.rawX - event.x
+        val deltaY = event.rawY - event.y
+        event.offsetLocation(deltaX, deltaY)
+        velocityTracker.addMovement(event)
+        event.offsetLocation(-deltaX, -deltaY)
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/AdminSecondaryLockScreenControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/AdminSecondaryLockScreenControllerTest.java
index cf1299f..ce032c9 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/AdminSecondaryLockScreenControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/AdminSecondaryLockScreenControllerTest.java
@@ -115,6 +115,7 @@
 
     @Test
     public void testShow_dismissedByCallback() throws Exception {
+        doAnswer(answerVoid(Runnable::run)).when(mHandler).post(any(Runnable.class));
         doAnswer(invocation -> {
             IKeyguardCallback callback = (IKeyguardCallback) invocation.getArguments()[1];
             callback.onDismiss();
@@ -184,7 +185,7 @@
 
     private void verifyViewDismissed(SurfaceView v) throws Exception {
         verify(mParent).removeView(v);
-        verify(mKeyguardCallback).dismiss(true, TARGET_USER_ID);
+        verify(mKeyguardCallback).dismiss(true, TARGET_USER_ID, true);
         assertThat(mContext.isBound(mComponentName)).isFalse();
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java
index 69e933e..45f9437 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java
@@ -52,6 +52,7 @@
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
+import com.android.systemui.util.time.FakeSystemClock;
 
 import junit.framework.Assert;
 
@@ -69,6 +70,7 @@
     private ForegroundServiceController mFsc;
     private ForegroundServiceNotificationListener mListener;
     private NotificationEntryListener mEntryListener;
+    private final FakeSystemClock mClock = new FakeSystemClock();
     @Mock private NotificationEntryManager mEntryManager;
     @Mock private AppOpsController mAppOpsController;
     @Mock private Handler mMainHandler;
@@ -80,9 +82,10 @@
         allowTestableLooperAsMainThread();
 
         MockitoAnnotations.initMocks(this);
-        mFsc = new ForegroundServiceController(mEntryManager, mAppOpsController, mMainHandler);
+        mFsc = new ForegroundServiceController(
+                mEntryManager, mAppOpsController, mMainHandler);
         mListener = new ForegroundServiceNotificationListener(
-                mContext, mFsc, mEntryManager, mNotifPipeline);
+                mContext, mFsc, mEntryManager, mNotifPipeline, mClock);
         ArgumentCaptor<NotificationEntryListener> entryListenerCaptor =
                 ArgumentCaptor.forClass(NotificationEntryListener.class);
         verify(mEntryManager).addNotificationEntryListener(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceNotificationListenerTest.java b/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceNotificationListenerTest.java
index 46a473b..bca8dee 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceNotificationListenerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceNotificationListenerTest.java
@@ -17,7 +17,6 @@
 package com.android.systemui;
 
 import static com.android.systemui.ForegroundServiceLifetimeExtender.MIN_FGS_TIME_MS;
-import static com.android.systemui.statusbar.NotificationEntryHelper.modifySbn;
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -29,6 +28,7 @@
 
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
+import com.android.systemui.util.time.FakeSystemClock;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -37,12 +37,15 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class ForegroundServiceNotificationListenerTest extends SysuiTestCase {
-    private ForegroundServiceLifetimeExtender mExtender = new ForegroundServiceLifetimeExtender();
+    private ForegroundServiceLifetimeExtender mExtender;
     private NotificationEntry mEntry;
     private Notification mNotif;
+    private final FakeSystemClock mClock = new FakeSystemClock();
 
     @Before
     public void setup() {
+        mExtender = new ForegroundServiceLifetimeExtender(mClock);
+
         mNotif = new Notification.Builder(mContext, "")
                 .setSmallIcon(R.drawable.ic_person)
                 .setContentTitle("Title")
@@ -50,6 +53,7 @@
                 .build();
 
         mEntry = new NotificationEntryBuilder()
+                .setCreationTime(mClock.uptimeMillis())
                 .setNotification(mNotif)
                 .build();
     }
@@ -62,27 +66,26 @@
         // Extend the lifetime of a FGS notification iff it has not been visible
         // for the minimum time
         mNotif.flags |= Notification.FLAG_FOREGROUND_SERVICE;
-        modifySbn(mEntry)
-                .setPostTime(System.currentTimeMillis())
-                .build();
+
+        // No time has elapsed, keep showing
         assertTrue(mExtender.shouldExtendLifetime(mEntry));
     }
 
     @Test
     public void testShouldExtendLifetime_shouldNot_foreground() {
         mNotif.flags |= Notification.FLAG_FOREGROUND_SERVICE;
-        modifySbn(mEntry)
-                .setPostTime(System.currentTimeMillis() - MIN_FGS_TIME_MS - 1)
-                .build();
+
+        // Entry was created at mClock.uptimeMillis(), advance it MIN_FGS_TIME_MS + 1
+        mClock.advanceTime(MIN_FGS_TIME_MS + 1);
         assertFalse(mExtender.shouldExtendLifetime(mEntry));
     }
 
     @Test
     public void testShouldExtendLifetime_shouldNot_notForeground() {
         mNotif.flags = 0;
-        modifySbn(mEntry)
-                .setPostTime(System.currentTimeMillis() - MIN_FGS_TIME_MS - 1)
-                .build();
+
+        // Entry was created at mClock.uptimeMillis(), advance it MIN_FGS_TIME_MS + 1
+        mClock.advanceTime(MIN_FGS_TIME_MS + 1);
         assertFalse(mExtender.shouldExtendLifetime(mEntry));
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
index 037f04e..e472de3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
@@ -374,7 +374,7 @@
         assertNotNull(mBubbleData.getBubbleWithKey(mRow2.getEntry().getKey()));
         assertTrue(mBubbleController.hasBubbles());
 
-        mBubbleController.dismissStack(BubbleController.DISMISS_USER_GESTURE);
+        mBubbleData.dismissAll(BubbleController.DISMISS_USER_GESTURE);
         assertFalse(mNotificationShadeWindowController.getBubblesShowing());
         verify(mNotificationEntryManager, times(3)).updateNotifications(any());
         assertNull(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()));
@@ -399,7 +399,7 @@
 
         // Expand the stack
         BubbleStackView stackView = mBubbleController.getStackView();
-        mBubbleController.expandStack();
+        mBubbleData.setExpanded(true);
         assertTrue(mBubbleController.isStackExpanded());
         verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().getKey());
         assertTrue(mNotificationShadeWindowController.getBubbleExpanded());
@@ -436,7 +436,7 @@
 
         // Expand
         BubbleStackView stackView = mBubbleController.getStackView();
-        mBubbleController.expandStack();
+        mBubbleData.setExpanded(true);
         assertTrue(mBubbleController.isStackExpanded());
         verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow2.getEntry().getKey());
 
@@ -448,7 +448,7 @@
                 mRow2.getEntry()));
 
         // Switch which bubble is expanded
-        mBubbleController.selectBubble(mRow.getEntry().getKey());
+        mBubbleData.setSelectedBubble(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()));
         mBubbleData.setExpanded(true);
         assertEquals(mRow.getEntry(),
                 mBubbleData.getBubbleWithKey(stackView.getExpandedBubble().getKey()).getEntry());
@@ -482,7 +482,7 @@
         assertTrue(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showDot());
 
         // Expand
-        mBubbleController.expandStack();
+        mBubbleData.setExpanded(true);
         assertTrue(mBubbleController.isStackExpanded());
         verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().getKey());
 
@@ -510,7 +510,7 @@
         assertTrue(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showDot());
 
         // Expand
-        mBubbleController.expandStack();
+        mBubbleData.setExpanded(true);
         assertTrue(mBubbleController.isStackExpanded());
         verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().getKey());
 
@@ -544,7 +544,7 @@
 
         // Expand
         BubbleStackView stackView = mBubbleController.getStackView();
-        mBubbleController.expandStack();
+        mBubbleData.setExpanded(true);
 
         assertTrue(mSysUiStateBubblesExpanded);
 
@@ -726,7 +726,7 @@
     public void testDeleteIntent_dismissStack() throws PendingIntent.CanceledException {
         mBubbleController.updateBubble(mRow.getEntry());
         mBubbleController.updateBubble(mRow2.getEntry());
-        mBubbleController.dismissStack(BubbleController.DISMISS_USER_GESTURE);
+        mBubbleData.dismissAll(BubbleController.DISMISS_USER_GESTURE);
         verify(mDeleteIntent, times(2)).send();
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java
index 545de21..5f4f2ef 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java
@@ -321,7 +321,7 @@
         assertNotNull(mBubbleData.getBubbleWithKey(mRow2.getEntry().getKey()));
         assertTrue(mBubbleController.hasBubbles());
 
-        mBubbleController.dismissStack(BubbleController.DISMISS_USER_GESTURE);
+        mBubbleData.dismissAll(BubbleController.DISMISS_USER_GESTURE);
         assertFalse(mNotificationShadeWindowController.getBubblesShowing());
         verify(mNotifCallback, times(3)).invalidateNotifications(anyString());
         assertNull(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()));
@@ -344,7 +344,7 @@
 
         // Expand the stack
         BubbleStackView stackView = mBubbleController.getStackView();
-        mBubbleController.expandStack();
+        mBubbleData.setExpanded(true);
         assertTrue(mBubbleController.isStackExpanded());
         verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().getKey());
         assertTrue(mNotificationShadeWindowController.getBubbleExpanded());
@@ -376,7 +376,7 @@
 
         // Expand
         BubbleStackView stackView = mBubbleController.getStackView();
-        mBubbleController.expandStack();
+        mBubbleData.setExpanded(true);
         assertTrue(mBubbleController.isStackExpanded());
         verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow2.getEntry().getKey());
 
@@ -385,7 +385,7 @@
         assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(mRow2.getEntry()));
 
         // Switch which bubble is expanded
-        mBubbleController.selectBubble(mRow.getEntry().getKey());
+        mBubbleData.setSelectedBubble(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()));
         mBubbleData.setExpanded(true);
         assertEquals(mRow.getEntry(),
                 mBubbleData.getBubbleWithKey(stackView.getExpandedBubble().getKey()).getEntry());
@@ -416,7 +416,7 @@
         assertTrue(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showDot());
 
         // Expand
-        mBubbleController.expandStack();
+        mBubbleData.setExpanded(true);
         assertTrue(mBubbleController.isStackExpanded());
         verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().getKey());
 
@@ -442,7 +442,7 @@
         assertTrue(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showDot());
 
         // Expand
-        mBubbleController.expandStack();
+        mBubbleData.setExpanded(true);
         assertTrue(mBubbleController.isStackExpanded());
         verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().getKey());
 
@@ -474,7 +474,7 @@
 
         // Expand
         BubbleStackView stackView = mBubbleController.getStackView();
-        mBubbleController.expandStack();
+        mBubbleData.setExpanded(true);
 
         assertTrue(mBubbleController.isStackExpanded());
         verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow2.getEntry().getKey());
@@ -628,7 +628,7 @@
     public void testDeleteIntent_dismissStack() throws PendingIntent.CanceledException {
         mBubbleController.updateBubble(mRow.getEntry());
         mBubbleController.updateBubble(mRow2.getEntry());
-        mBubbleController.dismissStack(BubbleController.DISMISS_USER_GESTURE);
+        mBubbleData.dismissAll(BubbleController.DISMISS_USER_GESTURE);
         verify(mDeleteIntent, times(2)).send();
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt
index 88316f2..bb003ee 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt
@@ -125,7 +125,7 @@
         loadSubscriberCaptor.value.onSubscribe(Binder(), subscription)
 
         canceller.run()
-        verify(subscription).cancel()
+        verify(providers[0]).cancelSubscription(subscription)
     }
 
     @Test
@@ -145,7 +145,7 @@
 
         loadSubscriberCaptor.value.onComplete(b)
         canceller.run()
-        verify(subscription, never()).cancel()
+        verify(providers[0], never()).cancelSubscription(subscription)
     }
 
     @Test
@@ -203,7 +203,7 @@
 
         loadSubscriberCaptor.value.onError(b, "")
         canceller.run()
-        verify(subscription, never()).cancel()
+        verify(providers[0], never()).cancelSubscription(subscription)
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt
index 260f520..d407b8a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt
@@ -67,10 +67,11 @@
         val isEnabled = false
         val data = SeekBarViewModel.Progress(isEnabled, false, null, null, null)
         observer.onChanged(data)
-        // THEN seek bar visibility is set to GONE
-        assertThat(seekBarView.getVisibility()).isEqualTo(View.GONE)
-        assertThat(elapsedTimeView.getVisibility()).isEqualTo(View.GONE)
-        assertThat(totalTimeView.getVisibility()).isEqualTo(View.GONE)
+        // THEN seek bar shows just a line with no text
+        assertThat(seekBarView.isEnabled()).isFalse()
+        assertThat(seekBarView.getThumb().getAlpha()).isEqualTo(0)
+        assertThat(elapsedTimeView.getText()).isEqualTo("")
+        assertThat(totalTimeView.getText()).isEqualTo("")
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryBuilder.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryBuilder.java
index 43cf83f..81f9546 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryBuilder.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryBuilder.java
@@ -29,6 +29,7 @@
 import com.android.internal.logging.InstanceId;
 import com.android.systemui.statusbar.RankingBuilder;
 import com.android.systemui.statusbar.SbnBuilder;
+import com.android.systemui.util.time.FakeSystemClock;
 
 import java.util.ArrayList;
 
@@ -44,16 +45,22 @@
 public class NotificationEntryBuilder {
     private final SbnBuilder mSbnBuilder = new SbnBuilder();
     private final RankingBuilder mRankingBuilder = new RankingBuilder();
+    private final FakeSystemClock mClock = new FakeSystemClock();
     private StatusBarNotification mSbn = null;
 
     /* ListEntry properties */
     private GroupEntry mParent;
     private int mSection = -1;
 
+    /* If set, use this creation time instead of mClock.uptimeMillis */
+    private long mCreationTime = -1;
+
     public NotificationEntry build() {
         StatusBarNotification sbn = mSbn != null ? mSbn : mSbnBuilder.build();
         mRankingBuilder.setKey(sbn.getKey());
-        final NotificationEntry entry = new NotificationEntry(sbn, mRankingBuilder.build());
+        long creationTime = mCreationTime != -1 ? mCreationTime : mClock.uptimeMillis();
+        final NotificationEntry entry = new NotificationEntry(
+                sbn, mRankingBuilder.build(), mClock.uptimeMillis());
 
         /* ListEntry properties */
         entry.setParent(mParent);
@@ -86,6 +93,14 @@
         return this;
     }
 
+    /**
+     * Set the creation time
+     */
+    public NotificationEntryBuilder setCreationTime(long creationTime) {
+        mCreationTime = creationTime;
+        return this;
+    }
+
     /* Delegated to SbnBuilder */
 
     public NotificationEntryBuilder setPkg(String pkg) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java
index 5b0b668..1a022ec 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java
@@ -52,6 +52,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.statusbar.RankingBuilder;
 import com.android.systemui.statusbar.SbnBuilder;
+import com.android.systemui.util.time.FakeSystemClock;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -71,6 +72,7 @@
     private int mId;
 
     private NotificationEntry mEntry;
+    private final FakeSystemClock mClock = new FakeSystemClock();
 
     @Before
     public void setup() {
@@ -187,7 +189,7 @@
                 .build();
 
         NotificationEntry entry =
-                new NotificationEntry(sbn, ranking);
+                new NotificationEntry(sbn, ranking, mClock.uptimeMillis());
 
         assertEquals(systemGeneratedSmartActions, entry.getSmartActions());
         assertEquals(NOTIFICATION_CHANNEL, entry.getChannel());
diff --git a/packages/Tethering/tests/unit/src/com/android/server/connectivity/tethering/EntitlementManagerTest.java b/packages/Tethering/tests/unit/src/com/android/server/connectivity/tethering/EntitlementManagerTest.java
index b3a30ab..6695eed 100644
--- a/packages/Tethering/tests/unit/src/com/android/server/connectivity/tethering/EntitlementManagerTest.java
+++ b/packages/Tethering/tests/unit/src/com/android/server/connectivity/tethering/EntitlementManagerTest.java
@@ -33,7 +33,6 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
 import static org.mockito.Matchers.anyBoolean;
 import static org.mockito.Matchers.anyInt;
 import static org.mockito.Matchers.anyString;
@@ -72,8 +71,6 @@
 import org.mockito.quality.Strictness;
 
 import java.util.ArrayList;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
@@ -255,19 +252,16 @@
 
     @Test
     public void testRequestLastEntitlementCacheValue() throws Exception {
-        final CountDownLatch mCallbacklatch = new CountDownLatch(1);
         // 1. Entitlement check is not required.
         mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
         ResultReceiver receiver = new ResultReceiver(null) {
             @Override
             protected void onReceiveResult(int resultCode, Bundle resultData) {
                 assertEquals(TETHER_ERROR_NO_ERROR, resultCode);
-                mCallbacklatch.countDown();
             }
         };
         mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
         mLooper.dispatchAll();
-        callbackTimeoutHelper(mCallbacklatch);
         assertEquals(0, mEnMgr.uiProvisionCount);
         mEnMgr.reset();
 
@@ -277,12 +271,10 @@
             @Override
             protected void onReceiveResult(int resultCode, Bundle resultData) {
                 assertEquals(TETHER_ERROR_ENTITLEMENT_UNKNOWN, resultCode);
-                mCallbacklatch.countDown();
             }
         };
         mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, false);
         mLooper.dispatchAll();
-        callbackTimeoutHelper(mCallbacklatch);
         assertEquals(0, mEnMgr.uiProvisionCount);
         mEnMgr.reset();
         // 3. No cache value and ui entitlement check is needed.
@@ -291,12 +283,10 @@
             @Override
             protected void onReceiveResult(int resultCode, Bundle resultData) {
                 assertEquals(TETHER_ERROR_PROVISIONING_FAILED, resultCode);
-                mCallbacklatch.countDown();
             }
         };
         mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
         mLooper.dispatchAll();
-        callbackTimeoutHelper(mCallbacklatch);
         assertEquals(1, mEnMgr.uiProvisionCount);
         mEnMgr.reset();
         // 4. Cache value is TETHER_ERROR_PROVISIONING_FAILED and don't need to run entitlement
@@ -306,12 +296,10 @@
             @Override
             protected void onReceiveResult(int resultCode, Bundle resultData) {
                 assertEquals(TETHER_ERROR_PROVISIONING_FAILED, resultCode);
-                mCallbacklatch.countDown();
             }
         };
         mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, false);
         mLooper.dispatchAll();
-        callbackTimeoutHelper(mCallbacklatch);
         assertEquals(0, mEnMgr.uiProvisionCount);
         mEnMgr.reset();
         // 5. Cache value is TETHER_ERROR_PROVISIONING_FAILED and ui entitlement check is needed.
@@ -320,12 +308,10 @@
             @Override
             protected void onReceiveResult(int resultCode, Bundle resultData) {
                 assertEquals(TETHER_ERROR_NO_ERROR, resultCode);
-                mCallbacklatch.countDown();
             }
         };
         mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
         mLooper.dispatchAll();
-        callbackTimeoutHelper(mCallbacklatch);
         assertEquals(1, mEnMgr.uiProvisionCount);
         mEnMgr.reset();
         // 6. Cache value is TETHER_ERROR_NO_ERROR.
@@ -334,12 +320,10 @@
             @Override
             protected void onReceiveResult(int resultCode, Bundle resultData) {
                 assertEquals(TETHER_ERROR_NO_ERROR, resultCode);
-                mCallbacklatch.countDown();
             }
         };
         mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
         mLooper.dispatchAll();
-        callbackTimeoutHelper(mCallbacklatch);
         assertEquals(0, mEnMgr.uiProvisionCount);
         mEnMgr.reset();
         // 7. Test get value for other downstream type.
@@ -347,12 +331,10 @@
             @Override
             protected void onReceiveResult(int resultCode, Bundle resultData) {
                 assertEquals(TETHER_ERROR_ENTITLEMENT_UNKNOWN, resultCode);
-                mCallbacklatch.countDown();
             }
         };
         mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_USB, receiver, false);
         mLooper.dispatchAll();
-        callbackTimeoutHelper(mCallbacklatch);
         assertEquals(0, mEnMgr.uiProvisionCount);
         mEnMgr.reset();
         // 8. Test get value for invalid downstream type.
@@ -361,22 +343,14 @@
             @Override
             protected void onReceiveResult(int resultCode, Bundle resultData) {
                 assertEquals(TETHER_ERROR_ENTITLEMENT_UNKNOWN, resultCode);
-                mCallbacklatch.countDown();
             }
         };
         mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI_P2P, receiver, true);
         mLooper.dispatchAll();
-        callbackTimeoutHelper(mCallbacklatch);
         assertEquals(0, mEnMgr.uiProvisionCount);
         mEnMgr.reset();
     }
 
-    void callbackTimeoutHelper(final CountDownLatch latch) throws Exception {
-        if (!latch.await(1, TimeUnit.SECONDS)) {
-            fail("Timout, fail to receive callback");
-        }
-    }
-
     @Test
     public void verifyPermissionResult() {
         setupForRequiredProvisioning();
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityShellCommand.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityShellCommand.java
index ff59c24..20a11bd 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityShellCommand.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityShellCommand.java
@@ -17,6 +17,7 @@
 package com.android.server.accessibility;
 
 import android.annotation.NonNull;
+import android.app.ActivityManager;
 import android.os.ShellCommand;
 import android.os.UserHandle;
 
@@ -83,7 +84,7 @@
                 return null;
             }
         }
-        return UserHandle.USER_SYSTEM;
+        return ActivityManager.getCurrentUser();
     }
 
     @Override
diff --git a/services/core/java/com/android/server/am/AnrHelper.java b/services/core/java/com/android/server/am/AnrHelper.java
index bc45533..8f8990f 100644
--- a/services/core/java/com/android/server/am/AnrHelper.java
+++ b/services/core/java/com/android/server/am/AnrHelper.java
@@ -98,7 +98,7 @@
                 final long endTime = SystemClock.uptimeMillis();
                 Slog.d(TAG, "Completed ANR of " + r.mApp.processName + " in "
                         + (endTime - startTime) + "ms, latency " + reportLatency
-                        + (onlyDumpSelf ? "ms" : "ms (expired, only dump ANR app)"));
+                        + (onlyDumpSelf ? "ms (expired, only dump ANR app)" : "ms"));
             }
 
             mRunning.set(false);
diff --git a/services/core/java/com/android/server/content/ContentService.java b/services/core/java/com/android/server/content/ContentService.java
index 962f337..9a910bf 100644
--- a/services/core/java/com/android/server/content/ContentService.java
+++ b/services/core/java/com/android/server/content/ContentService.java
@@ -569,9 +569,10 @@
                 // Immediately dispatch notifications to foreground apps that
                 // are important to the user; all other background observers are
                 // delayed to avoid stampeding
+                final boolean noDelay = (key.flags & ContentResolver.NOTIFY_NO_DELAY) != 0;
                 final int procState = LocalServices.getService(ActivityManagerInternal.class)
                         .getUidProcessState(key.uid);
-                if (procState <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND) {
+                if (procState <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND || noDelay) {
                     task.run();
                 } else {
                     BackgroundThread.getHandler().postDelayed(task, BACKGROUND_OBSERVER_DELAY);
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index 1931079..c3413e8 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -98,18 +98,26 @@
     public List<MediaRoute2Info> getSystemRoutes() {
         final int uid = Binder.getCallingUid();
         final int userId = UserHandle.getUserHandleForUid(uid).getIdentifier();
+        final boolean hasModifyAudioRoutingPermission = mContext.checkCallingOrSelfPermission(
+                android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+                == PackageManager.PERMISSION_GRANTED;
 
         final long token = Binder.clearCallingIdentity();
         try {
             Collection<MediaRoute2Info> systemRoutes;
             synchronized (mLock) {
                 UserRecord userRecord = getOrCreateUserRecordLocked(userId);
-                MediaRoute2ProviderInfo providerInfo =
-                        userRecord.mHandler.mSystemProvider.getProviderInfo();
-                if (providerInfo != null) {
-                    systemRoutes = providerInfo.getRoutes();
+                if (hasModifyAudioRoutingPermission) {
+                    MediaRoute2ProviderInfo providerInfo =
+                            userRecord.mHandler.mSystemProvider.getProviderInfo();
+                    if (providerInfo != null) {
+                        systemRoutes = providerInfo.getRoutes();
+                    } else {
+                        systemRoutes = Collections.emptyList();
+                    }
                 } else {
-                    systemRoutes = Collections.emptyList();
+                    systemRoutes = new ArrayList<>();
+                    systemRoutes.add(userRecord.mHandler.mSystemProvider.getDefaultRoute());
                 }
             }
             return new ArrayList<>(systemRoutes);
@@ -122,18 +130,25 @@
     public RoutingSessionInfo getSystemSessionInfo() {
         final int uid = Binder.getCallingUid();
         final int userId = UserHandle.getUserHandleForUid(uid).getIdentifier();
+        final boolean hasModifyAudioRoutingPermission = mContext.checkCallingOrSelfPermission(
+                android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+                == PackageManager.PERMISSION_GRANTED;
 
         final long token = Binder.clearCallingIdentity();
         try {
             RoutingSessionInfo systemSessionInfo = null;
             synchronized (mLock) {
                 UserRecord userRecord = getOrCreateUserRecordLocked(userId);
-                List<RoutingSessionInfo> sessionInfos =
-                        userRecord.mHandler.mSystemProvider.getSessionInfos();
-                if (sessionInfos != null && !sessionInfos.isEmpty()) {
-                    systemSessionInfo = sessionInfos.get(0);
+                List<RoutingSessionInfo> sessionInfos;
+                if (hasModifyAudioRoutingPermission) {
+                    sessionInfos = userRecord.mHandler.mSystemProvider.getSessionInfos();
+                    if (sessionInfos != null && !sessionInfos.isEmpty()) {
+                        systemSessionInfo = sessionInfos.get(0);
+                    } else {
+                        Slog.w(TAG, "System provider does not have any session info.");
+                    }
                 } else {
-                    Slog.w(TAG, "System provider does not have any session info.");
+                    systemSessionInfo = userRecord.mHandler.mSystemProvider.getDefaultSessionInfo();
                 }
             }
             return systemSessionInfo;
@@ -654,10 +669,20 @@
             return;
         }
 
-        routerRecord.mUserRecord.mHandler.sendMessage(
-                obtainMessage(UserHandler::transferToRouteOnHandler,
-                        routerRecord.mUserRecord.mHandler,
-                        DUMMY_REQUEST_ID, routerRecord, uniqueSessionId, route));
+        String defaultRouteId =
+                routerRecord.mUserRecord.mHandler.mSystemProvider.getDefaultRoute().getId();
+        if (route.isSystemRoute() && !routerRecord.mHasModifyAudioRoutingPermission
+                && !TextUtils.equals(route.getId(), defaultRouteId)) {
+            routerRecord.mUserRecord.mHandler.sendMessage(
+                    obtainMessage(UserHandler::notifySessionCreationFailedToRouter,
+                            routerRecord.mUserRecord.mHandler,
+                            routerRecord, toOriginalRequestId(DUMMY_REQUEST_ID)));
+        } else {
+            routerRecord.mUserRecord.mHandler.sendMessage(
+                    obtainMessage(UserHandler::transferToRouteOnHandler,
+                            routerRecord.mUserRecord.mHandler,
+                            DUMMY_REQUEST_ID, routerRecord, uniqueSessionId, route));
+        }
     }
 
     private void setSessionVolumeWithRouter2Locked(@NonNull IMediaRouter2 router,
@@ -1185,18 +1210,42 @@
                 }
             }
 
-            List<IMediaRouter2> routers = getRouters();
+            List<IMediaRouter2> routersWithModifyAudioRoutingPermission = getRouters(true);
+            List<IMediaRouter2> routersWithoutModifyAudioRoutingPermission = getRouters(false);
             List<IMediaRouter2Manager> managers = getManagers();
+            List<MediaRoute2Info> defaultRoute = new ArrayList<>();
+            defaultRoute.add(mSystemProvider.getDefaultRoute());
+
             if (addedRoutes.size() > 0) {
-                notifyRoutesAddedToRouters(routers, addedRoutes);
+                notifyRoutesAddedToRouters(routersWithModifyAudioRoutingPermission, addedRoutes);
+                if (!provider.mIsSystemRouteProvider) {
+                    notifyRoutesAddedToRouters(routersWithoutModifyAudioRoutingPermission,
+                            addedRoutes);
+                } else if (prevInfo == null) {
+                    notifyRoutesAddedToRouters(routersWithoutModifyAudioRoutingPermission,
+                            defaultRoute);
+                } // 'else' is handled as changed routes
                 notifyRoutesAddedToManagers(managers, addedRoutes);
             }
             if (removedRoutes.size() > 0) {
-                notifyRoutesRemovedToRouters(routers, removedRoutes);
+                notifyRoutesRemovedToRouters(routersWithModifyAudioRoutingPermission,
+                        removedRoutes);
+                if (!provider.mIsSystemRouteProvider) {
+                    notifyRoutesRemovedToRouters(routersWithoutModifyAudioRoutingPermission,
+                            removedRoutes);
+                }
                 notifyRoutesRemovedToManagers(managers, removedRoutes);
             }
             if (changedRoutes.size() > 0) {
-                notifyRoutesChangedToRouters(routers, changedRoutes);
+                notifyRoutesChangedToRouters(routersWithModifyAudioRoutingPermission,
+                        changedRoutes);
+                if (!provider.mIsSystemRouteProvider) {
+                    notifyRoutesChangedToRouters(routersWithoutModifyAudioRoutingPermission,
+                            changedRoutes);
+                } else if (prevInfo != null) {
+                    notifyRoutesChangedToRouters(routersWithoutModifyAudioRoutingPermission,
+                            defaultRoute);
+                } // 'else' is handled as added routes
                 notifyRoutesChangedToManagers(managers, changedRoutes);
             }
         }
@@ -1223,6 +1272,15 @@
                         toOriginalRequestId(uniqueRequestId));
                 return;
             }
+            if (route.isSystemRoute() && !routerRecord.mHasModifyAudioRoutingPermission
+                    && !TextUtils.equals(route.getId(),
+                            mSystemProvider.getDefaultRoute().getId())) {
+                Slog.w(TAG, "MODIFY_AUDIO_ROUTING permission is required to transfer to"
+                        + route);
+                notifySessionCreationFailedToRouter(routerRecord,
+                        toOriginalRequestId(uniqueRequestId));
+                return;
+            }
 
             SessionCreationRequest request =
                     new SessionCreationRequest(routerRecord, uniqueRequestId, route, managerRecord);
@@ -1444,7 +1502,9 @@
                 if (service == null) {
                     return;
                 }
-                notifySessionInfoChangedToRouters(getRouters(), sessionInfo);
+                notifySessionInfoChangedToRouters(getRouters(true), sessionInfo);
+                notifySessionInfoChangedToRouters(getRouters(false),
+                        mSystemProvider.getDefaultSessionInfo());
                 return;
             }
 
@@ -1565,7 +1625,7 @@
             }
         }
 
-        private List<IMediaRouter2> getRouters() {
+        private List<IMediaRouter2> getAllRouters() {
             final List<IMediaRouter2> routers = new ArrayList<>();
             MediaRouter2ServiceImpl service = mServiceRef.get();
             if (service == null) {
@@ -1579,6 +1639,23 @@
             return routers;
         }
 
+        private List<IMediaRouter2> getRouters(boolean hasModifyAudioRoutingPermission) {
+            final List<IMediaRouter2> routers = new ArrayList<>();
+            MediaRouter2ServiceImpl service = mServiceRef.get();
+            if (service == null) {
+                return routers;
+            }
+            synchronized (service.mLock) {
+                for (RouterRecord routerRecord : mUserRecord.mRouterRecords) {
+                    if (hasModifyAudioRoutingPermission
+                            == routerRecord.mHasModifyAudioRoutingPermission) {
+                        routers.add(routerRecord.mRouter);
+                    }
+                }
+            }
+            return routers;
+        }
+
         private List<IMediaRouter2Manager> getManagers() {
             final List<IMediaRouter2Manager> managers = new ArrayList<>();
             MediaRouter2ServiceImpl service = mServiceRef.get();
diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
index 5b16d68..6e2feeb 100644
--- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
+++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
@@ -72,6 +72,7 @@
     // This should be the currently selected route.
     MediaRoute2Info mDefaultRoute;
     MediaRoute2Info mDeviceRoute;
+    RoutingSessionInfo mDefaultSessionInfo;
     final AudioRoutesInfo mCurAudioRoutesInfo = new AudioRoutesInfo();
 
     final IAudioRoutesObserver.Stub mAudioRoutesObserver = new IAudioRoutesObserver.Stub() {
@@ -114,6 +115,7 @@
             }
         });
         updateSessionInfosIfNeeded();
+
         mContext.registerReceiver(new VolumeChangeReceiver(),
                 new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION));
 
@@ -156,6 +158,10 @@
 
     @Override
     public void transferToRoute(long requestId, String sessionId, String routeId) {
+        if (TextUtils.equals(routeId, DEFAULT_ROUTE_ID)) {
+            // The currently selected route is the default route.
+            return;
+        }
         if (mBtRouteProvider != null) {
             if (TextUtils.equals(routeId, mDeviceRoute.getId())) {
                 mBtRouteProvider.transferTo(null);
@@ -182,6 +188,10 @@
         return mDefaultRoute;
     }
 
+    public RoutingSessionInfo getDefaultSessionInfo() {
+        return mDefaultSessionInfo;
+    }
+
     private void updateDeviceRoute(AudioRoutesInfo newRoutes) {
         int name = R.string.default_audio_route_name;
         if (newRoutes != null) {
@@ -229,8 +239,6 @@
      */
     boolean updateSessionInfosIfNeeded() {
         synchronized (mLock) {
-            // Prevent to execute this method before mBtRouteProvider is created.
-            if (mBtRouteProvider == null) return false;
             RoutingSessionInfo oldSessionInfo = mSessionInfos.isEmpty() ? null : mSessionInfos.get(
                     0);
 
@@ -238,14 +246,19 @@
                     SYSTEM_SESSION_ID, "" /* clientPackageName */)
                     .setSystemSession(true);
 
-            MediaRoute2Info selectedRoute = mBtRouteProvider.getSelectedRoute();
-            if (selectedRoute == null) {
-                selectedRoute = mDeviceRoute;
-            } else {
-                builder.addTransferableRoute(mDeviceRoute.getId());
+            MediaRoute2Info selectedRoute = mDeviceRoute;
+            if (mBtRouteProvider != null) {
+                MediaRoute2Info selectedBtRoute = mBtRouteProvider.getSelectedRoute();
+                if (selectedBtRoute != null) {
+                    selectedRoute = selectedBtRoute;
+                    builder.addTransferableRoute(mDeviceRoute.getId());
+                }
             }
             mSelectedRouteId = selectedRoute.getId();
-            mDefaultRoute = new MediaRoute2Info.Builder(DEFAULT_ROUTE_ID, selectedRoute).build();
+            mDefaultRoute = new MediaRoute2Info.Builder(DEFAULT_ROUTE_ID, selectedRoute)
+                    .setSystemRoute(true)
+                    .setProviderId(mUniqueId)
+                    .build();
             builder.addSelectedRoute(mSelectedRouteId);
 
             for (MediaRoute2Info route : mBtRouteProvider.getTransferableRoutes()) {
@@ -258,6 +271,12 @@
             } else {
                 mSessionInfos.clear();
                 mSessionInfos.add(newSessionInfo);
+                mDefaultSessionInfo = new RoutingSessionInfo.Builder(
+                        SYSTEM_SESSION_ID, "" /* clientPackageName */)
+                        .setProviderId(mUniqueId)
+                        .setSystemSession(true)
+                        .addSelectedRoute(DEFAULT_ROUTE_ID)
+                        .build();
                 return true;
             }
         }
@@ -302,6 +321,9 @@
                 } else if (mBtRouteProvider != null) {
                     mBtRouteProvider.setSelectedRouteVolume(newVolume);
                 }
+                mDefaultRoute = new MediaRoute2Info.Builder(mDefaultRoute)
+                        .setVolume(newVolume)
+                        .build();
                 publishProviderState();
             }
         }
diff --git a/services/core/java/com/android/server/pm/ApexManager.java b/services/core/java/com/android/server/pm/ApexManager.java
index 1e4dc7b..daf4bf2 100644
--- a/services/core/java/com/android/server/pm/ApexManager.java
+++ b/services/core/java/com/android/server/pm/ApexManager.java
@@ -399,7 +399,9 @@
         @VisibleForTesting
         protected IApexService waitForApexService() {
             try {
-                return IApexService.Stub.asInterface(Binder.waitForService("apexservice"));
+                // Since apexd is a trusted platform component, synchronized calls are allowable
+                return IApexService.Stub.asInterface(
+                        Binder.allowBlocking(Binder.waitForService("apexservice")));
             } catch (RemoteException e) {
                 throw new IllegalStateException("Required service apexservice not available");
             }
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index c47d215..c4d0342 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -6484,11 +6484,18 @@
         final Rect containingBounds = mTmpBounds;
         mCompatDisplayInsets.getContainerBounds(containingAppBounds, containingBounds, rotation,
                 orientation, orientationRequested, canChangeOrientation);
-        resolvedBounds.set(containingAppBounds);
+        resolvedBounds.set(containingBounds);
         // The size of floating task is fixed (only swap), so the aspect ratio is already correct.
         if (!mCompatDisplayInsets.mIsFloating) {
             applyAspectRatio(resolvedBounds, containingAppBounds, containingBounds);
         }
+        // If the bounds are restricted by fixed aspect ratio, the resolved bounds should be put in
+        // the container app bounds. Otherwise the entire container bounds are available.
+        final boolean fillContainer = resolvedBounds.equals(containingBounds);
+        if (!fillContainer) {
+            // The horizontal position should not cover insets.
+            resolvedBounds.left = containingAppBounds.left;
+        }
 
         // Use resolvedBounds to compute other override configurations such as appBounds. The bounds
         // are calculated in compat container space. The actual position on screen will be applied
@@ -6557,7 +6564,7 @@
         final int offsetX = getHorizontalCenterOffset(
                 (int) viewportW, (int) (contentW * mSizeCompatScale));
         // Above coordinates are in "@" space, now place "*" and "#" to screen space.
-        final int screenPosX = parentAppBounds.left + offsetX;
+        final int screenPosX = (fillContainer ? parentBounds.left : parentAppBounds.left) + offsetX;
         final int screenPosY = parentBounds.top;
         if (screenPosX != 0 || screenPosY != 0) {
             if (mSizeCompatBounds != null) {
@@ -7654,8 +7661,6 @@
                 // Ensure the app bounds won't overlap with insets.
                 Task.intersectWithInsetsIfFits(outAppBounds, outBounds, mNonDecorInsets[rotation]);
             }
-            // The horizontal position is centered and it should not cover insets.
-            outBounds.left = outAppBounds.left;
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 98d4b55..40243e8 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -3421,7 +3421,10 @@
 
     private void setInputMethodTarget(WindowState target, boolean targetWaitingAnim) {
         // Always update control target. This is needed to handle rotation.
-        updateImeControlTarget(target);
+        // We cannot set target as the control target, because mInputMethodTarget can only help
+        // decide the z-order of IME, but cannot control IME. Only the IME target reported from
+        // updateInputMethodTargetWindow can control IME.
+        updateImeControlTarget(mInputMethodControlTarget);
         if (target == mInputMethodTarget && mInputMethodTargetWaitingAnim == targetWaitingAnim) {
             return;
         }
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 8d3a4f2..dea2e96 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -32,6 +32,7 @@
 import static android.content.pm.PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT;
 import static android.content.pm.PackageManager.FEATURE_PC;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.os.Process.INVALID_UID;
 import static android.os.Process.SYSTEM_UID;
 import static android.os.Process.myPid;
 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
@@ -2573,6 +2574,8 @@
             String packageName, boolean fromClientToken) {
         final boolean callerCanManageAppTokens =
                 checkCallingPermission(MANAGE_APP_TOKENS, "addWindowToken()");
+        // WindowContext users usually don't hold MANAGE_APP_TOKEN permission. Check permissions
+        // by checkAddPermission.
         if (!callerCanManageAppTokens) {
             final int res = mPolicy.checkAddPermission(type, false /* isRoundedCornerOverlay */,
                     packageName, new int[1]);
@@ -2587,7 +2590,7 @@
             synchronized (mGlobalLock) {
                 if (!callerCanManageAppTokens) {
                     if (packageName == null || !unprivilegedAppCanCreateTokenWith(
-                            null /* parentWindow */, callingUid, type, type, null /* tokenForLog */,
+                            null /* parentWindow */, callingUid, type, type, binder,
                             packageName)) {
                         throw new SecurityException("Requires MANAGE_APP_TOKENS permission");
                     }
@@ -2612,7 +2615,7 @@
                     new WallpaperWindowToken(this, binder, true, dc, callerCanManageAppTokens);
                 } else {
                     new WindowToken(this, binder, type, true, dc, callerCanManageAppTokens,
-                            false /* roundedCornerOverlay */, fromClientToken);
+                            callingUid, false /* roundedCornerOverlay */, fromClientToken);
                 }
             }
         } finally {
@@ -2635,8 +2638,25 @@
 
     @Override
     public void removeWindowToken(IBinder binder, int displayId) {
-        if (!checkCallingPermission(MANAGE_APP_TOKENS, "removeWindowToken()")) {
-            throw new SecurityException("Requires MANAGE_APP_TOKENS permission");
+        final boolean callerCanManageAppTokens =
+                checkCallingPermission(MANAGE_APP_TOKENS, "removeWindowToken()");
+        final WindowToken windowToken;
+        synchronized (mGlobalLock) {
+            windowToken = mRoot.getWindowToken(binder);
+        }
+        if (windowToken == null) {
+            ProtoLog.w(WM_ERROR,
+                    "removeWindowToken: Attempted to remove non-existing token: %s", binder);
+            return;
+        }
+        final int callingUid = Binder.getCallingUid();
+
+        // If MANAGE_APP_TOKEN permission is not held(usually from WindowContext), callers can only
+        // remove the window tokens which they added themselves.
+        if (!callerCanManageAppTokens && (windowToken.getOwnerUid() == INVALID_UID
+                || callingUid != windowToken.getOwnerUid())) {
+            throw new SecurityException("removeWindowToken: Requires MANAGE_APP_TOKENS permission"
+                    + " to remove token owned by another uid");
         }
 
         final long origId = Binder.clearCallingIdentity();
@@ -2649,14 +2669,7 @@
                     return;
                 }
 
-                final WindowToken token = dc.removeWindowToken(binder);
-                if (token == null) {
-                    ProtoLog.w(WM_ERROR,
-                            "removeWindowToken: Attempted to remove non-existing token: %s",
-                            binder);
-                    return;
-                }
-
+                dc.removeWindowToken(binder);
                 dc.getInputMonitor().updateInputWindowsLw(true /*force*/);
             }
         } finally {
@@ -8006,6 +8019,33 @@
         }
     }
 
+    /** Set layer tracing flags. */
+    public void setLayerTracingFlags(int flags) {
+        mAtmInternal.enforceCallerIsRecentsOrHasPermission(android.Manifest.permission.DUMP,
+                "setLayerTracingFlags");
+        long token = Binder.clearCallingIdentity();
+        try {
+            Parcel data = null;
+            try {
+                IBinder sf = ServiceManager.getService("SurfaceFlinger");
+                if (sf != null) {
+                    data = Parcel.obtain();
+                    data.writeInterfaceToken("android.ui.ISurfaceComposer");
+                    data.writeInt(flags);
+                    sf.transact(1033 /* LAYER_TRACE_FLAGS_CODE */, data, null, 0 /* flags */);
+                }
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Failed to set layer tracing flags");
+            } finally {
+                if (data != null) {
+                    data.recycle();
+                }
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
     @Override
     public boolean mirrorDisplay(int displayId, SurfaceControl outSurfaceControl) {
         if (!checkCallingPermission(READ_FRAME_BUFFER, "mirrorDisplay()")) {
diff --git a/services/core/java/com/android/server/wm/WindowToken.java b/services/core/java/com/android/server/wm/WindowToken.java
index 23091a0..5976b48 100644
--- a/services/core/java/com/android/server/wm/WindowToken.java
+++ b/services/core/java/com/android/server/wm/WindowToken.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wm;
 
+import static android.os.Process.INVALID_UID;
 import static android.view.Display.INVALID_DISPLAY;
 import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
 import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
@@ -44,6 +45,7 @@
 import android.os.Debug;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.util.Slog;
 import android.util.proto.ProtoOutputStream;
 import android.view.DisplayInfo;
 import android.view.InsetsState;
@@ -106,6 +108,11 @@
     @VisibleForTesting
     final boolean mFromClientToken;
 
+    private DeathRecipient mDeathRecipient;
+    private boolean mBinderDied = false;
+
+    private final int mOwnerUid;
+
     /**
      * Used to fix the transform of the token to be rotated to a rotation different than it's
      * display. The window frames and surfaces corresponding to this token will be layouted and
@@ -165,6 +172,30 @@
         }
     }
 
+    private class DeathRecipient implements IBinder.DeathRecipient {
+        private boolean mHasUnlinkToDeath = false;
+
+        @Override
+        public void binderDied() {
+            synchronized (mWmService.mGlobalLock) {
+                mBinderDied = true;
+                removeImmediately();
+            }
+        }
+
+        void linkToDeath() throws RemoteException {
+            token.linkToDeath(DeathRecipient.this, 0);
+        }
+
+        void unlinkToDeath() {
+            if (mHasUnlinkToDeath) {
+                return;
+            }
+            token.unlinkToDeath(DeathRecipient.this, 0);
+            mHasUnlinkToDeath = true;
+        }
+    }
+
     /**
      * Compares two child window of this token and returns -1 if the first is lesser than the
      * second in terms of z-order and 1 otherwise.
@@ -193,23 +224,35 @@
 
     WindowToken(WindowManagerService service, IBinder _token, int type, boolean persistOnEmpty,
             DisplayContent dc, boolean ownerCanManageAppTokens, boolean roundedCornerOverlay) {
-        this(service, _token, type, persistOnEmpty, dc, ownerCanManageAppTokens,
+        this(service, _token, type, persistOnEmpty, dc, ownerCanManageAppTokens, INVALID_UID,
                 roundedCornerOverlay, false /* fromClientToken */);
     }
 
     WindowToken(WindowManagerService service, IBinder _token, int type, boolean persistOnEmpty,
-            DisplayContent dc, boolean ownerCanManageAppTokens, boolean roundedCornerOverlay,
-            boolean fromClientToken) {
+            DisplayContent dc, boolean ownerCanManageAppTokens, int ownerUid,
+            boolean roundedCornerOverlay, boolean fromClientToken) {
         super(service);
         token = _token;
         windowType = type;
         mPersistOnEmpty = persistOnEmpty;
         mOwnerCanManageAppTokens = ownerCanManageAppTokens;
+        mOwnerUid = ownerUid;
         mRoundedCornerOverlay = roundedCornerOverlay;
         mFromClientToken = fromClientToken;
         if (dc != null) {
             dc.addWindowToken(token, this);
         }
+        if (shouldReportToClient()) {
+            try {
+                mDeathRecipient = new DeathRecipient();
+                mDeathRecipient.linkToDeath();
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Unable to add window token with type " + windowType + " on "
+                        + "display " + dc.getDisplayId(), e);
+                mDeathRecipient = null;
+                return;
+            }
+        }
     }
 
     void removeAllWindowsIfPossible() {
@@ -222,7 +265,7 @@
     }
 
     void setExiting() {
-        if (mChildren.size() == 0) {
+        if (isEmpty()) {
             super.removeImmediately();
             return;
         }
@@ -340,6 +383,21 @@
         // Needs to occur after the token is removed from the display above to avoid attempt at
         // duplicate removal of this window container from it's parent.
         super.removeImmediately();
+
+        reportWindowTokenRemovedToClient();
+    }
+
+    private void reportWindowTokenRemovedToClient() {
+        if (!shouldReportToClient()) {
+            return;
+        }
+        mDeathRecipient.unlinkToDeath();
+        IWindowToken windowTokenClient = IWindowToken.Stub.asInterface(token);
+        try {
+            windowTokenClient.onWindowTokenRemoved();
+        } catch (RemoteException e) {
+            ProtoLog.w(WM_ERROR, "Could not report token removal to the window token client.");
+        }
     }
 
     @Override
@@ -361,17 +419,9 @@
     }
 
     void reportConfigToWindowTokenClient() {
-        if (asActivityRecord() != null) {
-            // Activities are updated through ATM callbacks.
+        if (!shouldReportToClient()) {
             return;
         }
-
-        // Unfortunately, this WindowToken is not from WindowContext so it cannot handle
-        // its own configuration changes.
-        if (!mFromClientToken) {
-            return;
-        }
-
         final Configuration config = getConfiguration();
         final int displayId = getDisplayContent().getDisplayId();
         if (config.equals(mLastReportedConfig) && displayId == mLastReportedDisplay) {
@@ -383,16 +433,26 @@
         mLastReportedDisplay = displayId;
 
         IWindowToken windowTokenClient = IWindowToken.Stub.asInterface(token);
-        if (windowTokenClient != null) {
-            try {
-                windowTokenClient.onConfigurationChanged(config, displayId);
-            } catch (RemoteException e) {
-                ProtoLog.w(WM_ERROR,
-                        "Could not report config changes to the window token client.");
-            }
+        try {
+            windowTokenClient.onConfigurationChanged(config, displayId);
+        } catch (RemoteException e) {
+            ProtoLog.w(WM_ERROR,
+                    "Could not report config changes to the window token client.");
         }
     }
 
+    /**
+     * @return {@code true} if this {@link WindowToken} is not an {@link ActivityRecord} and
+     * registered from client side.
+     */
+    private boolean shouldReportToClient() {
+        // Only report to client for WindowToken because Activities are updated through ATM
+        // callbacks.
+        return asActivityRecord() == null
+        // Report to {@link android.view.WindowTokenClient} if this token was registered from it.
+                && mFromClientToken && !mBinderDied;
+    }
+
     @Override
     void assignLayer(SurfaceControl.Transaction t, int layer) {
         if (windowType == TYPE_DOCK_DIVIDER) {
@@ -616,4 +676,8 @@
     int getWindowLayerFromType() {
         return mWmService.mPolicy.getWindowLayerFromTypeLw(windowType, mOwnerCanManageAppTokens);
     }
+
+    int getOwnerUid() {
+        return mOwnerUid;
+    }
 }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 67e83ba..d1d353d 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -1836,8 +1836,9 @@
         Bundle addSyntheticRestrictions(Bundle restrictions) {
             if (disableCamera) {
                 restrictions.putBoolean(UserManager.DISALLOW_CAMERA, true);
-            } else {
-                restrictions.remove(UserManager.DISALLOW_CAMERA);
+            }
+            if (requireAutoTime) {
+                restrictions.putBoolean(UserManager.DISALLOW_CONFIG_DATE_TIME, true);
             }
             return restrictions;
         }
@@ -1864,7 +1865,7 @@
 
         Bundle getEffectiveRestrictions() {
             return addSyntheticRestrictions(
-                    removeDeprecatedRestrictions(ensureUserRestrictions()));
+                    removeDeprecatedRestrictions(new Bundle(ensureUserRestrictions())));
         }
 
         Bundle getLocalUserRestrictions(int adminType) {
@@ -2747,6 +2748,8 @@
         // The following policies weren't available to PO, but will be available after migration.
         parentAdmin.disableCamera = doAdmin.disableCamera;
 
+        parentAdmin.requireAutoTime = doAdmin.requireAutoTime;
+
         // TODO(b/143516163): Uncomment once corresponding APIs are available via parent instance.
         // parentAdmin.disableScreenCapture = doAdmin.disableScreenCapture;
         // parentAdmin.accountTypesWithManagementDisabled.addAll(
@@ -7839,16 +7842,21 @@
         }
         Objects.requireNonNull(who, "ComponentName is null");
         final int userHandle = UserHandle.getCallingUserId();
+        boolean requireAutoTimeChanged = false;
         synchronized (getLockObject()) {
             ActiveAdmin admin = getActiveAdminForCallerLocked(who,
                     DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             if (admin.requireAutoTime != required) {
                 admin.requireAutoTime = required;
                 saveSettingsLocked(userHandle);
+                requireAutoTimeChanged = true;
             }
         }
-
-        // TODO: (b/145604635) Add upgrade case
+        // requireAutoTime is now backed by DISALLOW_CONFIG_DATE_TIME restriction, so propagate
+        // updated restrictions to the framework.
+        if (requireAutoTimeChanged) {
+            pushUserRestrictions(userHandle);
+        }
         // Turn AUTO_TIME on in settings if it is required
         if (required) {
             mInjector.binderWithCleanCallingIdentity(
diff --git a/services/incremental/IncrementalService.cpp b/services/incremental/IncrementalService.cpp
index d1153e6..92366e5 100644
--- a/services/incremental/IncrementalService.cpp
+++ b/services/incremental/IncrementalService.cpp
@@ -51,7 +51,7 @@
 namespace fs = std::filesystem;
 
 constexpr const char* kDataUsageStats = "android.permission.LOADER_USAGE_STATS";
-constexpr const char* kOpUsage = "android:get_usage_stats";
+constexpr const char* kOpUsage = "android:loader_usage_stats";
 
 namespace android::incremental {
 
diff --git a/services/incremental/test/IncrementalServiceTest.cpp b/services/incremental/test/IncrementalServiceTest.cpp
index 0635ae1..18ae4b5 100644
--- a/services/incremental/test/IncrementalServiceTest.cpp
+++ b/services/incremental/test/IncrementalServiceTest.cpp
@@ -137,6 +137,7 @@
                                           bool* _aidl_return) {
         mId = mountId;
         mListener = listener;
+        mServiceConnector = control.service;
         *_aidl_return = true;
         return binder::Status::ok();
     }
@@ -166,9 +167,17 @@
         mListener->onStatusChanged(mId, IDataLoaderStatusListener::DATA_LOADER_CREATED);
     }
 
+    int32_t setStorageParams(bool enableReadLogs) {
+        int32_t result = -1;
+        EXPECT_NE(mServiceConnector.get(), nullptr);
+        EXPECT_TRUE(mServiceConnector->setStorageParams(enableReadLogs, &result).isOk());
+        return result;
+    }
+
 private:
     int mId;
     sp<IDataLoaderStatusListener> mListener;
+    sp<IIncrementalServiceConnector> mServiceConnector;
     sp<IDataLoader> mDataLoader = sp<IDataLoader>(new FakeDataLoader());
 };
 
@@ -453,7 +462,7 @@
             mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel), {},
                                                IncrementalService::CreateOptions::CreateNew);
     ASSERT_GE(storageId, 0);
-    ASSERT_GE(mIncrementalService->setStorageParams(storageId, true), 0);
+    ASSERT_GE(mDataLoaderManager->setStorageParams(true), 0);
 }
 
 TEST_F(IncrementalServiceTest, testSetIncFsMountOptionsSuccessAndPermissionChanged) {
@@ -480,7 +489,7 @@
             mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel), {},
                                                IncrementalService::CreateOptions::CreateNew);
     ASSERT_GE(storageId, 0);
-    ASSERT_GE(mIncrementalService->setStorageParams(storageId, true), 0);
+    ASSERT_GE(mDataLoaderManager->setStorageParams(true), 0);
     ASSERT_NE(nullptr, mAppOpsManager->mStoredCallback.get());
     mAppOpsManager->mStoredCallback->opChanged(0, {});
 }
@@ -503,7 +512,7 @@
             mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel), {},
                                                IncrementalService::CreateOptions::CreateNew);
     ASSERT_GE(storageId, 0);
-    ASSERT_LT(mIncrementalService->setStorageParams(storageId, true), 0);
+    ASSERT_LT(mDataLoaderManager->setStorageParams(true), 0);
 }
 
 TEST_F(IncrementalServiceTest, testSetIncFsMountOptionsFails) {
@@ -526,7 +535,7 @@
             mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel), {},
                                                IncrementalService::CreateOptions::CreateNew);
     ASSERT_GE(storageId, 0);
-    ASSERT_LT(mIncrementalService->setStorageParams(storageId, true), 0);
+    ASSERT_LT(mDataLoaderManager->setStorageParams(true), 0);
 }
 
 TEST_F(IncrementalServiceTest, testMakeDirectory) {
diff --git a/services/systemcaptions/java/com/android/server/systemcaptions/RemoteSystemCaptionsManagerService.java b/services/systemcaptions/java/com/android/server/systemcaptions/RemoteSystemCaptionsManagerService.java
index c225d3f..1aab672 100644
--- a/services/systemcaptions/java/com/android/server/systemcaptions/RemoteSystemCaptionsManagerService.java
+++ b/services/systemcaptions/java/com/android/server/systemcaptions/RemoteSystemCaptionsManagerService.java
@@ -16,6 +16,8 @@
 
 package com.android.server.systemcaptions;
 
+import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+
 import android.annotation.Nullable;
 import android.content.ComponentName;
 import android.content.Context;
@@ -37,6 +39,8 @@
     private static final String SERVICE_INTERFACE =
             "android.service.systemcaptions.SystemCaptionsManagerService";
 
+    private static final int MSG_BIND = 1;
+
     private final Object mLock = new Object();
 
     private final Context mContext;
@@ -71,18 +75,26 @@
         if (mVerbose) {
             Slog.v(TAG, "initialize()");
         }
-        ensureBound();
+        scheduleBind();
     }
 
-    void destroy() {
+    /**
+     * Destroys this service.
+     */
+    public void destroy() {
+        mHandler.sendMessage(
+                obtainMessage(RemoteSystemCaptionsManagerService::handleDestroy, this));
+    }
+
+    void handleDestroy() {
         if (mVerbose) {
-            Slog.v(TAG, "destroy()");
+            Slog.v(TAG, "handleDestroy()");
         }
 
         synchronized (mLock) {
             if (mDestroyed) {
                 if (mVerbose) {
-                    Slog.v(TAG, "destroy(): Already destroyed");
+                    Slog.v(TAG, "handleDestroy(): Already destroyed");
                 }
                 return;
             }
@@ -97,14 +109,24 @@
         }
     }
 
-    private void ensureBound() {
+    private void scheduleBind() {
+        if (mHandler.hasMessages(MSG_BIND)) {
+            if (mVerbose) Slog.v(TAG, "scheduleBind(): already scheduled");
+            return;
+        }
+        mHandler.sendMessage(
+                obtainMessage(RemoteSystemCaptionsManagerService::handleEnsureBound, this)
+                .setWhat(MSG_BIND));
+    }
+
+    private void handleEnsureBound() {
         synchronized (mLock) {
             if (mService != null || mBinding) {
                 return;
             }
 
             if (mVerbose) {
-                Slog.v(TAG, "ensureBound(): binding");
+                Slog.v(TAG, "handleEnsureBound(): binding");
             }
             mBinding = true;
 
diff --git a/services/tests/servicestests/res/raw/comp_policies_primary.xml b/services/tests/servicestests/res/raw/comp_policies_primary.xml
index d30f479..8b7709e 100644
--- a/services/tests/servicestests/res/raw/comp_policies_primary.xml
+++ b/services/tests/servicestests/res/raw/comp_policies_primary.xml
@@ -3,6 +3,7 @@
     <admin name="com.android.frameworks.servicestests/com.android.server.devicepolicy.DummyDeviceAdmins$Admin1">
         <policies flags="991"/>
         <password-history-length value="33" />
+        <require_auto_time value="true" />
         <user-restrictions no_bluetooth="true" />
     </admin>
 </policies>
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceMigrationTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceMigrationTest.java
index de2addf..c9bd01a 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceMigrationTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceMigrationTest.java
@@ -385,6 +385,15 @@
             assertFalse("User restriction was put into non-parent PO instance",
                     dpm.getUserRestrictions(admin1).containsKey(UserManager.DISALLOW_BLUETOOTH));
 
+            assertTrue("User restriction wasn't migrated to PO parent instance",
+                    dpms.getProfileOwnerAdminLocked(COPE_PROFILE_USER_ID)
+                            .getParentActiveAdmin()
+                            .getEffectiveRestrictions()
+                            .containsKey(UserManager.DISALLOW_CONFIG_DATE_TIME));
+            assertFalse("User restriction was put into non-parent PO instance",
+                    dpms.getProfileOwnerAdminLocked(COPE_PROFILE_USER_ID)
+                            .getEffectiveRestrictions()
+                            .containsKey(UserManager.DISALLOW_CONFIG_DATE_TIME));
             // TODO(b/143516163): verify more policies.
         });
     }
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index d780370..fe224ce 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -2032,13 +2032,17 @@
                 eq(false));
         DpmTestUtils.assertRestrictions(
                 DpmTestUtils.newRestrictions(UserManager.DISALLOW_CAMERA),
-                parentDpm.getUserRestrictions(admin1)
+                dpms.getProfileOwnerAdminLocked(DpmMockContext.CALLER_USER_HANDLE)
+                        .getParentActiveAdmin()
+                        .getEffectiveRestrictions()
         );
 
         parentDpm.setCameraDisabled(admin1, false);
         DpmTestUtils.assertRestrictions(
                 DpmTestUtils.newRestrictions(),
-                parentDpm.getUserRestrictions(admin1)
+                dpms.getProfileOwnerAdminLocked(DpmMockContext.CALLER_USER_HANDLE)
+                        .getParentActiveAdmin()
+                        .getEffectiveRestrictions()
         );
         reset(getServices().userManagerInternal);
     }
@@ -2053,7 +2057,9 @@
         parentDpm.clearUserRestriction(admin1, restriction);
         DpmTestUtils.assertRestrictions(
                 DpmTestUtils.newRestrictions(),
-                parentDpm.getUserRestrictions(admin1)
+                dpms.getProfileOwnerAdminLocked(DpmMockContext.CALLER_USER_HANDLE)
+                        .getParentActiveAdmin()
+                        .getEffectiveRestrictions()
         );
     }
 
@@ -2088,11 +2094,7 @@
     private void assertNoDeviceOwnerRestrictions() {
         DpmTestUtils.assertRestrictions(
                 DpmTestUtils.newRestrictions(),
-                getDeviceOwner().ensureUserRestrictions()
-        );
-        DpmTestUtils.assertRestrictions(
-                DpmTestUtils.newRestrictions(),
-                dpm.getUserRestrictions(admin1)
+                getDeviceOwner().getEffectiveRestrictions()
         );
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 6734694..e47792f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -243,6 +243,26 @@
     }
 
     @Test
+    public void testAspectRatioMatchParentBoundsAndImeAttachable() {
+        setUpApp(new TestDisplayContent.Builder(mService, 1000, 2000)
+                .setSystemDecorations(true).build());
+        prepareUnresizable(2f /* maxAspect */, SCREEN_ORIENTATION_UNSPECIFIED);
+        assertFitted();
+
+        rotateDisplay(mActivity.mDisplayContent, ROTATION_90);
+        mActivity.mDisplayContent.mInputMethodTarget = addWindowToActivity(mActivity);
+        // Because the aspect ratio of display doesn't exceed the max aspect ratio of activity.
+        // The activity should still fill its parent container and IME can attach to the activity.
+        assertTrue(mActivity.matchParentBounds());
+        assertTrue(mActivity.mDisplayContent.isImeAttachedToApp());
+
+        final Rect letterboxInnerBounds = new Rect();
+        mActivity.getLetterboxInnerBounds(letterboxInnerBounds);
+        // The activity should not have letterbox.
+        assertTrue(letterboxInnerBounds.isEmpty());
+    }
+
+    @Test
     public void testMoveToDifferentOrientDisplay() {
         setUpDisplaySizeWithApp(1000, 2500);
         prepareUnresizable(-1.f /* maxAspect */, SCREEN_ORIENTATION_PORTRAIT);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java
index 57c7504..d2a2732 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java
@@ -139,6 +139,7 @@
             spyOn(displayPolicy);
             if (mSystemDecorations) {
                 doReturn(true).when(newDisplay).supportsSystemDecorations();
+                doReturn(true).when(displayPolicy).hasNavigationBar();
             } else {
                 doReturn(false).when(displayPolicy).hasNavigationBar();
                 doReturn(false).when(displayPolicy).hasStatusBar();
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
index da4bde5..79b9ae1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -16,13 +16,18 @@
 
 package com.android.server.wm;
 
+import static android.os.Process.INVALID_UID;
 import static android.view.WindowManager.LayoutParams.TYPE_TOAST;
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 import android.content.pm.PackageManager;
 import android.os.IBinder;
@@ -85,4 +90,19 @@
         assertFalse(windowToken.mRoundedCornerOverlay);
         assertTrue(windowToken.mFromClientToken);
     }
+
+    @Test(expected = SecurityException.class)
+    public void testRemoveWindowToken_ownerUidNotMatch_throwException() {
+        IBinder token = mock(IBinder.class);
+        mWm.addWindowTokenWithOptions(token, TYPE_TOAST, mDisplayContent.getDisplayId(),
+                null /* options */, null /* options */);
+
+        spyOn(mWm);
+        when(mWm.checkCallingPermission(anyString(), anyString())).thenReturn(false);
+        WindowToken windowToken = mWm.mRoot.getWindowToken(token);
+        spyOn(windowToken);
+        when(windowToken.getOwnerUid()).thenReturn(INVALID_UID);
+
+        mWm.removeWindowToken(token, mDisplayContent.getDisplayId());
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTokenTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowTokenTests.java
index 7a347cb..76479cb 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTokenTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTokenTests.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wm;
 
+import static android.os.Process.INVALID_UID;
 import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
@@ -152,7 +153,7 @@
 
         token = new WindowToken(mDisplayContent.mWmService, mock(IBinder.class), TYPE_TOAST,
                 true /* persistOnEmpty */, mDisplayContent, true /* ownerCanManageAppTokens */,
-                true /* roundedCornerOverlay */, true /* fromClientToken */);
+                INVALID_UID, true /* roundedCornerOverlay */, true /* fromClientToken */);
         assertTrue(token.mRoundedCornerOverlay);
         assertTrue(token.mFromClientToken);
     }
@@ -166,7 +167,7 @@
     public void testSurfaceCreatedForWindowToken() {
         final WindowToken fromClientToken = new WindowToken(mDisplayContent.mWmService,
                 mock(IBinder.class), TYPE_APPLICATION_OVERLAY, true /* persistOnEmpty */,
-                mDisplayContent, true /* ownerCanManageAppTokens */,
+                mDisplayContent, true /* ownerCanManageAppTokens */, INVALID_UID,
                 true /* roundedCornerOverlay */, true /* fromClientToken */);
         assertNull(fromClientToken.mSurfaceControl);
 
@@ -175,7 +176,7 @@
 
         final WindowToken nonClientToken = new WindowToken(mDisplayContent.mWmService,
                 mock(IBinder.class), TYPE_TOAST, true /* persistOnEmpty */, mDisplayContent,
-                true /* ownerCanManageAppTokens */, true /* roundedCornerOverlay */,
+                true /* ownerCanManageAppTokens */, INVALID_UID, true /* roundedCornerOverlay */,
                 false /* fromClientToken */);
         assertNotNull(nonClientToken.mSurfaceControl);
     }
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index d6cdaa6..f623649 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -147,6 +147,10 @@
  * information unless it has the appropriate permissions declared in
  * its manifest file. Where permissions apply, they are noted in the
  * the methods through which you access the protected information.
+ *
+ * <p>TelephonyManager is intended for use on devices that implement
+ * {@link android.content.pm.PackageManager#FEATURE_TELEPHONY FEATURE_TELEPHONY}. On devices
+ * that do not implement this feature, the behavior is not reliable.
  */
 @SystemService(Context.TELEPHONY_SERVICE)
 public class TelephonyManager {
@@ -5842,6 +5846,10 @@
      * {@link android.telephony.PhoneStateListener#onCellInfoChanged onCellInfoChanged()}
      * for each active subscription.
      *
+     * <p>This method returns valid data for devices with
+     * {@link android.content.pm.PackageManager#FEATURE_TELEPHONY FEATURE_TELEPHONY}. On devices
+     * that do not implement this feature, the behavior is not reliable.
+     *
      * @param executor the executor on which callback will be invoked.
      * @param callback a callback to receive CellInfo.
      */
@@ -5888,6 +5896,10 @@
      * {@link android.telephony.PhoneStateListener#onCellInfoChanged onCellInfoChanged()}
      * for each active subscription.
      *
+     * <p>This method returns valid data for devices with
+     * {@link android.content.pm.PackageManager#FEATURE_TELEPHONY FEATURE_TELEPHONY}. On devices
+     * that do not implement this feature, the behavior is not reliable.
+     *
      * @param workSource the requestor to whom the power consumption for this should be attributed.
      * @param executor the executor on which callback will be invoked.
      * @param callback a callback to receive CellInfo.
diff --git a/tests/permission/src/com/android/framework/permission/tests/WindowManagerPermissionTests.java b/tests/permission/src/com/android/framework/permission/tests/WindowManagerPermissionTests.java
index 4c2a984..737665f 100644
--- a/tests/permission/src/com/android/framework/permission/tests/WindowManagerPermissionTests.java
+++ b/tests/permission/src/com/android/framework/permission/tests/WindowManagerPermissionTests.java
@@ -17,6 +17,7 @@
 package com.android.framework.permission.tests;
 
 import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
 
 import android.os.Binder;
 import android.os.RemoteException;
@@ -27,6 +28,8 @@
 
 import junit.framework.TestCase;
 
+import org.junit.Test;
+
 /**
  * TODO: Remove this. This is only a placeholder, need to implement this.
  */
@@ -53,7 +56,7 @@
         }
 
         try {
-            mWm.addWindowToken(null, 0, DEFAULT_DISPLAY);
+            mWm.addWindowToken(null, TYPE_APPLICATION, DEFAULT_DISPLAY);
             fail("IWindowManager.addWindowToken did not throw SecurityException as"
                     + " expected");
         } catch (SecurityException e) {
@@ -63,16 +66,6 @@
         }
 
         try {
-            mWm.removeWindowToken(null, DEFAULT_DISPLAY);
-            fail("IWindowManager.removeWindowToken did not throw SecurityException as"
-                    + " expected");
-        } catch (SecurityException e) {
-            // expected
-        } catch (RemoteException e) {
-            fail("Unexpected remote exception");
-        }
-
-        try {
             mWm.prepareAppTransition(0, false);
             fail("IWindowManager.prepareAppTransition did not throw SecurityException as"
                     + " expected");
@@ -182,4 +175,29 @@
             fail("Unexpected remote exception");
         }
     }
+
+    @Test
+    public void testADD_WINDOW_TOKEN_WITH_OPTIONS() {
+        // Verify if addWindowTokenWithOptions throw SecurityException for privileged window type.
+        try {
+            mWm.addWindowTokenWithOptions(null, TYPE_APPLICATION, DEFAULT_DISPLAY, null, "");
+            fail("IWindowManager.addWindowTokenWithOptions did not throw SecurityException as"
+                    + " expected");
+        } catch (SecurityException e) {
+            // expected
+        } catch (RemoteException e) {
+            fail("Unexpected remote exception");
+        }
+
+        // Verify if addWindowTokenWithOptions throw SecurityException for null packageName.
+        try {
+            mWm.addWindowTokenWithOptions(null, TYPE_APPLICATION, DEFAULT_DISPLAY, null, null);
+            fail("IWindowManager.addWindowTokenWithOptions did not throw SecurityException as"
+                    + " expected");
+        } catch (SecurityException e) {
+            // expected
+        } catch (RemoteException e) {
+            fail("Unexpected remote exception");
+        }
+    }
 }
diff --git a/tests/utils/testutils/java/com/android/server/wm/test/filters/FrameworksTestsFilter.java b/tests/utils/testutils/java/com/android/server/wm/test/filters/FrameworksTestsFilter.java
index 3026e0b..0dd45ba 100644
--- a/tests/utils/testutils/java/com/android/server/wm/test/filters/FrameworksTestsFilter.java
+++ b/tests/utils/testutils/java/com/android/server/wm/test/filters/FrameworksTestsFilter.java
@@ -47,7 +47,8 @@
             "android.view.InsetsSourceConsumerTest",
             "android.view.InsetsStateTest",
             "android.view.WindowMetricsTest",
-            "android.view.PendingInsetsControllerTest"
+            "android.view.PendingInsetsControllerTest",
+            "android.app.WindowContextTest"
     };
 
     public FrameworksTestsFilter(Bundle testArgs) {
diff --git a/tools/stats_log_api_gen/atoms_info_writer.cpp b/tools/stats_log_api_gen/atoms_info_writer.cpp
index 23a0f72..5fe9498 100644
--- a/tools/stats_log_api_gen/atoms_info_writer.cpp
+++ b/tools/stats_log_api_gen/atoms_info_writer.cpp
@@ -42,7 +42,6 @@
     fprintf(out,
             "  const static std::set<int> "
             "kTruncatingTimestampAtomBlackList;\n");
-    fprintf(out, "  const static std::map<int, int> kAtomsWithUidField;\n");
     fprintf(out, "  const static std::set<int> kAtomsWithAttributionChain;\n");
     fprintf(out,
             "  const static std::map<int, StateAtomFieldOptions> "
@@ -101,28 +100,6 @@
     fprintf(out, "};\n");
     fprintf(out, "\n");
 
-    fprintf(out, "static std::map<int, int> getAtomUidField() {\n");
-    fprintf(out, "    std::map<int, int> uidField;\n");
-    for (AtomDeclSet::const_iterator atomIt = atoms.decls.begin(); atomIt != atoms.decls.end();
-         atomIt++) {
-        if ((*atomIt)->uidField == 0) {
-            continue;
-        }
-        fprintf(out,
-                "\n    // Adding uid field for atom "
-                "(%d)%s\n",
-                (*atomIt)->code, (*atomIt)->name.c_str());
-        fprintf(out, "    uidField[%d /* %s */] = %d;\n", (*atomIt)->code,
-                make_constant_name((*atomIt)->name).c_str(), (*atomIt)->uidField);
-    }
-
-    fprintf(out, "    return uidField;\n");
-    fprintf(out, "};\n");
-
-    fprintf(out,
-            "const std::map<int, int> AtomsInfo::kAtomsWithUidField = "
-            "getAtomUidField();\n");
-
     fprintf(out,
             "static std::map<int, StateAtomFieldOptions> "
             "getStateAtomFieldOptions() {\n");
diff --git a/wifi/jarjar-rules.txt b/wifi/jarjar-rules.txt
index eeb006e..cf11f43 100644
--- a/wifi/jarjar-rules.txt
+++ b/wifi/jarjar-rules.txt
@@ -53,6 +53,9 @@
 rule com.android.internal.messages.SystemMessageProto* com.android.server.x.wifi.messages.SystemMessageProto@1
 # Use our statically linked PlatformProperties library
 rule android.sysprop.** com.android.server.x.wifi.sysprop.@1
+# Use our statically linked HIDL stubs
+rule android.hardware.** com.android.server.x.wifi.hardware.@1
+rule android.hidl.** com.android.server.x.wifi.hidl.@1
 
 # used by both framework-wifi and wifi-service
 rule android.content.pm.BaseParceledListSlice* android.x.net.wifi.util.BaseParceledListSlice@1