Merge "Fix initialization bug in LocationRequest" into rvc-dev
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/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..9cdf582 100644
--- a/cmds/statsd/tests/LogEntryMatcher_test.cpp
+++ b/cmds/statsd/tests/LogEntryMatcher_test.cpp
@@ -48,15 +48,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 +58,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,15 +68,9 @@
     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,
@@ -99,13 +81,8 @@
 
     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
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..3e1cc5e 100644
--- a/cmds/statsd/tests/external/puller_util_test.cpp
+++ b/cmds/statsd/tests/external/puller_util_test.cpp
@@ -23,6 +23,7 @@
 #include "../metrics/metrics_test_helper.h"
 #include "stats_event.h"
 #include "statslog_statsdtest.h"
+#include "tests/statsd_test_util.h"
 
 #ifdef __ANDROID__
 
@@ -71,14 +72,9 @@
     AStatsEvent_writeInt32(statsEvent, uid);
     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 +82,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/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/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/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/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/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 f7a05d4..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);
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/systemui/bubbles/BubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
index 037f04ec..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/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/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;