Merge "Do not dispatch leashes if they are not ready" into rvc-dev
diff --git a/Android.bp b/Android.bp
index bd30de2..8d3b4af 100644
--- a/Android.bp
+++ b/Android.bp
@@ -708,11 +708,9 @@
     name: "framework-services-net-module-wifi-shared-srcs",
     srcs: [
         "core/java/android/net/DhcpResults.java",
-        "core/java/android/net/shared/Inet4AddressUtils.java",
         "core/java/android/net/shared/InetAddressUtils.java",
         "core/java/android/net/util/IpUtils.java",
         "core/java/android/util/LocalLog.java",
-        "core/java/com/android/internal/util/Preconditions.java",
     ],
 }
 
@@ -1175,7 +1173,10 @@
         "framework-annotations-lib",
         "unsupportedappusage",
     ],
-    visibility: ["//frameworks/base/wifi"],
+    visibility: [
+        "//frameworks/base/wifi",
+        "//frameworks/base/services/net",
+    ],
 }
 
 filegroup {
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java
index 6af1178..b4a7cd4 100644
--- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java
@@ -131,6 +131,10 @@
                         LEASE_ACQUISITION_WAIT_DURATION_MS = properties.getLong(key,
                                 DEFAULT_LEASE_ACQUISITION_WAIT_DURATION_MS);
                         break;
+                    case KEY_COMMIT_COOL_OFF_DURATION_MS:
+                        COMMIT_COOL_OFF_DURATION_MS = properties.getLong(key,
+                                DEFAULT_COMMIT_COOL_OFF_DURATION_MS);
+                        break;
                     default:
                         Slog.wtf(TAG, "Unknown key in device config properties: " + key);
                 }
@@ -149,6 +153,9 @@
             fout.println(String.format(dumpFormat, KEY_LEASE_ACQUISITION_WAIT_DURATION_MS,
                     TimeUtils.formatDuration(LEASE_ACQUISITION_WAIT_DURATION_MS),
                     TimeUtils.formatDuration(DEFAULT_LEASE_ACQUISITION_WAIT_DURATION_MS)));
+            fout.println(String.format(dumpFormat, KEY_COMMIT_COOL_OFF_DURATION_MS,
+                    TimeUtils.formatDuration(COMMIT_COOL_OFF_DURATION_MS),
+                    TimeUtils.formatDuration(DEFAULT_COMMIT_COOL_OFF_DURATION_MS)));
         }
     }
 
diff --git a/apex/statsd/aidl/android/os/IStatsd.aidl b/apex/statsd/aidl/android/os/IStatsd.aidl
index 80308d2..0d3f420 100644
--- a/apex/statsd/aidl/android/os/IStatsd.aidl
+++ b/apex/statsd/aidl/android/os/IStatsd.aidl
@@ -182,12 +182,6 @@
     void unsetBroadcastSubscriber(long configId, long subscriberId, int callingUid);
 
     /**
-     * Apps can send an atom via this application breadcrumb with the specified label and state for
-     * this label. This allows building custom metrics and predicates.
-     */
-    void sendAppBreadcrumbAtom(int label, int state);
-
-    /**
      * Tell the stats daemon that all the pullers registered during boot have been sent.
      */
     oneway void allPullersFromBootRegistered();
diff --git a/apex/statsd/framework/java/android/util/StatsLog.java b/apex/statsd/framework/java/android/util/StatsLog.java
index 536b71a..4eeae57 100644
--- a/apex/statsd/framework/java/android/util/StatsLog.java
+++ b/apex/statsd/framework/java/android/util/StatsLog.java
@@ -25,8 +25,7 @@
 import android.annotation.SystemApi;
 import android.content.Context;
 import android.os.IStatsd;
-import android.os.RemoteException;
-import android.os.StatsFrameworkInitializer;
+import android.os.Process;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.util.StatsdStatsLog;
@@ -45,10 +44,6 @@
     private static final boolean DEBUG = false;
     private static final int EXPERIMENT_IDS_FIELD_ID = 1;
 
-    private static IStatsd sService;
-
-    private static Object sLogLock = new Object();
-
     private StatsLog() {
     }
 
@@ -59,26 +54,13 @@
      * @return True if the log request was sent to statsd.
      */
     public static boolean logStart(int label) {
-        synchronized (sLogLock) {
-            try {
-                IStatsd service = getIStatsdLocked();
-                if (service == null) {
-                    if (DEBUG) {
-                        Log.d(TAG, "Failed to find statsd when logging start");
-                    }
-                    return false;
-                }
-                service.sendAppBreadcrumbAtom(label,
-                        StatsdStatsLog.APP_BREADCRUMB_REPORTED__STATE__START);
-                return true;
-            } catch (RemoteException e) {
-                sService = null;
-                if (DEBUG) {
-                    Log.d(TAG, "Failed to connect to statsd when logging start");
-                }
-                return false;
-            }
-        }
+        int callingUid = Process.myUid();
+        StatsdStatsLog.write(
+                StatsdStatsLog.APP_BREADCRUMB_REPORTED,
+                callingUid,
+                label,
+                StatsdStatsLog.APP_BREADCRUMB_REPORTED__STATE__START);
+        return true;
     }
 
     /**
@@ -88,26 +70,13 @@
      * @return True if the log request was sent to statsd.
      */
     public static boolean logStop(int label) {
-        synchronized (sLogLock) {
-            try {
-                IStatsd service = getIStatsdLocked();
-                if (service == null) {
-                    if (DEBUG) {
-                        Log.d(TAG, "Failed to find statsd when logging stop");
-                    }
-                    return false;
-                }
-                service.sendAppBreadcrumbAtom(
-                        label, StatsdStatsLog.APP_BREADCRUMB_REPORTED__STATE__STOP);
-                return true;
-            } catch (RemoteException e) {
-                sService = null;
-                if (DEBUG) {
-                    Log.d(TAG, "Failed to connect to statsd when logging stop");
-                }
-                return false;
-            }
-        }
+        int callingUid = Process.myUid();
+        StatsdStatsLog.write(
+                StatsdStatsLog.APP_BREADCRUMB_REPORTED,
+                callingUid,
+                label,
+                StatsdStatsLog.APP_BREADCRUMB_REPORTED__STATE__STOP);
+        return true;
     }
 
     /**
@@ -117,26 +86,13 @@
      * @return True if the log request was sent to statsd.
      */
     public static boolean logEvent(int label) {
-        synchronized (sLogLock) {
-            try {
-                IStatsd service = getIStatsdLocked();
-                if (service == null) {
-                    if (DEBUG) {
-                        Log.d(TAG, "Failed to find statsd when logging event");
-                    }
-                    return false;
-                }
-                service.sendAppBreadcrumbAtom(
-                        label, StatsdStatsLog.APP_BREADCRUMB_REPORTED__STATE__UNSPECIFIED);
-                return true;
-            } catch (RemoteException e) {
-                sService = null;
-                if (DEBUG) {
-                    Log.d(TAG, "Failed to connect to statsd when logging event");
-                }
-                return false;
-            }
-        }
+        int callingUid = Process.myUid();
+        StatsdStatsLog.write(
+                StatsdStatsLog.APP_BREADCRUMB_REPORTED,
+                callingUid,
+                label,
+                StatsdStatsLog.APP_BREADCRUMB_REPORTED__STATE__UNSPECIFIED);
+        return true;
     }
 
     /**
@@ -181,17 +137,6 @@
         return true;
     }
 
-    private static IStatsd getIStatsdLocked() throws RemoteException {
-        if (sService != null) {
-            return sService;
-        }
-        sService = IStatsd.Stub.asInterface(StatsFrameworkInitializer
-            .getStatsServiceManager()
-            .getStatsdServiceRegisterer()
-            .get());
-        return sService;
-    }
-
     /**
      * Write an event to stats log using the raw format.
      *
diff --git a/api/current.txt b/api/current.txt
index 80e2d00..3f8bee9 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -48759,12 +48759,8 @@
     method public void onCapabilitiesStatusChanged(@NonNull android.telephony.ims.feature.MmTelFeature.MmTelCapabilities);
   }
 
-  public class ImsRcsManager implements android.telephony.ims.RegistrationManager {
-    method @RequiresPermission("android.permission.READ_PRIVILEGED_PHONE_STATE") public void getRegistrationState(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
-    method @RequiresPermission("android.permission.READ_PRIVILEGED_PHONE_STATE") public void getRegistrationTransportType(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
+  public class ImsRcsManager {
     method @NonNull public android.telephony.ims.RcsUceAdapter getUceAdapter();
-    method @RequiresPermission("android.permission.READ_PRIVILEGED_PHONE_STATE") public void registerImsRegistrationCallback(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.ims.RegistrationManager.RegistrationCallback) throws android.telephony.ims.ImsException;
-    method @RequiresPermission("android.permission.READ_PRIVILEGED_PHONE_STATE") public void unregisterImsRegistrationCallback(@NonNull android.telephony.ims.RegistrationManager.RegistrationCallback);
     field public static final String ACTION_SHOW_CAPABILITY_DISCOVERY_OPT_IN = "android.telephony.ims.action.SHOW_CAPABILITY_DISCOVERY_OPT_IN";
   }
 
diff --git a/api/test-current.txt b/api/test-current.txt
index 755380e..ca291f3 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -126,6 +126,7 @@
 
   public class ActivityTaskManager {
     method @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS) public void clearLaunchParamsForPackages(java.util.List<java.lang.String>);
+    method public static boolean currentUiModeSupportsErrorDialogs(@NonNull android.content.Context);
     method @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS) public String listAllStacks();
     method @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS) public void moveTaskToStack(int, int, boolean);
     method @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS) public boolean moveTopActivityToPinnedStack(int, android.graphics.Rect);
@@ -504,6 +505,11 @@
     method public boolean isStatusBarExpansionDisabled();
   }
 
+  public class TaskInfo {
+    method @NonNull public android.content.res.Configuration getConfiguration();
+    method @NonNull public android.window.WindowContainerToken getToken();
+  }
+
   public class TimePickerDialog extends android.app.AlertDialog implements android.content.DialogInterface.OnClickListener android.widget.TimePicker.OnTimeChangedListener {
     method public android.widget.TimePicker getTimePicker();
   }
diff --git a/cmds/screencap/screencap.cpp b/cmds/screencap/screencap.cpp
index 4410f1c..bb32dd2 100644
--- a/cmds/screencap/screencap.cpp
+++ b/cmds/screencap/screencap.cpp
@@ -105,7 +105,7 @@
     char *cmd[] = {
         (char*) "am",
         (char*) "broadcast",
-        (char*) "am",
+        (char*) "-a",
         (char*) "android.intent.action.MEDIA_SCANNER_SCAN_FILE",
         (char*) "-d",
         &filePath[0],
diff --git a/cmds/statsd/Android.bp b/cmds/statsd/Android.bp
index acd9ec3..f30ed17 100644
--- a/cmds/statsd/Android.bp
+++ b/cmds/statsd/Android.bp
@@ -104,7 +104,7 @@
         "src/subscriber/IncidentdReporter.cpp",
         "src/subscriber/SubscriberReporter.cpp",
         "src/uid_data.proto",
-        "src/utils/NamedLatch.cpp",
+        "src/utils/MultiConditionTrigger.cpp",
     ],
 
     local_include_dirs: [
@@ -295,7 +295,12 @@
 
     //TODO(b/153588990): Remove when the build system properly separates
     //32bit and 64bit architectures.
-    compile_multilib: "prefer32",
+    compile_multilib: "both",
+    multilib: {
+        lib64: {
+            suffix: "64",
+        }
+    },
 
     cflags: [
         "-Wall",
@@ -366,7 +371,7 @@
         "tests/StatsService_test.cpp",
         "tests/storage/StorageManager_test.cpp",
         "tests/UidMap_test.cpp",
-        "tests/utils/NamedLatch_test.cpp",
+        "tests/utils/MultiConditionTrigger_test.cpp",
     ],
 
     static_libs: [
diff --git a/cmds/statsd/src/FieldValue.h b/cmds/statsd/src/FieldValue.h
index e251399..ba4cf11 100644
--- a/cmds/statsd/src/FieldValue.h
+++ b/cmds/statsd/src/FieldValue.h
@@ -382,10 +382,6 @@
 
     inline void setUidField(bool isUid) { setBitmaskAtPos(UID_POS, isUid); }
 
-    inline void setResetState(int32_t resetState) {
-        mResetState = resetState;
-    }
-
     // Default value = false
     inline bool isNested() const { return getValueFromBitmask(NESTED_POS); }
 
@@ -398,12 +394,6 @@
     // Default value = false
     inline bool isUidField() const { return getValueFromBitmask(UID_POS); }
 
-    // If a reset state is not sent in the StatsEvent, returns -1. Note that a
-    // reset satate is only sent if and only if a reset should be triggered.
-    inline int32_t getResetState() const {
-        return mResetState;
-    }
-
 private:
     inline void setBitmaskAtPos(int pos, bool value) {
         mBooleanBitmask &= ~(1 << pos); // clear
@@ -417,8 +407,6 @@
     // This is a bitmask over all annotations stored in boolean form. Because
     // there are only 4 booleans, just one byte is required.
     uint8_t mBooleanBitmask = 0;
-
-    int32_t mResetState = -1;
 };
 
 /**
diff --git a/cmds/statsd/src/StatsLogProcessor.cpp b/cmds/statsd/src/StatsLogProcessor.cpp
index d914ab2..cc48d50 100644
--- a/cmds/statsd/src/StatsLogProcessor.cpp
+++ b/cmds/statsd/src/StatsLogProcessor.cpp
@@ -389,15 +389,24 @@
 void StatsLogProcessor::OnLogEvent(LogEvent* event, int64_t elapsedRealtimeNs) {
     std::lock_guard<std::mutex> lock(mMetricsMutex);
 
+    // Tell StatsdStats about new event
+    const int64_t eventElapsedTimeNs = event->GetElapsedTimestampNs();
+    int atomId = event->GetTagId();
+    StatsdStats::getInstance().noteAtomLogged(atomId, eventElapsedTimeNs / NS_PER_SEC);
+    if (!event->isValid()) {
+        StatsdStats::getInstance().noteAtomError(atomId);
+        return;
+    }
+
     // Hard-coded logic to update train info on disk and fill in any information
     // this log event may be missing.
-    if (event->GetTagId() == android::os::statsd::util::BINARY_PUSH_STATE_CHANGED) {
+    if (atomId == android::os::statsd::util::BINARY_PUSH_STATE_CHANGED) {
         onBinaryPushStateChangedEventLocked(event);
     }
 
     // Hard-coded logic to update experiment ids on disk for certain rollback
     // types and fill the rollback atom with experiment ids
-    if (event->GetTagId() == android::os::statsd::util::WATCHDOG_ROLLBACK_OCCURRED) {
+    if (atomId == android::os::statsd::util::WATCHDOG_ROLLBACK_OCCURRED) {
         onWatchdogRollbackOccurredLocked(event);
     }
 
@@ -406,16 +415,11 @@
         ALOGI("%s", event->ToString().c_str());
     }
 #endif
-    const int64_t eventElapsedTimeNs = event->GetElapsedTimestampNs();
-
     resetIfConfigTtlExpiredLocked(eventElapsedTimeNs);
 
-    StatsdStats::getInstance().noteAtomLogged(
-        event->GetTagId(), event->GetElapsedTimestampNs() / NS_PER_SEC);
-
     // Hard-coded logic to update the isolated uid's in the uid-map.
     // The field numbers need to be currently updated by hand with atoms.proto
-    if (event->GetTagId() == android::os::statsd::util::ISOLATED_UID_CHANGED) {
+    if (atomId == android::os::statsd::util::ISOLATED_UID_CHANGED) {
         onIsolatedUidChangedEventLocked(*event);
     }
 
@@ -432,7 +436,7 @@
     }
 
 
-    if (event->GetTagId() != android::os::statsd::util::ISOLATED_UID_CHANGED) {
+    if (atomId != android::os::statsd::util::ISOLATED_UID_CHANGED) {
         // Map the isolated uid to host uid if necessary.
         mapIsolatedUidToHostUidIfNecessaryLocked(event);
     }
@@ -1051,8 +1055,8 @@
 void StatsLogProcessor::notifyAppUpgrade(const int64_t& eventTimeNs, const string& apk,
                                          const int uid, const int64_t version) {
     std::lock_guard<std::mutex> lock(mMetricsMutex);
-    ALOGW("Received app upgrade");
-    for (auto it : mMetricsManagers) {
+    VLOG("Received app upgrade");
+    for (const auto& it : mMetricsManagers) {
         it.second->notifyAppUpgrade(eventTimeNs, apk, uid, version);
     }
 }
@@ -1060,20 +1064,28 @@
 void StatsLogProcessor::notifyAppRemoved(const int64_t& eventTimeNs, const string& apk,
                                          const int uid) {
     std::lock_guard<std::mutex> lock(mMetricsMutex);
-    ALOGW("Received app removed");
-    for (auto it : mMetricsManagers) {
+    VLOG("Received app removed");
+    for (const auto& it : mMetricsManagers) {
         it.second->notifyAppRemoved(eventTimeNs, apk, uid);
     }
 }
 
 void StatsLogProcessor::onUidMapReceived(const int64_t& eventTimeNs) {
     std::lock_guard<std::mutex> lock(mMetricsMutex);
-    ALOGW("Received uid map");
-    for (auto it : mMetricsManagers) {
+    VLOG("Received uid map");
+    for (const auto& it : mMetricsManagers) {
         it.second->onUidMapReceived(eventTimeNs);
     }
 }
 
+void StatsLogProcessor::onStatsdInitCompleted(const int64_t& elapsedTimeNs) {
+    std::lock_guard<std::mutex> lock(mMetricsMutex);
+    VLOG("Received boot completed signal");
+    for (const auto& it : mMetricsManagers) {
+        it.second->onStatsdInitCompleted(elapsedTimeNs);
+    }
+}
+
 void StatsLogProcessor::noteOnDiskData(const ConfigKey& key) {
     std::lock_guard<std::mutex> lock(mMetricsMutex);
     mOnDiskDataConfigs.insert(key);
diff --git a/cmds/statsd/src/StatsLogProcessor.h b/cmds/statsd/src/StatsLogProcessor.h
index 97512ed..ffd83ba 100644
--- a/cmds/statsd/src/StatsLogProcessor.h
+++ b/cmds/statsd/src/StatsLogProcessor.h
@@ -120,6 +120,11 @@
     /* Notify all MetricsManagers of uid map snapshots received */
     void onUidMapReceived(const int64_t& eventTimeNs) override;
 
+    /* Notify all metrics managers of boot completed
+     * This will force a bucket split when the boot is finished.
+     */
+    void onStatsdInitCompleted(const int64_t& elapsedTimeNs);
+
     // Reset all configs.
     void resetConfigs();
 
diff --git a/cmds/statsd/src/StatsService.cpp b/cmds/statsd/src/StatsService.cpp
index ae7a8d0..bd9f7a5 100644
--- a/cmds/statsd/src/StatsService.cpp
+++ b/cmds/statsd/src/StatsService.cpp
@@ -118,7 +118,8 @@
                   }
               })),
       mEventQueue(queue),
-      mBootCompleteLatch({kBootCompleteTag, kUidMapReceivedTag, kAllPullersRegisteredTag}),
+      mBootCompleteTrigger({kBootCompleteTag, kUidMapReceivedTag, kAllPullersRegisteredTag},
+                           [this]() { mProcessor->onStatsdInitCompleted(getElapsedRealtimeNs()); }),
       mStatsCompanionServiceDeathRecipient(
               AIBinder_DeathRecipient_new(StatsService::statsCompanionServiceDied)) {
     mUidMap = UidMap::getInstance();
@@ -165,12 +166,6 @@
         std::thread pushedEventThread([this] { readLogs(); });
         pushedEventThread.detach();
     }
-
-    std::thread bootCompletedThread([this] {
-        mBootCompleteLatch.wait();
-        VLOG("In the boot completed thread");
-    });
-    bootCompletedThread.detach();
 }
 
 StatsService::~StatsService() {
@@ -946,7 +941,7 @@
                        packageNames,
                        installers);
 
-    mBootCompleteLatch.countDown(kUidMapReceivedTag);
+    mBootCompleteTrigger.markComplete(kUidMapReceivedTag);
     VLOG("StatsService::informAllUidData UidData proto parsed successfully.");
     return Status::ok();
 }
@@ -1066,7 +1061,7 @@
     ENFORCE_UID(AID_SYSTEM);
 
     VLOG("StatsService::bootCompleted was called");
-    mBootCompleteLatch.countDown(kBootCompleteTag);
+    mBootCompleteTrigger.markComplete(kBootCompleteTag);
     return Status::ok();
 }
 
@@ -1222,20 +1217,11 @@
     return Status::ok();
 }
 
-Status StatsService::sendAppBreadcrumbAtom(int32_t label, int32_t state) {
-    // Permission check not necessary as it's meant for applications to write to
-    // statsd.
-    android::os::statsd::util::stats_write(android::os::statsd::util::APP_BREADCRUMB_REPORTED,
-                               (int32_t) AIBinder_getCallingUid(), label,
-                               state);
-    return Status::ok();
-}
-
 Status StatsService::allPullersFromBootRegistered() {
     ENFORCE_UID(AID_SYSTEM);
 
     VLOG("StatsService::allPullersFromBootRegistered was called");
-    mBootCompleteLatch.countDown(kAllPullersRegisteredTag);
+    mBootCompleteTrigger.markComplete(kAllPullersRegisteredTag);
     return Status::ok();
 }
 
diff --git a/cmds/statsd/src/StatsService.h b/cmds/statsd/src/StatsService.h
index 79324d8..b49fa1d 100644
--- a/cmds/statsd/src/StatsService.h
+++ b/cmds/statsd/src/StatsService.h
@@ -33,7 +33,7 @@
 #include "packages/UidMap.h"
 #include "shell/ShellSubscriber.h"
 #include "statscompanion_util.h"
-#include "utils/NamedLatch.h"
+#include "utils/MultiConditionTrigger.h"
 
 using namespace android;
 using namespace android::os;
@@ -162,11 +162,6 @@
     virtual void sayHiToStatsCompanion();
 
     /**
-     * Binder call to get AppBreadcrumbReported atom.
-     */
-    virtual Status sendAppBreadcrumbAtom(int32_t label, int32_t state) override;
-
-    /**
      * Binder call to notify statsd that all pullers from boot have been registered.
      */
     virtual Status allPullersFromBootRegistered();
@@ -386,7 +381,7 @@
     mutable mutex mShellSubscriberMutex;
     std::shared_ptr<LogEventQueue> mEventQueue;
 
-    NamedLatch mBootCompleteLatch;
+    MultiConditionTrigger mBootCompleteTrigger;
     static const inline string kBootCompleteTag = "BOOT_COMPLETE";
     static const inline string kUidMapReceivedTag = "UID_MAP";
     static const inline string kAllPullersRegisteredTag = "PULLERS_REGISTERED";
@@ -399,11 +394,14 @@
     FRIEND_TEST(StatsServiceTest, TestAddConfig_invalid);
     FRIEND_TEST(StatsServiceTest, TestGetUidFromArgs);
     FRIEND_TEST(PartialBucketE2eTest, TestCountMetricNoSplitOnNewApp);
+    FRIEND_TEST(PartialBucketE2eTest, TestCountMetricSplitOnBoot);
     FRIEND_TEST(PartialBucketE2eTest, TestCountMetricSplitOnUpgrade);
     FRIEND_TEST(PartialBucketE2eTest, TestCountMetricSplitOnRemoval);
     FRIEND_TEST(PartialBucketE2eTest, TestCountMetricWithoutSplit);
+    FRIEND_TEST(PartialBucketE2eTest, TestValueMetricOnBootWithoutMinPartialBucket);
     FRIEND_TEST(PartialBucketE2eTest, TestValueMetricWithoutMinPartialBucket);
     FRIEND_TEST(PartialBucketE2eTest, TestValueMetricWithMinPartialBucket);
+    FRIEND_TEST(PartialBucketE2eTest, TestGaugeMetricOnBootWithoutMinPartialBucket);
     FRIEND_TEST(PartialBucketE2eTest, TestGaugeMetricWithoutMinPartialBucket);
     FRIEND_TEST(PartialBucketE2eTest, TestGaugeMetricWithMinPartialBucket);
 };
diff --git a/cmds/statsd/src/external/StatsCallbackPuller.cpp b/cmds/statsd/src/external/StatsCallbackPuller.cpp
index 933f48d..3618bb0 100644
--- a/cmds/statsd/src/external/StatsCallbackPuller.cpp
+++ b/cmds/statsd/src/external/StatsCallbackPuller.cpp
@@ -67,8 +67,14 @@
                     lock_guard<mutex> lk(*cv_mutex);
                     for (const StatsEventParcel& parcel: output) {
                         shared_ptr<LogEvent> event = make_shared<LogEvent>(/*uid=*/-1, /*pid=*/-1);
-                        event->parseBuffer((uint8_t*)parcel.buffer.data(), parcel.buffer.size());
-                        sharedData->push_back(event);
+                        bool valid = event->parseBuffer((uint8_t*)parcel.buffer.data(),
+                                                        parcel.buffer.size());
+                        if (valid) {
+                            sharedData->push_back(event);
+                        } else {
+                            StatsdStats::getInstance().noteAtomError(event->GetTagId(),
+                                                                     /*pull=*/true);
+                        }
                     }
                     *pullSuccess = success;
                     *pullFinish = true;
diff --git a/cmds/statsd/src/external/StatsPullerManager.cpp b/cmds/statsd/src/external/StatsPullerManager.cpp
index ebe9610..cfd5d14 100644
--- a/cmds/statsd/src/external/StatsPullerManager.cpp
+++ b/cmds/statsd/src/external/StatsPullerManager.cpp
@@ -252,9 +252,13 @@
     mPullUidProviders[configKey] = provider;
 }
 
-void StatsPullerManager::UnregisterPullUidProvider(const ConfigKey& configKey) {
+void StatsPullerManager::UnregisterPullUidProvider(const ConfigKey& configKey,
+                                                   wp<PullUidProvider> provider) {
     std::lock_guard<std::mutex> _l(mLock);
-    mPullUidProviders.erase(configKey);
+    const auto& it = mPullUidProviders.find(configKey);
+    if (it != mPullUidProviders.end() && it->second == provider) {
+        mPullUidProviders.erase(it);
+    }
 }
 
 void StatsPullerManager::OnAlarmFired(int64_t elapsedTimeNs) {
diff --git a/cmds/statsd/src/external/StatsPullerManager.h b/cmds/statsd/src/external/StatsPullerManager.h
index ab0ccee..5e18aaa 100644
--- a/cmds/statsd/src/external/StatsPullerManager.h
+++ b/cmds/statsd/src/external/StatsPullerManager.h
@@ -78,11 +78,12 @@
                                     wp<PullDataReceiver> receiver);
 
     // Registers a pull uid provider for the config key. When pulling atoms, it will be used to
-    // determine which atoms to pull from.
+    // determine which uids to pull from.
     virtual void RegisterPullUidProvider(const ConfigKey& configKey, wp<PullUidProvider> provider);
 
     // Unregister a pull uid provider.
-    virtual void UnregisterPullUidProvider(const ConfigKey& configKey);
+    virtual void UnregisterPullUidProvider(const ConfigKey& configKey,
+                                           wp<PullUidProvider> provider);
 
     // Verify if we know how to pull for this matcher
     bool PullerForMatcherExists(int tagId) const;
@@ -180,6 +181,8 @@
     FRIEND_TEST(ValueMetricE2eTest, TestPulledEvents);
     FRIEND_TEST(ValueMetricE2eTest, TestPulledEvents_LateAlarm);
     FRIEND_TEST(ValueMetricE2eTest, TestPulledEvents_WithActivation);
+
+    FRIEND_TEST(StatsLogProcessorTest, TestPullUidProviderSetOnConfigUpdate);
 };
 
 }  // namespace statsd
diff --git a/cmds/statsd/src/guardrail/StatsdStats.cpp b/cmds/statsd/src/guardrail/StatsdStats.cpp
index db637b1..46f5dbd 100644
--- a/cmds/statsd/src/guardrail/StatsdStats.cpp
+++ b/cmds/statsd/src/guardrail/StatsdStats.cpp
@@ -54,6 +54,7 @@
 
 const int FIELD_ID_ATOM_STATS_TAG = 1;
 const int FIELD_ID_ATOM_STATS_COUNT = 2;
+const int FIELD_ID_ATOM_STATS_ERROR_COUNT = 3;
 
 const int FIELD_ID_ANOMALY_ALARMS_REGISTERED = 1;
 const int FIELD_ID_PERIODIC_ALARMS_REGISTERED = 1;
@@ -549,6 +550,20 @@
             std::min(pullStats.minBucketBoundaryDelayNs, timeDelayNs);
 }
 
+void StatsdStats::noteAtomError(int atomTag, bool pull) {
+    lock_guard<std::mutex> lock(mLock);
+    if (pull) {
+        mPulledAtomStats[atomTag].atomErrorCount++;
+        return;
+    }
+
+    bool present = (mPushedAtomErrorStats.find(atomTag) != mPushedAtomErrorStats.end());
+    bool full = (mPushedAtomErrorStats.size() >= (size_t)kMaxPushedAtomErrorStatsSize);
+    if (!full || present) {
+        mPushedAtomErrorStats[atomTag]++;
+    }
+}
+
 StatsdStats::AtomMetricStats& StatsdStats::getAtomMetricStats(int64_t metricId) {
     auto atomMetricStatsIter = mAtomMetricStats.find(metricId);
     if (atomMetricStatsIter != mAtomMetricStats.end()) {
@@ -604,9 +619,11 @@
         pullStats.second.pullExceedMaxDelay = 0;
         pullStats.second.registeredCount = 0;
         pullStats.second.unregisteredCount = 0;
+        pullStats.second.atomErrorCount = 0;
     }
     mAtomMetricStats.clear();
     mActivationBroadcastGuardrailStats.clear();
+    mPushedAtomErrorStats.clear();
 }
 
 string buildTimeString(int64_t timeSec) {
@@ -617,6 +634,15 @@
     return string(timeBuffer);
 }
 
+int StatsdStats::getPushedAtomErrors(int atomId) const {
+    const auto& it = mPushedAtomErrorStats.find(atomId);
+    if (it != mPushedAtomErrorStats.end()) {
+        return it->second;
+    } else {
+        return 0;
+    }
+}
+
 void StatsdStats::dumpStats(int out) const {
     lock_guard<std::mutex> lock(mLock);
     time_t t = mStartTimeSec;
@@ -721,11 +747,13 @@
     const size_t atomCounts = mPushedAtomStats.size();
     for (size_t i = 2; i < atomCounts; i++) {
         if (mPushedAtomStats[i] > 0) {
-            dprintf(out, "Atom %lu->%d\n", (unsigned long)i, mPushedAtomStats[i]);
+            dprintf(out, "Atom %zu->(total count)%d, (error count)%d\n", i, mPushedAtomStats[i],
+                    getPushedAtomErrors((int)i));
         }
     }
     for (const auto& pair : mNonPlatformPushedAtomStats) {
-        dprintf(out, "Atom %lu->%d\n", (unsigned long)pair.first, pair.second);
+        dprintf(out, "Atom %d->(total count)%d, (error count)%d\n", pair.first, pair.second,
+                getPushedAtomErrors(pair.first));
     }
 
     dprintf(out, "********Pulled Atom stats***********\n");
@@ -737,13 +765,15 @@
                 "nanos)%lld, "
                 "  (max pull delay nanos)%lld, (data error)%ld\n"
                 "  (pull timeout)%ld, (pull exceed max delay)%ld\n"
-                "  (registered count) %ld, (unregistered count) %ld\n",
+                "  (registered count) %ld, (unregistered count) %ld\n"
+                "  (atom error count) %d\n",
                 (int)pair.first, (long)pair.second.totalPull, (long)pair.second.totalPullFromCache,
                 (long)pair.second.pullFailed, (long)pair.second.minPullIntervalSec,
                 (long long)pair.second.avgPullTimeNs, (long long)pair.second.maxPullTimeNs,
                 (long long)pair.second.avgPullDelayNs, (long long)pair.second.maxPullDelayNs,
                 pair.second.dataError, pair.second.pullTimeout, pair.second.pullExceedMaxDelay,
-                pair.second.registeredCount, pair.second.unregisteredCount);
+                pair.second.registeredCount, pair.second.unregisteredCount,
+                pair.second.atomErrorCount);
     }
 
     if (mAnomalyAlarmRegisteredStats > 0) {
@@ -919,6 +949,10 @@
                     proto.start(FIELD_TYPE_MESSAGE | FIELD_ID_ATOM_STATS | FIELD_COUNT_REPEATED);
             proto.write(FIELD_TYPE_INT32 | FIELD_ID_ATOM_STATS_TAG, (int32_t)i);
             proto.write(FIELD_TYPE_INT32 | FIELD_ID_ATOM_STATS_COUNT, mPushedAtomStats[i]);
+            int errors = getPushedAtomErrors(i);
+            if (errors > 0) {
+                proto.write(FIELD_TYPE_INT32 | FIELD_ID_ATOM_STATS_ERROR_COUNT, errors);
+            }
             proto.end(token);
         }
     }
@@ -928,6 +962,10 @@
                 proto.start(FIELD_TYPE_MESSAGE | FIELD_ID_ATOM_STATS | FIELD_COUNT_REPEATED);
         proto.write(FIELD_TYPE_INT32 | FIELD_ID_ATOM_STATS_TAG, pair.first);
         proto.write(FIELD_TYPE_INT32 | FIELD_ID_ATOM_STATS_COUNT, pair.second);
+        int errors = getPushedAtomErrors(pair.first);
+        if (errors > 0) {
+            proto.write(FIELD_TYPE_INT32 | FIELD_ID_ATOM_STATS_ERROR_COUNT, errors);
+        }
         proto.end(token);
     }
 
diff --git a/cmds/statsd/src/guardrail/StatsdStats.h b/cmds/statsd/src/guardrail/StatsdStats.h
index ff31e9e..21e524a 100644
--- a/cmds/statsd/src/guardrail/StatsdStats.h
+++ b/cmds/statsd/src/guardrail/StatsdStats.h
@@ -461,6 +461,16 @@
      */
      void noteActivationBroadcastGuardrailHit(const int uid);
 
+     /**
+      * Reports that an atom is erroneous or cannot be parsed successfully by
+      * statsd. An atom tag of 0 indicates that the client did not supply the
+      * atom id within the encoding.
+      *
+      * For pushed atoms only, this call should be preceded by a call to
+      * noteAtomLogged.
+      */
+     void noteAtomError(int atomTag, bool pull=false);
+
     /**
      * Reset the historical stats. Including all stats in icebox, and the tracked stats about
      * metrics, matchers, and atoms. The active configs will be kept and StatsdStats will continue
@@ -499,6 +509,7 @@
         long emptyData = 0;
         long registeredCount = 0;
         long unregisteredCount = 0;
+        int32_t atomErrorCount = 0;
     } PulledAtomStats;
 
     typedef struct {
@@ -546,6 +557,12 @@
     // Maps PullAtomId to its stats. The size is capped by the puller atom counts.
     std::map<int, PulledAtomStats> mPulledAtomStats;
 
+    // Stores the number of times a pushed atom was logged erroneously. The
+    // corresponding counts for pulled atoms are stored in PulledAtomStats.
+    // The max size of this map is kMaxAtomErrorsStatsSize.
+    std::map<int, int> mPushedAtomErrorStats;
+    int kMaxPushedAtomErrorStatsSize = 100;
+
     // Maps metric ID to its stats. The size is capped by the number of metrics.
     std::map<int64_t, AtomMetricStats> mAtomMetricStats;
 
@@ -613,6 +630,8 @@
 
     void addToIceBoxLocked(std::shared_ptr<ConfigStats>& stats);
 
+    int getPushedAtomErrors(int atomId) const;
+
     /**
      * Get a reference to AtomMetricStats for a metric. If none exists, create it. The reference
      * will live as long as `this`.
@@ -631,6 +650,7 @@
     FRIEND_TEST(StatsdStatsTest, TestPullAtomStats);
     FRIEND_TEST(StatsdStatsTest, TestAtomMetricsStats);
     FRIEND_TEST(StatsdStatsTest, TestActivationBroadcastGuardrailHit);
+    FRIEND_TEST(StatsdStatsTest, TestAtomErrorStats);
 };
 
 }  // namespace statsd
diff --git a/cmds/statsd/src/logd/LogEvent.cpp b/cmds/statsd/src/logd/LogEvent.cpp
index 61cd017..eb830e1 100644
--- a/cmds/statsd/src/logd/LogEvent.cpp
+++ b/cmds/statsd/src/logd/LogEvent.cpp
@@ -114,14 +114,6 @@
     mValues.push_back(FieldValue(Field(mTagId, getSimpleField(4)), Value(trainInfo.status)));
 }
 
-LogEvent::~LogEvent() {
-    if (mContext) {
-        // This is for the case when LogEvent is created using the test interface
-        // but init() isn't called.
-        android_log_destroy(&mContext);
-    }
-}
-
 void LogEvent::parseInt32(int32_t* pos, int32_t depth, bool* last, uint8_t numAnnotations) {
     int32_t value = readNextValue<int32_t>();
     addToValues(pos, depth, value, last);
@@ -303,8 +295,7 @@
         return;
     }
 
-    int32_t resetState = readNextValue<int32_t>();
-    mValues[mValues.size() - 1].mAnnotations.setResetState(resetState);
+    mResetState = readNextValue<int32_t>();
 }
 
 void LogEvent::parseStateNestedAnnotation(uint8_t annotationType) {
@@ -386,7 +377,6 @@
         typeInfo = readNextValue<uint8_t>();
         uint8_t typeId = getTypeId(typeInfo);
 
-        // TODO(b/144373276): handle errors passed to the socket
         switch (typeId) {
             case BOOL_TYPE:
                 parseBool(pos, /*depth=*/0, last, getNumAnnotations(typeInfo));
@@ -413,8 +403,13 @@
                 parseAttributionChain(pos, /*depth=*/0, last, getNumAnnotations(typeInfo));
                 if (mAttributionChainIndex == -1) mAttributionChainIndex = pos[0];
                 break;
+            case ERROR_TYPE:
+                mErrorBitmask = readNextValue<int32_t>();
+                mValid = false;
+                break;
             default:
                 mValid = false;
+                break;
         }
     }
 
diff --git a/cmds/statsd/src/logd/LogEvent.h b/cmds/statsd/src/logd/LogEvent.h
index 41fdcc2..dedcfaf 100644
--- a/cmds/statsd/src/logd/LogEvent.h
+++ b/cmds/statsd/src/logd/LogEvent.h
@@ -70,7 +70,7 @@
     explicit LogEvent(int64_t wallClockTimestampNs, int64_t elapsedTimestampNs,
                       const InstallTrainInfo& installTrainInfo);
 
-    ~LogEvent();
+    ~LogEvent() {}
 
     /**
      * Get the timestamp associated with this event.
@@ -184,6 +184,12 @@
         return mExclusiveStateFieldIndex;
     }
 
+    // If a reset state is not sent in the StatsEvent, returns -1. Note that a
+    // reset state is sent if and only if a reset should be triggered.
+    inline int getResetState() const {
+        return mResetState;
+    }
+
     inline LogEvent makeCopy() {
         return LogEvent(*this);
     }
@@ -204,6 +210,14 @@
         return BAD_INDEX;
     }
 
+    bool isValid() const {
+        return mValid;
+    }
+
+    int32_t getErrorBitmask() const {
+        return mErrorBitmask;
+    }
+
 private:
     /**
      * Only use this if copy is absolutely needed.
@@ -230,12 +244,13 @@
     bool checkPreviousValueType(Type expected);
 
     /**
-     * The below three variables are only valid during the execution of
+     * The below two variables are only valid during the execution of
      * parseBuffer. There are no guarantees about the state of these variables
      * before/after.
      */
     uint8_t* mBuf;
     uint32_t mRemainingLen; // number of valid bytes left in the buffer being parsed
+
     bool mValid = true; // stores whether the event we received from the socket is valid
 
     /**
@@ -287,19 +302,15 @@
     // matching.
     std::vector<FieldValue> mValues;
 
-    // This field is used when statsD wants to create log event object and write fields to it. After
-    // calling init() function, this object would be destroyed to save memory usage.
-    // When the log event is created from log msg, this field is never initiated.
-    android_log_context mContext = NULL;
-
     // The timestamp set by the logd.
     int64_t mLogdTimestampNs;
 
     // The elapsed timestamp set by statsd log writer.
     int64_t mElapsedTimestampNs;
 
-    // The atom tag of the event.
-    int mTagId;
+    // The atom tag of the event (defaults to 0 if client does not
+    // appropriately set the atom id).
+    int mTagId = 0;
 
     // The uid of the logging client (defaults to -1).
     int32_t mLogUid = -1;
@@ -307,11 +318,15 @@
     // The pid of the logging client (defaults to -1).
     int32_t mLogPid = -1;
 
+    // Bitmask of errors sent by StatsEvent/AStatsEvent.
+    int32_t mErrorBitmask = 0;
+
     // Annotations
     bool mTruncateTimestamp = false;
     int mUidFieldIndex = -1;
     int mAttributionChainIndex = -1;
     int mExclusiveStateFieldIndex = -1;
+    int mResetState = -1;
 };
 
 void writeExperimentIdsToProto(const std::vector<int64_t>& experimentIds, std::vector<uint8_t>* protoOut);
diff --git a/cmds/statsd/src/main.cpp b/cmds/statsd/src/main.cpp
index e394533..cd9c4e5 100644
--- a/cmds/statsd/src/main.cpp
+++ b/cmds/statsd/src/main.cpp
@@ -37,6 +37,7 @@
 using std::make_shared;
 
 shared_ptr<StatsService> gStatsService = nullptr;
+sp<StatsSocketListener> gSocketListener = nullptr;
 
 void signalHandler(int sig) {
     if (sig == SIGPIPE) {
@@ -47,6 +48,7 @@
         return;
     }
 
+    if (gSocketListener != nullptr) gSocketListener->stopListener();
     if (gStatsService != nullptr) gStatsService->Terminate();
     ALOGW("statsd terminated on receiving signal %d.", sig);
     exit(1);
@@ -92,11 +94,11 @@
 
     gStatsService->Startup();
 
-    sp<StatsSocketListener> socketListener = new StatsSocketListener(eventQueue);
+    gSocketListener = new StatsSocketListener(eventQueue);
 
     ALOGI("Statsd starts to listen to socket.");
     // Backlog and /proc/sys/net/unix/max_dgram_qlen set to large value
-    if (socketListener->startListener(600)) {
+    if (gSocketListener->startListener(600)) {
         exit(1);
     }
 
diff --git a/cmds/statsd/src/metrics/CountMetricProducer.h b/cmds/statsd/src/metrics/CountMetricProducer.h
index a4711e8..f9a8842 100644
--- a/cmds/statsd/src/metrics/CountMetricProducer.h
+++ b/cmds/statsd/src/metrics/CountMetricProducer.h
@@ -109,10 +109,11 @@
     FRIEND_TEST(CountMetricProducerTest, TestEventsWithNonSlicedCondition);
     FRIEND_TEST(CountMetricProducerTest, TestEventsWithSlicedCondition);
     FRIEND_TEST(CountMetricProducerTest, TestAnomalyDetectionUnSliced);
-    FRIEND_TEST(CountMetricProducerTest, TestEventWithAppUpgrade);
-    FRIEND_TEST(CountMetricProducerTest, TestEventWithAppUpgradeInNextBucket);
     FRIEND_TEST(CountMetricProducerTest, TestFirstBucket);
     FRIEND_TEST(CountMetricProducerTest, TestOneWeekTimeUnit);
+
+    FRIEND_TEST(CountMetricProducerTest_PartialBucket, TestSplitInCurrentBucket);
+    FRIEND_TEST(CountMetricProducerTest_PartialBucket, TestSplitInNextBucket);
 };
 
 }  // namespace statsd
diff --git a/cmds/statsd/src/metrics/DurationMetricProducer.h b/cmds/statsd/src/metrics/DurationMetricProducer.h
index cc48f99..6f84076 100644
--- a/cmds/statsd/src/metrics/DurationMetricProducer.h
+++ b/cmds/statsd/src/metrics/DurationMetricProducer.h
@@ -154,12 +154,14 @@
     FRIEND_TEST(DurationMetricTrackerTest, TestNoCondition);
     FRIEND_TEST(DurationMetricTrackerTest, TestNonSlicedCondition);
     FRIEND_TEST(DurationMetricTrackerTest, TestNonSlicedConditionUnknownState);
-    FRIEND_TEST(DurationMetricTrackerTest, TestSumDurationWithUpgrade);
-    FRIEND_TEST(DurationMetricTrackerTest, TestSumDurationWithUpgradeInFollowingBucket);
-    FRIEND_TEST(DurationMetricTrackerTest, TestMaxDurationWithUpgrade);
-    FRIEND_TEST(DurationMetricTrackerTest, TestMaxDurationWithUpgradeInNextBucket);
     FRIEND_TEST(WakelockDurationE2eTest, TestAggregatedPredicates);
     FRIEND_TEST(DurationMetricTrackerTest, TestFirstBucket);
+
+    FRIEND_TEST(DurationMetricProducerTest_PartialBucket, TestSumDuration);
+    FRIEND_TEST(DurationMetricProducerTest_PartialBucket,
+                TestSumDurationWithSplitInFollowingBucket);
+    FRIEND_TEST(DurationMetricProducerTest_PartialBucket, TestMaxDuration);
+    FRIEND_TEST(DurationMetricProducerTest_PartialBucket, TestMaxDurationWithSplitInNextBucket);
 };
 
 }  // namespace statsd
diff --git a/cmds/statsd/src/metrics/GaugeMetricProducer.h b/cmds/statsd/src/metrics/GaugeMetricProducer.h
index aa0cae2..2eb584b 100644
--- a/cmds/statsd/src/metrics/GaugeMetricProducer.h
+++ b/cmds/statsd/src/metrics/GaugeMetricProducer.h
@@ -73,18 +73,23 @@
                       bool pullSuccess, int64_t originalPullTimeNs) override;
 
     // GaugeMetric needs to immediately trigger another pull when we create the partial bucket.
-    void notifyAppUpgrade(const int64_t& eventTimeNs, const string& apk, const int uid,
-                          const int64_t version) override {
+    void notifyAppUpgrade(const int64_t& eventTimeNs) override {
         std::lock_guard<std::mutex> lock(mMutex);
 
         if (!mSplitBucketForAppUpgrade) {
             return;
         }
-        if (eventTimeNs > getCurrentBucketEndTimeNs()) {
-            // Flush full buckets on the normal path up to the latest bucket boundary.
-            flushIfNeededLocked(eventTimeNs);
+        flushLocked(eventTimeNs);
+        if (mIsPulled && mSamplingType == GaugeMetric::RANDOM_ONE_SAMPLE) {
+            pullAndMatchEventsLocked(eventTimeNs);
         }
-        flushCurrentBucketLocked(eventTimeNs, eventTimeNs);
+    };
+
+    // GaugeMetric needs to immediately trigger another pull when we create the partial bucket.
+    void onStatsdInitCompleted(const int64_t& eventTimeNs) override {
+        std::lock_guard<std::mutex> lock(mMutex);
+
+        flushLocked(eventTimeNs);
         if (mIsPulled && mSamplingType == GaugeMetric::RANDOM_ONE_SAMPLE) {
             pullAndMatchEventsLocked(eventTimeNs);
         }
@@ -190,13 +195,14 @@
     FRIEND_TEST(GaugeMetricProducerTest, TestPulledEventsWithCondition);
     FRIEND_TEST(GaugeMetricProducerTest, TestPulledEventsWithSlicedCondition);
     FRIEND_TEST(GaugeMetricProducerTest, TestPulledEventsNoCondition);
-    FRIEND_TEST(GaugeMetricProducerTest, TestPushedEventsWithUpgrade);
-    FRIEND_TEST(GaugeMetricProducerTest, TestPulledWithUpgrade);
     FRIEND_TEST(GaugeMetricProducerTest, TestPulledWithAppUpgradeDisabled);
     FRIEND_TEST(GaugeMetricProducerTest, TestPulledEventsAnomalyDetection);
     FRIEND_TEST(GaugeMetricProducerTest, TestFirstBucket);
     FRIEND_TEST(GaugeMetricProducerTest, TestPullOnTrigger);
     FRIEND_TEST(GaugeMetricProducerTest, TestRemoveDimensionInOutput);
+
+    FRIEND_TEST(GaugeMetricProducerTest_PartialBucket, TestPushedEvents);
+    FRIEND_TEST(GaugeMetricProducerTest_PartialBucket, TestPulled);
 };
 
 }  // namespace statsd
diff --git a/cmds/statsd/src/metrics/MetricProducer.h b/cmds/statsd/src/metrics/MetricProducer.h
index 6aba13ca..91c98ea2 100644
--- a/cmds/statsd/src/metrics/MetricProducer.h
+++ b/cmds/statsd/src/metrics/MetricProducer.h
@@ -141,30 +141,25 @@
     }
 
     /**
-     * Forces this metric to split into a partial bucket right now. If we're past a full bucket, we
-     * first call the standard flushing code to flush up to the latest full bucket. Then we call
-     * the flush again when the end timestamp is forced to be now, and then after flushing, update
-     * the start timestamp to be now.
+     * Force a partial bucket split on app upgrade
      */
-    virtual void notifyAppUpgrade(const int64_t& eventTimeNs, const string& apk, const int uid,
-                          const int64_t version) {
+    virtual void notifyAppUpgrade(const int64_t& eventTimeNs) {
         std::lock_guard<std::mutex> lock(mMutex);
-
-        if (eventTimeNs > getCurrentBucketEndTimeNs()) {
-            // Flush full buckets on the normal path up to the latest bucket boundary.
-            flushIfNeededLocked(eventTimeNs);
-        }
-        // Now flush a partial bucket.
-        flushCurrentBucketLocked(eventTimeNs, eventTimeNs);
-        // Don't update the current bucket number so that the anomaly tracker knows this bucket
-        // is a partial bucket and can merge it with the previous bucket.
+        flushLocked(eventTimeNs);
     };
 
-    void notifyAppRemoved(const int64_t& eventTimeNs, const string& apk, const int uid) {
+    void notifyAppRemoved(const int64_t& eventTimeNs) {
         // Force buckets to split on removal also.
-        notifyAppUpgrade(eventTimeNs, apk, uid, 0);
+        notifyAppUpgrade(eventTimeNs);
     };
 
+    /**
+     * Force a partial bucket split on boot complete.
+     */
+    virtual void onStatsdInitCompleted(const int64_t& eventTimeNs) {
+        std::lock_guard<std::mutex> lock(mMutex);
+        flushLocked(eventTimeNs);
+    }
     // Consume the parsed stats log entry that already matched the "what" of the metric.
     void onMatchedLogEvent(const size_t matcherIndex, const LogEvent& event) {
         std::lock_guard<std::mutex> lock(mMutex);
@@ -292,8 +287,7 @@
     // End: getters/setters
 protected:
     /**
-     * Flushes the current bucket if the eventTime is after the current bucket's end time. This will
-       also flush the current partial bucket in memory.
+     * Flushes the current bucket if the eventTime is after the current bucket's end time.
      */
     virtual void flushIfNeededLocked(const int64_t& eventTime){};
 
diff --git a/cmds/statsd/src/metrics/MetricsManager.cpp b/cmds/statsd/src/metrics/MetricsManager.cpp
index d832ed8..e03de0b 100644
--- a/cmds/statsd/src/metrics/MetricsManager.cpp
+++ b/cmds/statsd/src/metrics/MetricsManager.cpp
@@ -189,7 +189,7 @@
             StateManager::getInstance().unregisterListener(atomId, it);
         }
     }
-    mPullerManager->UnregisterPullUidProvider(mConfigKey);
+    mPullerManager->UnregisterPullUidProvider(mConfigKey, this);
 
     VLOG("~MetricsManager()");
 }
@@ -231,8 +231,8 @@
 void MetricsManager::notifyAppUpgrade(const int64_t& eventTimeNs, const string& apk, const int uid,
                                       const int64_t version) {
     // Inform all metric producers.
-    for (auto it : mAllMetricProducers) {
-        it->notifyAppUpgrade(eventTimeNs, apk, uid, version);
+    for (const auto& it : mAllMetricProducers) {
+        it->notifyAppUpgrade(eventTimeNs);
     }
     // check if we care this package
     if (std::find(mAllowedPkg.begin(), mAllowedPkg.end(), apk) != mAllowedPkg.end()) {
@@ -252,8 +252,8 @@
 void MetricsManager::notifyAppRemoved(const int64_t& eventTimeNs, const string& apk,
                                       const int uid) {
     // Inform all metric producers.
-    for (auto it : mAllMetricProducers) {
-        it->notifyAppRemoved(eventTimeNs, apk, uid);
+    for (const auto& it : mAllMetricProducers) {
+        it->notifyAppRemoved(eventTimeNs);
     }
     // check if we care this package
     if (std::find(mAllowedPkg.begin(), mAllowedPkg.end(), apk) != mAllowedPkg.end()) {
@@ -282,6 +282,13 @@
     initLogSourceWhiteList();
 }
 
+void MetricsManager::onStatsdInitCompleted(const int64_t& eventTimeNs) {
+    // Inform all metric producers.
+    for (const auto& it : mAllMetricProducers) {
+        it->onStatsdInitCompleted(eventTimeNs);
+    }
+}
+
 void MetricsManager::init() {
     for (const auto& producer : mAllMetricProducers) {
         producer->prepareFirstBucket();
diff --git a/cmds/statsd/src/metrics/MetricsManager.h b/cmds/statsd/src/metrics/MetricsManager.h
index 1fd6572..ef03d20 100644
--- a/cmds/statsd/src/metrics/MetricsManager.h
+++ b/cmds/statsd/src/metrics/MetricsManager.h
@@ -70,6 +70,8 @@
 
     void onUidMapReceived(const int64_t& eventTimeNs);
 
+    void onStatsdInitCompleted(const int64_t& elapsedTimeNs);
+
     void init();
 
     vector<int32_t> getPullAtomUids(int32_t atomId) override;
diff --git a/cmds/statsd/src/metrics/ValueMetricProducer.h b/cmds/statsd/src/metrics/ValueMetricProducer.h
index e9273dc..c8dc8cc 100644
--- a/cmds/statsd/src/metrics/ValueMetricProducer.h
+++ b/cmds/statsd/src/metrics/ValueMetricProducer.h
@@ -69,8 +69,7 @@
                       bool pullSuccess, int64_t originalPullTimeNs) override;
 
     // ValueMetric needs special logic if it's a pulled atom.
-    void notifyAppUpgrade(const int64_t& eventTimeNs, const string& apk, const int uid,
-                          const int64_t version) override {
+    void notifyAppUpgrade(const int64_t& eventTimeNs) override {
         std::lock_guard<std::mutex> lock(mMutex);
         if (!mSplitBucketForAppUpgrade) {
             return;
@@ -81,6 +80,15 @@
         flushCurrentBucketLocked(eventTimeNs, eventTimeNs);
     };
 
+    // ValueMetric needs special logic if it's a pulled atom.
+    void onStatsdInitCompleted(const int64_t& eventTimeNs) override {
+        std::lock_guard<std::mutex> lock(mMutex);
+        if (mIsPulled && mCondition) {
+            pullAndMatchEventsLocked(eventTimeNs);
+        }
+        flushCurrentBucketLocked(eventTimeNs, eventTimeNs);
+    };
+
     void onStateChanged(int64_t eventTimeNs, int32_t atomId, const HashableDimensionKey& primaryKey,
                         int oldState, int newState) override;
 
@@ -256,7 +264,6 @@
 
     FRIEND_TEST(ValueMetricProducerTest, TestAnomalyDetection);
     FRIEND_TEST(ValueMetricProducerTest, TestBaseSetOnConditionChange);
-    FRIEND_TEST(ValueMetricProducerTest, TestBucketBoundariesOnAppUpgrade);
     FRIEND_TEST(ValueMetricProducerTest, TestBucketBoundariesOnConditionChange);
     FRIEND_TEST(ValueMetricProducerTest, TestBucketBoundaryNoCondition);
     FRIEND_TEST(ValueMetricProducerTest, TestBucketBoundaryWithCondition);
@@ -269,10 +276,8 @@
     FRIEND_TEST(ValueMetricProducerTest, TestEmptyDataResetsBase_onDataPulled);
     FRIEND_TEST(ValueMetricProducerTest, TestEventsWithNonSlicedCondition);
     FRIEND_TEST(ValueMetricProducerTest, TestFirstBucket);
-    FRIEND_TEST(ValueMetricProducerTest, TestFullBucketResetWhenLastBucketInvalid);
     FRIEND_TEST(ValueMetricProducerTest, TestLateOnDataPulledWithDiff);
     FRIEND_TEST(ValueMetricProducerTest, TestLateOnDataPulledWithoutDiff);
-    FRIEND_TEST(ValueMetricProducerTest, TestPartialBucketCreated);
     FRIEND_TEST(ValueMetricProducerTest, TestPartialResetOnBucketBoundaries);
     FRIEND_TEST(ValueMetricProducerTest, TestPulledData_noDiff_bucketBoundaryFalse);
     FRIEND_TEST(ValueMetricProducerTest, TestPulledData_noDiff_bucketBoundaryTrue);
@@ -283,15 +288,12 @@
     FRIEND_TEST(ValueMetricProducerTest, TestPulledEventsTakeAbsoluteValueOnReset);
     FRIEND_TEST(ValueMetricProducerTest, TestPulledEventsTakeZeroOnReset);
     FRIEND_TEST(ValueMetricProducerTest, TestPulledEventsWithFiltering);
-    FRIEND_TEST(ValueMetricProducerTest, TestPulledValueWithUpgrade);
-    FRIEND_TEST(ValueMetricProducerTest, TestPulledValueWithUpgradeWhileConditionFalse);
     FRIEND_TEST(ValueMetricProducerTest, TestPulledWithAppUpgradeDisabled);
     FRIEND_TEST(ValueMetricProducerTest, TestPushedAggregateAvg);
     FRIEND_TEST(ValueMetricProducerTest, TestPushedAggregateMax);
     FRIEND_TEST(ValueMetricProducerTest, TestPushedAggregateMin);
     FRIEND_TEST(ValueMetricProducerTest, TestPushedAggregateSum);
     FRIEND_TEST(ValueMetricProducerTest, TestPushedEventsWithCondition);
-    FRIEND_TEST(ValueMetricProducerTest, TestPushedEventsWithUpgrade);
     FRIEND_TEST(ValueMetricProducerTest, TestPushedEventsWithoutCondition);
     FRIEND_TEST(ValueMetricProducerTest, TestResetBaseOnPullDelayExceeded);
     FRIEND_TEST(ValueMetricProducerTest, TestResetBaseOnPullFailAfterConditionChange);
@@ -313,6 +315,14 @@
     FRIEND_TEST(ValueMetricProducerTest_BucketDrop, TestInvalidBucketWhenGuardRailHit);
     FRIEND_TEST(ValueMetricProducerTest_BucketDrop,
                 TestInvalidBucketWhenAccumulateEventWrongBucket);
+
+    FRIEND_TEST(ValueMetricProducerTest_PartialBucket, TestBucketBoundariesOnPartialBucket);
+    FRIEND_TEST(ValueMetricProducerTest_PartialBucket, TestFullBucketResetWhenLastBucketInvalid);
+    FRIEND_TEST(ValueMetricProducerTest_PartialBucket, TestPartialBucketCreated);
+    FRIEND_TEST(ValueMetricProducerTest_PartialBucket, TestPushedEvents);
+    FRIEND_TEST(ValueMetricProducerTest_PartialBucket, TestPulledValue);
+    FRIEND_TEST(ValueMetricProducerTest_PartialBucket, TestPulledValueWhileConditionFalse);
+
     friend class ValueMetricProducerTestHelper;
 };
 
diff --git a/cmds/statsd/src/state/StateTracker.cpp b/cmds/statsd/src/state/StateTracker.cpp
index b7f314a..b63713b 100644
--- a/cmds/statsd/src/state/StateTracker.cpp
+++ b/cmds/statsd/src/state/StateTracker.cpp
@@ -51,7 +51,7 @@
         return;
     }
 
-    const int32_t resetState = stateValue.mAnnotations.getResetState();
+    const int32_t resetState = event.getResetState();
     if (resetState != -1) {
         VLOG("StateTracker new reset state: %d", resetState);
         handleReset(eventTimeNs, resetState);
diff --git a/cmds/statsd/src/stats_log.proto b/cmds/statsd/src/stats_log.proto
index f4247ec..868247b 100644
--- a/cmds/statsd/src/stats_log.proto
+++ b/cmds/statsd/src/stats_log.proto
@@ -425,6 +425,7 @@
     message AtomStats {
         optional int32 tag = 1;
         optional int32 count = 2;
+        optional int32 error_count = 3;
     }
 
     repeated AtomStats atom_stats = 7;
@@ -460,6 +461,7 @@
         optional int64 empty_data = 15;
         optional int64 registered_count = 16;
         optional int64 unregistered_count = 17;
+        optional int32 atom_error_count = 18;
     }
     repeated PulledAtomStats pulled_atom_stats = 10;
 
diff --git a/cmds/statsd/src/stats_log_util.cpp b/cmds/statsd/src/stats_log_util.cpp
index 5635313..f9fddc8 100644
--- a/cmds/statsd/src/stats_log_util.cpp
+++ b/cmds/statsd/src/stats_log_util.cpp
@@ -80,6 +80,8 @@
 const int FIELD_ID_EMPTY_DATA = 15;
 const int FIELD_ID_PULL_REGISTERED_COUNT = 16;
 const int FIELD_ID_PULL_UNREGISTERED_COUNT = 17;
+const int FIELD_ID_ATOM_ERROR_COUNT = 18;
+
 // for AtomMetricStats proto
 const int FIELD_ID_ATOM_METRIC_STATS = 17;
 const int FIELD_ID_METRIC_ID = 1;
@@ -492,6 +494,7 @@
                        (long long) pair.second.registeredCount);
     protoOutput->write(FIELD_TYPE_INT64 | FIELD_ID_PULL_UNREGISTERED_COUNT,
                        (long long) pair.second.unregisteredCount);
+    protoOutput->write(FIELD_TYPE_INT32 | FIELD_ID_ATOM_ERROR_COUNT, pair.second.atomErrorCount);
     protoOutput->end(token);
 }
 
diff --git a/cmds/statsd/src/utils/MultiConditionTrigger.cpp b/cmds/statsd/src/utils/MultiConditionTrigger.cpp
new file mode 100644
index 0000000..43a6933
--- /dev/null
+++ b/cmds/statsd/src/utils/MultiConditionTrigger.cpp
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+#define DEBUG false  // STOPSHIP if true
+
+#include "MultiConditionTrigger.h"
+
+#include <thread>
+
+using namespace std;
+
+namespace android {
+namespace os {
+namespace statsd {
+
+MultiConditionTrigger::MultiConditionTrigger(const set<string>& conditionNames,
+                                             function<void()> trigger)
+    : mRemainingConditionNames(conditionNames),
+      mTrigger(trigger),
+      mCompleted(mRemainingConditionNames.empty()) {
+    if (mCompleted) {
+        thread executorThread([this] { mTrigger(); });
+        executorThread.detach();
+    }
+}
+
+void MultiConditionTrigger::markComplete(const string& conditionName) {
+    bool doTrigger = false;
+    {
+        lock_guard<mutex> lg(mMutex);
+        if (mCompleted) {
+            return;
+        }
+        mRemainingConditionNames.erase(conditionName);
+        mCompleted = mRemainingConditionNames.empty();
+        doTrigger = mCompleted;
+    }
+    if (doTrigger) {
+        std::thread executorThread([this] { mTrigger(); });
+        executorThread.detach();
+    }
+}
+}  // namespace statsd
+}  // namespace os
+}  // namespace android
diff --git a/cmds/statsd/src/utils/MultiConditionTrigger.h b/cmds/statsd/src/utils/MultiConditionTrigger.h
new file mode 100644
index 0000000..51f6029
--- /dev/null
+++ b/cmds/statsd/src/utils/MultiConditionTrigger.h
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+#pragma once
+
+#include <gtest/gtest_prod.h>
+
+#include <mutex>
+#include <set>
+
+namespace android {
+namespace os {
+namespace statsd {
+
+/**
+ * This class provides a utility to wait for a set of named conditions to occur.
+ *
+ * It will execute the trigger runnable in a detached thread once all conditions have been marked
+ * true.
+ */
+class MultiConditionTrigger {
+public:
+    explicit MultiConditionTrigger(const std::set<std::string>& conditionNames,
+                                   std::function<void()> trigger);
+
+    MultiConditionTrigger(const MultiConditionTrigger&) = delete;
+    MultiConditionTrigger& operator=(const MultiConditionTrigger&) = delete;
+
+    // Mark a specific condition as true. If this condition has called markComplete already or if
+    // the event was not specified in the constructor, the function is a no-op.
+    void markComplete(const std::string& eventName);
+
+private:
+    mutable std::mutex mMutex;
+    std::set<std::string> mRemainingConditionNames;
+    std::function<void()> mTrigger;
+    bool mCompleted;
+
+    FRIEND_TEST(MultiConditionTriggerTest, TestCountDownCalledBySameEventName);
+};
+}  // namespace statsd
+}  // namespace os
+}  // namespace android
diff --git a/cmds/statsd/src/utils/NamedLatch.cpp b/cmds/statsd/src/utils/NamedLatch.cpp
deleted file mode 100644
index 6e77977..0000000
--- a/cmds/statsd/src/utils/NamedLatch.cpp
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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.
- */
-#define DEBUG false  // STOPSHIP if true
-
-#include "NamedLatch.h"
-
-using namespace std;
-
-namespace android {
-namespace os {
-namespace statsd {
-
-NamedLatch::NamedLatch(const set<string>& eventNames) : mRemainingEventNames(eventNames) {
-}
-
-void NamedLatch::countDown(const string& eventName) {
-    bool notify = false;
-    {
-        lock_guard<mutex> lg(mMutex);
-        mRemainingEventNames.erase(eventName);
-        notify = mRemainingEventNames.empty();
-    }
-    if (notify) {
-        mConditionVariable.notify_all();
-    }
-}
-
-void NamedLatch::wait() const {
-    unique_lock<mutex> unique_lk(mMutex);
-    mConditionVariable.wait(unique_lk, [this] { return mRemainingEventNames.empty(); });
-}
-
-}  // namespace statsd
-}  // namespace os
-}  // namespace android
diff --git a/cmds/statsd/src/utils/NamedLatch.h b/cmds/statsd/src/utils/NamedLatch.h
deleted file mode 100644
index 70238370..0000000
--- a/cmds/statsd/src/utils/NamedLatch.h
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * 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.
- */
-#pragma once
-
-#include <gtest/gtest_prod.h>
-
-#include <condition_variable>
-#include <mutex>
-#include <set>
-
-namespace android {
-namespace os {
-namespace statsd {
-
-/**
- * This class provides a threading primitive similar to a latch.
- * The primary difference is that it waits for named events to occur instead of waiting for
- * N threads to reach a certain point.
- *
- * It uses a condition variable under the hood.
- */
-class NamedLatch {
-public:
-    explicit NamedLatch(const std::set<std::string>& eventNames);
-
-    NamedLatch(const NamedLatch&) = delete;
-    NamedLatch& operator=(const NamedLatch&) = delete;
-
-    // Mark a specific event as completed. If this event has called countDown already or if the
-    // event was not specified in the constructor, the function is a no-op.
-    void countDown(const std::string& eventName);
-
-    // Blocks the calling thread until all events in eventNames have called countDown.
-    void wait() const;
-
-private:
-    mutable std::mutex mMutex;
-    mutable std::condition_variable mConditionVariable;
-    std::set<std::string> mRemainingEventNames;
-
-    FRIEND_TEST(NamedLatchTest, TestCountDownCalledBySameEventName);
-};
-}  // namespace statsd
-}  // namespace os
-}  // namespace android
diff --git a/cmds/statsd/tests/LogEvent_test.cpp b/cmds/statsd/tests/LogEvent_test.cpp
index bb4578d..e52e2d0 100644
--- a/cmds/statsd/tests/LogEvent_test.cpp
+++ b/cmds/statsd/tests/LogEvent_test.cpp
@@ -352,7 +352,7 @@
 
     const vector<FieldValue>& values = event.getValues();
     EXPECT_EQ(values.size(), 1);
-    EXPECT_EQ(values[0].mAnnotations.getResetState(), resetState);
+    EXPECT_EQ(event.getResetState(), resetState);
 }
 
 }  // namespace statsd
diff --git a/cmds/statsd/tests/MetricsManager_test.cpp b/cmds/statsd/tests/MetricsManager_test.cpp
index 1075fe4..a44541d 100644
--- a/cmds/statsd/tests/MetricsManager_test.cpp
+++ b/cmds/statsd/tests/MetricsManager_test.cpp
@@ -528,7 +528,7 @@
             }));
     sp<MockStatsPullerManager> pullerManager = new StrictMock<MockStatsPullerManager>();
     EXPECT_CALL(*pullerManager, RegisterPullUidProvider(kConfigKey, _)).Times(1);
-    EXPECT_CALL(*pullerManager, UnregisterPullUidProvider(kConfigKey)).Times(1);
+    EXPECT_CALL(*pullerManager, UnregisterPullUidProvider(kConfigKey, _)).Times(1);
 
     sp<AlarmMonitor> anomalyAlarmMonitor;
     sp<AlarmMonitor> periodicAlarmMonitor;
diff --git a/cmds/statsd/tests/StatsLogProcessor_test.cpp b/cmds/statsd/tests/StatsLogProcessor_test.cpp
index d29394b..b809286 100644
--- a/cmds/statsd/tests/StatsLogProcessor_test.cpp
+++ b/cmds/statsd/tests/StatsLogProcessor_test.cpp
@@ -301,6 +301,29 @@
     EXPECT_TRUE(noData);
 }
 
+TEST(StatsLogProcessorTest, TestPullUidProviderSetOnConfigUpdate) {
+    // Setup simple config key corresponding to empty config.
+    sp<UidMap> m = new UidMap();
+    sp<StatsPullerManager> pullerManager = new StatsPullerManager();
+    sp<AlarmMonitor> anomalyAlarmMonitor;
+    sp<AlarmMonitor> subscriberAlarmMonitor;
+    StatsLogProcessor p(
+            m, pullerManager, anomalyAlarmMonitor, subscriberAlarmMonitor, 0,
+            [](const ConfigKey& key) { return true; },
+            [](const int&, const vector<int64_t>&) { return true; });
+    ConfigKey key(3, 4);
+    StatsdConfig config = MakeConfig(false);
+    p.OnConfigUpdated(0, key, config);
+    EXPECT_NE(pullerManager->mPullUidProviders.find(key), pullerManager->mPullUidProviders.end());
+
+    config.add_default_pull_packages("AID_STATSD");
+    p.OnConfigUpdated(5, key, config);
+    EXPECT_NE(pullerManager->mPullUidProviders.find(key), pullerManager->mPullUidProviders.end());
+
+    p.OnConfigRemoved(key);
+    EXPECT_EQ(pullerManager->mPullUidProviders.find(key), pullerManager->mPullUidProviders.end());
+}
+
 TEST(StatsLogProcessorTest, TestActiveConfigMetricDiskWriteRead) {
     int uid = 1111;
 
diff --git a/cmds/statsd/tests/e2e/PartialBucket_e2e_test.cpp b/cmds/statsd/tests/e2e/PartialBucket_e2e_test.cpp
index b173ee0..9117623 100644
--- a/cmds/statsd/tests/e2e/PartialBucket_e2e_test.cpp
+++ b/cmds/statsd/tests/e2e/PartialBucket_e2e_test.cpp
@@ -89,6 +89,7 @@
     valueMetric->set_bucket(FIVE_MINUTES);
     valueMetric->set_min_bucket_size_nanos(minTime);
     valueMetric->set_use_absolute_value_on_reset(true);
+    valueMetric->set_skip_zero_diff_output(false);
     return config;
 }
 
@@ -217,6 +218,35 @@
     EXPECT_EQ(1, report.metrics(0).count_metrics().data(0).bucket_info(0).count());
 }
 
+TEST(PartialBucketE2eTest, TestCountMetricSplitOnBoot) {
+    shared_ptr<StatsService> service = SharedRefBase::make<StatsService>(nullptr, nullptr);
+    SendConfig(service, MakeConfig());
+    int64_t start = getElapsedRealtimeNs();  // This is the start-time the metrics producers are
+                                             // initialized with.
+
+    // Goes into the first bucket
+    service->mProcessor->OnLogEvent(CreateAppCrashEvent(start + NS_PER_SEC, 100).get());
+    int64_t bootCompleteTimeNs = start + 2 * NS_PER_SEC;
+    service->mProcessor->onStatsdInitCompleted(bootCompleteTimeNs);
+    // Goes into the second bucket.
+    service->mProcessor->OnLogEvent(CreateAppCrashEvent(start + 3 * NS_PER_SEC, 100).get());
+
+    ConfigMetricsReport report = GetReports(service->mProcessor, start + 4 * NS_PER_SEC);
+    backfillStartEndTimestamp(&report);
+
+    ASSERT_EQ(1, report.metrics_size());
+    ASSERT_EQ(1, report.metrics(0).count_metrics().data_size());
+    ASSERT_EQ(1, report.metrics(0).count_metrics().data(0).bucket_info_size());
+    EXPECT_TRUE(report.metrics(0)
+                        .count_metrics()
+                        .data(0)
+                        .bucket_info(0)
+                        .has_start_bucket_elapsed_nanos());
+    EXPECT_EQ(MillisToNano(NanoToMillis(bootCompleteTimeNs)),
+              report.metrics(0).count_metrics().data(0).bucket_info(0).end_bucket_elapsed_nanos());
+    EXPECT_EQ(1, report.metrics(0).count_metrics().data(0).bucket_info(0).count());
+}
+
 TEST(PartialBucketE2eTest, TestValueMetricWithoutMinPartialBucket) {
     shared_ptr<StatsService> service = SharedRefBase::make<StatsService>(nullptr, nullptr);
     service->mPullerManager->RegisterPullAtomCallback(
@@ -229,13 +259,22 @@
                                              // initialized with.
 
     service->mProcessor->informPullAlarmFired(5 * 60 * NS_PER_SEC + start);
-    service->mUidMap->updateApp(5 * 60 * NS_PER_SEC + start + 2, String16(kApp1.c_str()), 1, 2,
-                                String16("v2"), String16(""));
+    int64_t appUpgradeTimeNs = 5 * 60 * NS_PER_SEC + start + 2 * NS_PER_SEC;
+    service->mUidMap->updateApp(appUpgradeTimeNs, String16(kApp1.c_str()), 1, 2, String16("v2"),
+                                String16(""));
 
     ConfigMetricsReport report =
-            GetReports(service->mProcessor, 5 * 60 * NS_PER_SEC + start + 100, true);
+            GetReports(service->mProcessor, 5 * 60 * NS_PER_SEC + start + 100 * NS_PER_SEC);
+    backfillStartEndTimestamp(&report);
+
     EXPECT_EQ(1, report.metrics_size());
     EXPECT_EQ(0, report.metrics(0).value_metrics().skipped_size());
+
+    // The fake subsystem state sleep puller returns two atoms.
+    ASSERT_EQ(2, report.metrics(0).value_metrics().data_size());
+    ASSERT_EQ(2, report.metrics(0).value_metrics().data(0).bucket_info_size());
+    EXPECT_EQ(MillisToNano(NanoToMillis(appUpgradeTimeNs)),
+              report.metrics(0).value_metrics().data(0).bucket_info(1).end_bucket_elapsed_nanos());
 }
 
 TEST(PartialBucketE2eTest, TestValueMetricWithMinPartialBucket) {
@@ -249,13 +288,13 @@
     int64_t start = getElapsedRealtimeNs();  // This is the start-time the metrics producers are
                                              // initialized with.
 
-    const int64_t endSkipped = 5 * 60 * NS_PER_SEC + start + 2;
+    const int64_t endSkipped = 5 * 60 * NS_PER_SEC + start + 2 * NS_PER_SEC;
     service->mProcessor->informPullAlarmFired(5 * 60 * NS_PER_SEC + start);
     service->mUidMap->updateApp(endSkipped, String16(kApp1.c_str()), 1, 2, String16("v2"),
                                String16(""));
 
     ConfigMetricsReport report =
-            GetReports(service->mProcessor, 5 * 60 * NS_PER_SEC + start + 100 * NS_PER_SEC, true);
+            GetReports(service->mProcessor, 5 * 60 * NS_PER_SEC + start + 100 * NS_PER_SEC);
     backfillStartEndTimestamp(&report);
 
     ASSERT_EQ(1, report.metrics_size());
@@ -264,10 +303,49 @@
     // Can't test the start time since it will be based on the actual time when the pulling occurs.
     EXPECT_EQ(MillisToNano(NanoToMillis(endSkipped)),
               report.metrics(0).value_metrics().skipped(0).end_bucket_elapsed_nanos());
+
+    ASSERT_EQ(2, report.metrics(0).value_metrics().data_size());
+    EXPECT_EQ(1, report.metrics(0).value_metrics().data(0).bucket_info_size());
+}
+
+TEST(PartialBucketE2eTest, TestValueMetricOnBootWithoutMinPartialBucket) {
+    shared_ptr<StatsService> service = SharedRefBase::make<StatsService>(nullptr, nullptr);
+    // Initial pull will fail since puller is not registered.
+    SendConfig(service, MakeValueMetricConfig(0));
+    int64_t start = getElapsedRealtimeNs();  // This is the start-time the metrics producers are
+                                             // initialized with.
+
+    service->mPullerManager->RegisterPullAtomCallback(
+            /*uid=*/0, util::SUBSYSTEM_SLEEP_STATE, NS_PER_SEC, NS_PER_SEC * 10, {},
+            SharedRefBase::make<FakeSubsystemSleepCallback>());
+
+    int64_t bootCompleteTimeNs = start + NS_PER_SEC;
+    service->mProcessor->onStatsdInitCompleted(bootCompleteTimeNs);
+
+    service->mProcessor->informPullAlarmFired(5 * 60 * NS_PER_SEC + start);
+
+    ConfigMetricsReport report = GetReports(service->mProcessor, 5 * 60 * NS_PER_SEC + start + 100);
+    backfillStartEndTimestamp(&report);
+
+    // First bucket is dropped due to the initial pull failing
+    ASSERT_EQ(1, report.metrics_size());
+    EXPECT_EQ(1, report.metrics(0).value_metrics().skipped_size());
+    EXPECT_EQ(MillisToNano(NanoToMillis(bootCompleteTimeNs)),
+              report.metrics(0).value_metrics().skipped(0).end_bucket_elapsed_nanos());
+
+    // The fake subsystem state sleep puller returns two atoms.
+    ASSERT_EQ(2, report.metrics(0).value_metrics().data_size());
+    ASSERT_EQ(1, report.metrics(0).value_metrics().data(0).bucket_info_size());
+    EXPECT_EQ(
+            MillisToNano(NanoToMillis(bootCompleteTimeNs)),
+            report.metrics(0).value_metrics().data(0).bucket_info(0).start_bucket_elapsed_nanos());
 }
 
 TEST(PartialBucketE2eTest, TestGaugeMetricWithoutMinPartialBucket) {
     shared_ptr<StatsService> service = SharedRefBase::make<StatsService>(nullptr, nullptr);
+    service->mPullerManager->RegisterPullAtomCallback(
+            /*uid=*/0, util::SUBSYSTEM_SLEEP_STATE, NS_PER_SEC, NS_PER_SEC * 10, {},
+            SharedRefBase::make<FakeSubsystemSleepCallback>());
     // Partial buckets don't occur when app is first installed.
     service->mUidMap->updateApp(1, String16(kApp1.c_str()), 1, 1, String16("v1"), String16(""));
     SendConfig(service, MakeGaugeMetricConfig(0));
@@ -278,16 +356,22 @@
     service->mUidMap->updateApp(5 * 60 * NS_PER_SEC + start + 2, String16(kApp1.c_str()), 1, 2,
                                String16("v2"), String16(""));
 
-    ConfigMetricsReport report =
-            GetReports(service->mProcessor, 5 * 60 * NS_PER_SEC + start + 100, true);
-    EXPECT_EQ(1, report.metrics_size());
+    ConfigMetricsReport report = GetReports(service->mProcessor, 5 * 60 * NS_PER_SEC + start + 100);
+    backfillStartEndTimestamp(&report);
+    ASSERT_EQ(1, report.metrics_size());
     EXPECT_EQ(0, report.metrics(0).gauge_metrics().skipped_size());
+    // The fake subsystem state sleep puller returns two atoms.
+    ASSERT_EQ(2, report.metrics(0).gauge_metrics().data_size());
+    EXPECT_EQ(2, report.metrics(0).gauge_metrics().data(0).bucket_info_size());
 }
 
 TEST(PartialBucketE2eTest, TestGaugeMetricWithMinPartialBucket) {
     shared_ptr<StatsService> service = SharedRefBase::make<StatsService>(nullptr, nullptr);
     // Partial buckets don't occur when app is first installed.
     service->mUidMap->updateApp(1, String16(kApp1.c_str()), 1, 1, String16("v1"), String16(""));
+    service->mPullerManager->RegisterPullAtomCallback(
+            /*uid=*/0, util::SUBSYSTEM_SLEEP_STATE, NS_PER_SEC, NS_PER_SEC * 10, {},
+            SharedRefBase::make<FakeSubsystemSleepCallback>());
     SendConfig(service, MakeGaugeMetricConfig(60 * NS_PER_SEC /* One minute */));
     int64_t start = getElapsedRealtimeNs();  // This is the start-time the metrics producers are
                                              // initialized with.
@@ -298,7 +382,7 @@
                                 String16(""));
 
     ConfigMetricsReport report =
-            GetReports(service->mProcessor, 5 * 60 * NS_PER_SEC + start + 100 * NS_PER_SEC, true);
+            GetReports(service->mProcessor, 5 * 60 * NS_PER_SEC + start + 100 * NS_PER_SEC);
     backfillStartEndTimestamp(&report);
     ASSERT_EQ(1, report.metrics_size());
     ASSERT_EQ(1, report.metrics(0).gauge_metrics().skipped_size());
@@ -306,6 +390,38 @@
     EXPECT_TRUE(report.metrics(0).gauge_metrics().skipped(0).has_start_bucket_elapsed_nanos());
     EXPECT_EQ(MillisToNano(NanoToMillis(endSkipped)),
               report.metrics(0).gauge_metrics().skipped(0).end_bucket_elapsed_nanos());
+    ASSERT_EQ(2, report.metrics(0).gauge_metrics().data_size());
+    EXPECT_EQ(1, report.metrics(0).gauge_metrics().data(0).bucket_info_size());
+}
+
+TEST(PartialBucketE2eTest, TestGaugeMetricOnBootWithoutMinPartialBucket) {
+    shared_ptr<StatsService> service = SharedRefBase::make<StatsService>(nullptr, nullptr);
+    // Initial pull will fail since puller hasn't been registered.
+    SendConfig(service, MakeGaugeMetricConfig(0));
+    int64_t start = getElapsedRealtimeNs();  // This is the start-time the metrics producers are
+                                             // initialized with.
+
+    service->mPullerManager->RegisterPullAtomCallback(
+            /*uid=*/0, util::SUBSYSTEM_SLEEP_STATE, NS_PER_SEC, NS_PER_SEC * 10, {},
+            SharedRefBase::make<FakeSubsystemSleepCallback>());
+
+    int64_t bootCompleteTimeNs = start + NS_PER_SEC;
+    service->mProcessor->onStatsdInitCompleted(bootCompleteTimeNs);
+
+    service->mProcessor->informPullAlarmFired(5 * 60 * NS_PER_SEC + start);
+
+    ConfigMetricsReport report = GetReports(service->mProcessor, 5 * 60 * NS_PER_SEC + start + 100);
+    backfillStartEndTimestamp(&report);
+
+    ASSERT_EQ(1, report.metrics_size());
+    EXPECT_EQ(0, report.metrics(0).gauge_metrics().skipped_size());
+    // The fake subsystem state sleep puller returns two atoms.
+    ASSERT_EQ(2, report.metrics(0).gauge_metrics().data_size());
+    // No data in the first bucket, so nothing is reported
+    ASSERT_EQ(1, report.metrics(0).gauge_metrics().data(0).bucket_info_size());
+    EXPECT_EQ(
+            MillisToNano(NanoToMillis(bootCompleteTimeNs)),
+            report.metrics(0).gauge_metrics().data(0).bucket_info(0).start_bucket_elapsed_nanos());
 }
 
 #else
diff --git a/cmds/statsd/tests/guardrail/StatsdStats_test.cpp b/cmds/statsd/tests/guardrail/StatsdStats_test.cpp
index 129fafa..cdde603 100644
--- a/cmds/statsd/tests/guardrail/StatsdStats_test.cpp
+++ b/cmds/statsd/tests/guardrail/StatsdStats_test.cpp
@@ -486,6 +486,41 @@
     EXPECT_TRUE(uid2Good);
 }
 
+TEST(StatsdStatsTest, TestAtomErrorStats) {
+    StatsdStats stats;
+
+    int pushAtomTag = 100;
+    int pullAtomTag = 1000;
+    int numErrors = 10;
+
+    for (int i = 0; i < numErrors; i++) {
+        // We must call noteAtomLogged as well because only those pushed atoms
+        // that have been logged will have stats printed about them in the
+        // proto.
+        stats.noteAtomLogged(pushAtomTag, /*timeSec=*/0);
+        stats.noteAtomError(pushAtomTag, /*pull=*/false);
+
+        stats.noteAtomError(pullAtomTag, /*pull=*/true);
+    }
+
+    vector<uint8_t> output;
+    stats.dumpStats(&output, false);
+    StatsdStatsReport report;
+    EXPECT_TRUE(report.ParseFromArray(&output[0], output.size()));
+
+    // Check error count = numErrors for push atom
+    EXPECT_EQ(1, report.atom_stats_size());
+    const auto& pushedAtomStats = report.atom_stats(0);
+    EXPECT_EQ(pushAtomTag, pushedAtomStats.tag());
+    EXPECT_EQ(numErrors, pushedAtomStats.error_count());
+
+    // Check error count = numErrors for pull atom
+    EXPECT_EQ(1, report.pulled_atom_stats_size());
+    const auto& pulledAtomStats = report.pulled_atom_stats(0);
+    EXPECT_EQ(pullAtomTag, pulledAtomStats.atom_id());
+    EXPECT_EQ(numErrors, pulledAtomStats.atom_error_count());
+}
+
 }  // namespace statsd
 }  // namespace os
 }  // namespace android
diff --git a/cmds/statsd/tests/metrics/CountMetricProducer_test.cpp b/cmds/statsd/tests/metrics/CountMetricProducer_test.cpp
index 65f8de6..8131725 100644
--- a/cmds/statsd/tests/metrics/CountMetricProducer_test.cpp
+++ b/cmds/statsd/tests/metrics/CountMetricProducer_test.cpp
@@ -38,9 +38,9 @@
 namespace os {
 namespace statsd {
 
-const ConfigKey kConfigKey(0, 12345);
 
 namespace {
+const ConfigKey kConfigKey(0, 12345);
 
 void makeLogEvent(LogEvent* logEvent, int64_t timestampNs, int atomId) {
     AStatsEvent* statsEvent = AStatsEvent_obtain();
@@ -61,6 +61,13 @@
 
 }  // namespace
 
+// Setup for parameterized tests.
+class CountMetricProducerTest_PartialBucket : public TestWithParam<BucketSplitEvent> {};
+
+INSTANTIATE_TEST_SUITE_P(CountMetricProducerTest_PartialBucket,
+                         CountMetricProducerTest_PartialBucket,
+                         testing::Values(APP_UPGRADE, BOOT_COMPLETE));
+
 TEST(CountMetricProducerTest, TestFirstBucket) {
     CountMetric metric;
     metric.set_id(1);
@@ -237,11 +244,11 @@
     EXPECT_EQ(1LL, bucketInfo.mCount);
 }
 
-TEST(CountMetricProducerTest, TestEventWithAppUpgrade) {
+TEST_P(CountMetricProducerTest_PartialBucket, TestSplitInCurrentBucket) {
     sp<AlarmMonitor> alarmMonitor;
     int64_t bucketStartTimeNs = 10000000000;
     int64_t bucketSizeNs = TimeUnitToBucketSizeInMillis(ONE_MINUTE) * 1000000LL;
-    int64_t eventUpgradeTimeNs = bucketStartTimeNs + 15 * NS_PER_SEC;
+    int64_t eventTimeNs = bucketStartTimeNs + 15 * NS_PER_SEC;
 
     int tagId = 1;
     int conditionTagId = 2;
@@ -260,22 +267,30 @@
     sp<AnomalyTracker> anomalyTracker = countProducer.addAnomalyTracker(alert, alarmMonitor);
     EXPECT_TRUE(anomalyTracker != nullptr);
 
-    // Bucket is flushed yet.
+    // Bucket is not flushed yet.
     LogEvent event1(/*uid=*/0, /*pid=*/0);
     makeLogEvent(&event1, bucketStartTimeNs + 1, tagId, /*uid=*/"111");
     countProducer.onMatchedLogEvent(1 /*log matcher index*/, event1);
     EXPECT_EQ(0UL, countProducer.mPastBuckets.size());
     EXPECT_EQ(0, anomalyTracker->getSumOverPastBuckets(DEFAULT_METRIC_DIMENSION_KEY));
 
-    // App upgrade forces bucket flush.
+    // App upgrade or boot complete forces bucket flush.
     // Check that there's a past bucket and the bucket end is not adjusted.
-    countProducer.notifyAppUpgrade(eventUpgradeTimeNs, "ANY.APP", 1, 1);
+    switch (GetParam()) {
+        case APP_UPGRADE:
+            countProducer.notifyAppUpgrade(eventTimeNs);
+            break;
+        case BOOT_COMPLETE:
+            countProducer.onStatsdInitCompleted(eventTimeNs);
+            break;
+    }
     EXPECT_EQ(1UL, countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
-    EXPECT_EQ((long long)bucketStartTimeNs,
+    EXPECT_EQ(bucketStartTimeNs,
               countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][0].mBucketStartNs);
-    EXPECT_EQ((long long)eventUpgradeTimeNs,
+    EXPECT_EQ(eventTimeNs,
               countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][0].mBucketEndNs);
-    EXPECT_EQ(eventUpgradeTimeNs, countProducer.mCurrentBucketStartTimeNs);
+    EXPECT_EQ(0, countProducer.getCurrentBucketNum());
+    EXPECT_EQ(eventTimeNs, countProducer.mCurrentBucketStartTimeNs);
     // Anomaly tracker only contains full buckets.
     EXPECT_EQ(0, anomalyTracker->getSumOverPastBuckets(DEFAULT_METRIC_DIMENSION_KEY));
 
@@ -285,7 +300,8 @@
     makeLogEvent(&event2, bucketStartTimeNs + 59 * NS_PER_SEC + 10, tagId, /*uid=*/"222");
     countProducer.onMatchedLogEvent(1 /*log matcher index*/, event2);
     EXPECT_EQ(1UL, countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
-    EXPECT_EQ(eventUpgradeTimeNs, countProducer.mCurrentBucketStartTimeNs);
+    EXPECT_EQ(eventTimeNs, countProducer.mCurrentBucketStartTimeNs);
+    EXPECT_EQ(0, countProducer.getCurrentBucketNum());
     EXPECT_EQ(0, anomalyTracker->getSumOverPastBuckets(DEFAULT_METRIC_DIMENSION_KEY));
 
     // Third event in following bucket.
@@ -294,13 +310,14 @@
     countProducer.onMatchedLogEvent(1 /*log matcher index*/, event3);
     EXPECT_EQ(2UL, countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
     EXPECT_EQ(lastEndTimeNs, countProducer.mCurrentBucketStartTimeNs);
+    EXPECT_EQ(1, countProducer.getCurrentBucketNum());
     EXPECT_EQ(2, anomalyTracker->getSumOverPastBuckets(DEFAULT_METRIC_DIMENSION_KEY));
 }
 
-TEST(CountMetricProducerTest, TestEventWithAppUpgradeInNextBucket) {
+TEST_P(CountMetricProducerTest_PartialBucket, TestSplitInNextBucket) {
     int64_t bucketStartTimeNs = 10000000000;
     int64_t bucketSizeNs = TimeUnitToBucketSizeInMillis(ONE_MINUTE) * 1000000LL;
-    int64_t eventUpgradeTimeNs = bucketStartTimeNs + 65 * NS_PER_SEC;
+    int64_t eventTimeNs = bucketStartTimeNs + 65 * NS_PER_SEC;
 
     int tagId = 1;
     int conditionTagId = 2;
@@ -319,15 +336,23 @@
     countProducer.onMatchedLogEvent(1 /*log matcher index*/, event1);
     EXPECT_EQ(0UL, countProducer.mPastBuckets.size());
 
-    // App upgrade forces bucket flush.
-    // Check that there's a past bucket and the bucket end is not adjusted.
-    countProducer.notifyAppUpgrade(eventUpgradeTimeNs, "ANY.APP", 1, 1);
+    // App upgrade or boot complete forces bucket flush.
+    // Check that there's a past bucket and the bucket end is not adjusted since the upgrade
+    // occurred after the bucket end time.
+    switch (GetParam()) {
+        case APP_UPGRADE:
+            countProducer.notifyAppUpgrade(eventTimeNs);
+            break;
+        case BOOT_COMPLETE:
+            countProducer.onStatsdInitCompleted(eventTimeNs);
+            break;
+    }
     EXPECT_EQ(1UL, countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
-    EXPECT_EQ((int64_t)bucketStartTimeNs,
+    EXPECT_EQ(bucketStartTimeNs,
               countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][0].mBucketStartNs);
     EXPECT_EQ(bucketStartTimeNs + bucketSizeNs,
               countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][0].mBucketEndNs);
-    EXPECT_EQ(eventUpgradeTimeNs, countProducer.mCurrentBucketStartTimeNs);
+    EXPECT_EQ(eventTimeNs, countProducer.mCurrentBucketStartTimeNs);
 
     // Next event occurs in same bucket as partial bucket created.
     LogEvent event2(/*uid=*/0, /*pid=*/0);
@@ -340,7 +365,7 @@
     makeLogEvent(&event3, bucketStartTimeNs + 121 * NS_PER_SEC + 10, tagId, /*uid=*/"333");
     countProducer.onMatchedLogEvent(1 /*log matcher index*/, event3);
     EXPECT_EQ(2UL, countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
-    EXPECT_EQ((int64_t)eventUpgradeTimeNs,
+    EXPECT_EQ((int64_t)eventTimeNs,
               countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][1].mBucketStartNs);
     EXPECT_EQ(bucketStartTimeNs + 2 * bucketSizeNs,
               countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][1].mBucketEndNs);
diff --git a/cmds/statsd/tests/metrics/DurationMetricProducer_test.cpp b/cmds/statsd/tests/metrics/DurationMetricProducer_test.cpp
index 30f8159..8ef2519 100644
--- a/cmds/statsd/tests/metrics/DurationMetricProducer_test.cpp
+++ b/cmds/statsd/tests/metrics/DurationMetricProducer_test.cpp
@@ -41,10 +41,10 @@
 namespace os {
 namespace statsd {
 
-const ConfigKey kConfigKey(0, 12345);
 
 namespace {
 
+const ConfigKey kConfigKey(0, 12345);
 void makeLogEvent(LogEvent* logEvent, int64_t timestampNs, int atomId) {
     AStatsEvent* statsEvent = AStatsEvent_obtain();
     AStatsEvent_setAtomId(statsEvent, atomId);
@@ -55,6 +55,13 @@
 
 }  // namespace
 
+// Setup for parameterized tests.
+class DurationMetricProducerTest_PartialBucket : public TestWithParam<BucketSplitEvent> {};
+
+INSTANTIATE_TEST_SUITE_P(DurationMetricProducerTest_PartialBucket,
+                         DurationMetricProducerTest_PartialBucket,
+                         testing::Values(APP_UPGRADE, BOOT_COMPLETE));
+
 TEST(DurationMetricTrackerTest, TestFirstBucket) {
     sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>();
     DurationMetric metric;
@@ -205,7 +212,7 @@
     EXPECT_EQ(1LL, buckets2[0].mDuration);
 }
 
-TEST(DurationMetricTrackerTest, TestSumDurationWithUpgrade) {
+TEST_P(DurationMetricProducerTest_PartialBucket, TestSumDuration) {
     /**
      * The duration starts from the first bucket, through the two partial buckets (10-70sec),
      * another bucket, and ends at the beginning of the next full bucket.
@@ -217,15 +224,7 @@
      */
     int64_t bucketStartTimeNs = 10000000000;
     int64_t bucketSizeNs = TimeUnitToBucketSizeInMillis(ONE_MINUTE) * 1000000LL;
-    int64_t eventUpgradeTimeNs = bucketStartTimeNs + 15 * NS_PER_SEC;
-    int64_t startTimeNs = bucketStartTimeNs + 1 * NS_PER_SEC;
-    int64_t endTimeNs = startTimeNs + 125 * NS_PER_SEC;
-
     int tagId = 1;
-    LogEvent event1(/*uid=*/0, /*pid=*/0);
-    makeLogEvent(&event1, startTimeNs, tagId);
-    LogEvent event2(/*uid=*/0, /*pid=*/0);
-    makeLogEvent(&event2, endTimeNs, tagId);
 
     DurationMetric metric;
     metric.set_id(1);
@@ -238,32 +237,47 @@
                                             3 /* stop_all index */, false /*nesting*/, wizard,
                                             dimensions, bucketStartTimeNs, bucketStartTimeNs);
 
+    int64_t startTimeNs = bucketStartTimeNs + 1 * NS_PER_SEC;
+    LogEvent event1(/*uid=*/0, /*pid=*/0);
+    makeLogEvent(&event1, startTimeNs, tagId);
     durationProducer.onMatchedLogEvent(1 /* start index*/, event1);
     EXPECT_EQ(0UL, durationProducer.mPastBuckets.size());
     EXPECT_EQ(bucketStartTimeNs, durationProducer.mCurrentBucketStartTimeNs);
 
-    durationProducer.notifyAppUpgrade(eventUpgradeTimeNs, "ANY.APP", 1, 1);
+    int64_t partialBucketSplitTimeNs = bucketStartTimeNs + 15 * NS_PER_SEC;
+    switch (GetParam()) {
+        case APP_UPGRADE:
+            durationProducer.notifyAppUpgrade(partialBucketSplitTimeNs);
+            break;
+        case BOOT_COMPLETE:
+            durationProducer.onStatsdInitCompleted(partialBucketSplitTimeNs);
+            break;
+    }
     EXPECT_EQ(1UL, durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
     std::vector<DurationBucket> buckets =
             durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY];
     EXPECT_EQ(bucketStartTimeNs, buckets[0].mBucketStartNs);
-    EXPECT_EQ(eventUpgradeTimeNs, buckets[0].mBucketEndNs);
-    EXPECT_EQ(eventUpgradeTimeNs - startTimeNs, buckets[0].mDuration);
-    EXPECT_EQ(eventUpgradeTimeNs, durationProducer.mCurrentBucketStartTimeNs);
+    EXPECT_EQ(partialBucketSplitTimeNs, buckets[0].mBucketEndNs);
+    EXPECT_EQ(partialBucketSplitTimeNs - startTimeNs, buckets[0].mDuration);
+    EXPECT_EQ(partialBucketSplitTimeNs, durationProducer.mCurrentBucketStartTimeNs);
+    EXPECT_EQ(0, durationProducer.getCurrentBucketNum());
 
     // We skip ahead one bucket, so we fill in the first two partial buckets and one full bucket.
+    int64_t endTimeNs = startTimeNs + 125 * NS_PER_SEC;
+    LogEvent event2(/*uid=*/0, /*pid=*/0);
+    makeLogEvent(&event2, endTimeNs, tagId);
     durationProducer.onMatchedLogEvent(2 /* stop index*/, event2);
     buckets = durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY];
     EXPECT_EQ(3UL, buckets.size());
-    EXPECT_EQ(eventUpgradeTimeNs, buckets[1].mBucketStartNs);
+    EXPECT_EQ(partialBucketSplitTimeNs, buckets[1].mBucketStartNs);
     EXPECT_EQ(bucketStartTimeNs + bucketSizeNs, buckets[1].mBucketEndNs);
-    EXPECT_EQ(bucketStartTimeNs + bucketSizeNs - eventUpgradeTimeNs, buckets[1].mDuration);
+    EXPECT_EQ(bucketStartTimeNs + bucketSizeNs - partialBucketSplitTimeNs, buckets[1].mDuration);
     EXPECT_EQ(bucketStartTimeNs + bucketSizeNs, buckets[2].mBucketStartNs);
     EXPECT_EQ(bucketStartTimeNs + 2 * bucketSizeNs, buckets[2].mBucketEndNs);
     EXPECT_EQ(bucketSizeNs, buckets[2].mDuration);
 }
 
-TEST(DurationMetricTrackerTest, TestSumDurationWithUpgradeInFollowingBucket) {
+TEST_P(DurationMetricProducerTest_PartialBucket, TestSumDurationWithSplitInFollowingBucket) {
     /**
      * Expected buckets (start at 11s, upgrade at 75s, end at 135s):
      *  - [10,70]: 59 secs
@@ -272,15 +286,7 @@
      */
     int64_t bucketStartTimeNs = 10000000000;
     int64_t bucketSizeNs = TimeUnitToBucketSizeInMillis(ONE_MINUTE) * 1000000LL;
-    int64_t eventUpgradeTimeNs = bucketStartTimeNs + 65 * NS_PER_SEC;
-    int64_t startTimeNs = bucketStartTimeNs + 1 * NS_PER_SEC;
-    int64_t endTimeNs = startTimeNs + 125 * NS_PER_SEC;
-
     int tagId = 1;
-    LogEvent event1(/*uid=*/0, /*pid=*/0);
-    makeLogEvent(&event1, startTimeNs, tagId);
-    LogEvent event2(/*uid=*/0, /*pid=*/0);
-    makeLogEvent(&event2, endTimeNs, tagId);
 
     DurationMetric metric;
     metric.set_id(1);
@@ -293,11 +299,22 @@
                                             3 /* stop_all index */, false /*nesting*/, wizard,
                                             dimensions, bucketStartTimeNs, bucketStartTimeNs);
 
+    int64_t startTimeNs = bucketStartTimeNs + 1 * NS_PER_SEC;
+    LogEvent event1(/*uid=*/0, /*pid=*/0);
+    makeLogEvent(&event1, startTimeNs, tagId);
     durationProducer.onMatchedLogEvent(1 /* start index*/, event1);
     EXPECT_EQ(0UL, durationProducer.mPastBuckets.size());
     EXPECT_EQ(bucketStartTimeNs, durationProducer.mCurrentBucketStartTimeNs);
 
-    durationProducer.notifyAppUpgrade(eventUpgradeTimeNs, "ANY.APP", 1, 1);
+    int64_t partialBucketSplitTimeNs = bucketStartTimeNs + 65 * NS_PER_SEC;
+    switch (GetParam()) {
+        case APP_UPGRADE:
+            durationProducer.notifyAppUpgrade(partialBucketSplitTimeNs);
+            break;
+        case BOOT_COMPLETE:
+            durationProducer.onStatsdInitCompleted(partialBucketSplitTimeNs);
+            break;
+    }
     EXPECT_EQ(2UL, durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
     std::vector<DurationBucket> buckets =
             durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY];
@@ -305,32 +322,29 @@
     EXPECT_EQ(bucketStartTimeNs + bucketSizeNs, buckets[0].mBucketEndNs);
     EXPECT_EQ(bucketStartTimeNs + bucketSizeNs - startTimeNs, buckets[0].mDuration);
     EXPECT_EQ(bucketStartTimeNs + bucketSizeNs, buckets[1].mBucketStartNs);
-    EXPECT_EQ(eventUpgradeTimeNs, buckets[1].mBucketEndNs);
-    EXPECT_EQ(eventUpgradeTimeNs - (bucketStartTimeNs + bucketSizeNs), buckets[1].mDuration);
-    EXPECT_EQ(eventUpgradeTimeNs, durationProducer.mCurrentBucketStartTimeNs);
+    EXPECT_EQ(partialBucketSplitTimeNs, buckets[1].mBucketEndNs);
+    EXPECT_EQ(partialBucketSplitTimeNs - (bucketStartTimeNs + bucketSizeNs), buckets[1].mDuration);
+    EXPECT_EQ(partialBucketSplitTimeNs, durationProducer.mCurrentBucketStartTimeNs);
+    EXPECT_EQ(1, durationProducer.getCurrentBucketNum());
 
     // We skip ahead one bucket, so we fill in the first two partial buckets and one full bucket.
+    int64_t endTimeNs = startTimeNs + 125 * NS_PER_SEC;
+    LogEvent event2(/*uid=*/0, /*pid=*/0);
+    makeLogEvent(&event2, endTimeNs, tagId);
     durationProducer.onMatchedLogEvent(2 /* stop index*/, event2);
     buckets = durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY];
     EXPECT_EQ(3UL, buckets.size());
-    EXPECT_EQ(eventUpgradeTimeNs, buckets[2].mBucketStartNs);
+    EXPECT_EQ(partialBucketSplitTimeNs, buckets[2].mBucketStartNs);
     EXPECT_EQ(bucketStartTimeNs + 2 * bucketSizeNs, buckets[2].mBucketEndNs);
-    EXPECT_EQ(bucketStartTimeNs + 2 * bucketSizeNs - eventUpgradeTimeNs, buckets[2].mDuration);
+    EXPECT_EQ(bucketStartTimeNs + 2 * bucketSizeNs - partialBucketSplitTimeNs,
+              buckets[2].mDuration);
 }
 
-TEST(DurationMetricTrackerTest, TestSumDurationAnomalyWithUpgrade) {
+TEST_P(DurationMetricProducerTest_PartialBucket, TestSumDurationAnomaly) {
     sp<AlarmMonitor> alarmMonitor;
     int64_t bucketStartTimeNs = 10000000000;
     int64_t bucketSizeNs = TimeUnitToBucketSizeInMillis(ONE_MINUTE) * 1000000LL;
-    int64_t eventUpgradeTimeNs = bucketStartTimeNs + 15 * NS_PER_SEC;
-    int64_t startTimeNs = bucketStartTimeNs + 1;
-    int64_t endTimeNs = startTimeNs + 65 * NS_PER_SEC;
-
     int tagId = 1;
-    LogEvent event1(/*uid=*/0, /*pid=*/0);
-    makeLogEvent(&event1, startTimeNs, tagId);
-    LogEvent event2(/*uid=*/0, /*pid=*/0);
-    makeLogEvent(&event2, endTimeNs, tagId);
 
     // Setup metric with alert.
     DurationMetric metric;
@@ -351,27 +365,35 @@
     sp<AnomalyTracker> anomalyTracker = durationProducer.addAnomalyTracker(alert, alarmMonitor);
     EXPECT_TRUE(anomalyTracker != nullptr);
 
+    int64_t startTimeNs = bucketStartTimeNs + 1;
+    LogEvent event1(/*uid=*/0, /*pid=*/0);
+    makeLogEvent(&event1, startTimeNs, tagId);
     durationProducer.onMatchedLogEvent(1 /* start index*/, event1);
-    durationProducer.notifyAppUpgrade(eventUpgradeTimeNs, "ANY.APP", 1, 1);
+
+    int64_t partialBucketSplitTimeNs = bucketStartTimeNs + 15 * NS_PER_SEC;
+    switch (GetParam()) {
+        case APP_UPGRADE:
+            durationProducer.notifyAppUpgrade(partialBucketSplitTimeNs);
+            break;
+        case BOOT_COMPLETE:
+            durationProducer.onStatsdInitCompleted(partialBucketSplitTimeNs);
+            break;
+    }
 
     // We skip ahead one bucket, so we fill in the first two partial buckets and one full bucket.
+    int64_t endTimeNs = startTimeNs + 65 * NS_PER_SEC;
+    LogEvent event2(/*uid=*/0, /*pid=*/0);
+    makeLogEvent(&event2, endTimeNs, tagId);
     durationProducer.onMatchedLogEvent(2 /* stop index*/, event2);
+
     EXPECT_EQ(bucketStartTimeNs + bucketSizeNs - startTimeNs,
               anomalyTracker->getSumOverPastBuckets(DEFAULT_METRIC_DIMENSION_KEY));
 }
 
-TEST(DurationMetricTrackerTest, TestMaxDurationWithUpgrade) {
+TEST_P(DurationMetricProducerTest_PartialBucket, TestMaxDuration) {
     int64_t bucketStartTimeNs = 10000000000;
     int64_t bucketSizeNs = TimeUnitToBucketSizeInMillis(ONE_MINUTE) * 1000000LL;
-    int64_t eventUpgradeTimeNs = bucketStartTimeNs + 15 * NS_PER_SEC;
-    int64_t startTimeNs = bucketStartTimeNs + 1;
-    int64_t endTimeNs = startTimeNs + 125 * NS_PER_SEC;
-
     int tagId = 1;
-    LogEvent event1(/*uid=*/0, /*pid=*/0);
-    makeLogEvent(&event1, startTimeNs, tagId);
-    LogEvent event2(/*uid=*/0, /*pid=*/0);
-    makeLogEvent(&event2, endTimeNs, tagId);
 
     DurationMetric metric;
     metric.set_id(1);
@@ -385,15 +407,30 @@
                                             3 /* stop_all index */, false /*nesting*/, wizard,
                                             dimensions, bucketStartTimeNs, bucketStartTimeNs);
 
+    int64_t startTimeNs = bucketStartTimeNs + 1;
+    LogEvent event1(/*uid=*/0, /*pid=*/0);
+    makeLogEvent(&event1, startTimeNs, tagId);
     durationProducer.onMatchedLogEvent(1 /* start index*/, event1);
     EXPECT_EQ(0UL, durationProducer.mPastBuckets.size());
     EXPECT_EQ(bucketStartTimeNs, durationProducer.mCurrentBucketStartTimeNs);
 
-    durationProducer.notifyAppUpgrade(eventUpgradeTimeNs, "ANY.APP", 1, 1);
+    int64_t partialBucketSplitTimeNs = bucketStartTimeNs + 15 * NS_PER_SEC;
+    switch (GetParam()) {
+        case APP_UPGRADE:
+            durationProducer.notifyAppUpgrade(partialBucketSplitTimeNs);
+            break;
+        case BOOT_COMPLETE:
+            durationProducer.onStatsdInitCompleted(partialBucketSplitTimeNs);
+            break;
+    }
     EXPECT_EQ(0UL, durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
-    EXPECT_EQ(eventUpgradeTimeNs, durationProducer.mCurrentBucketStartTimeNs);
+    EXPECT_EQ(partialBucketSplitTimeNs, durationProducer.mCurrentBucketStartTimeNs);
+    EXPECT_EQ(0, durationProducer.getCurrentBucketNum());
 
     // We skip ahead one bucket, so we fill in the first two partial buckets and one full bucket.
+    int64_t endTimeNs = startTimeNs + 125 * NS_PER_SEC;
+    LogEvent event2(/*uid=*/0, /*pid=*/0);
+    makeLogEvent(&event2, endTimeNs, tagId);
     durationProducer.onMatchedLogEvent(2 /* stop index*/, event2);
     EXPECT_EQ(0UL, durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
 
@@ -406,18 +443,10 @@
     EXPECT_EQ(endTimeNs - startTimeNs, buckets[0].mDuration);
 }
 
-TEST(DurationMetricTrackerTest, TestMaxDurationWithUpgradeInNextBucket) {
+TEST_P(DurationMetricProducerTest_PartialBucket, TestMaxDurationWithSplitInNextBucket) {
     int64_t bucketStartTimeNs = 10000000000;
     int64_t bucketSizeNs = TimeUnitToBucketSizeInMillis(ONE_MINUTE) * 1000000LL;
-    int64_t eventUpgradeTimeNs = bucketStartTimeNs + 65 * NS_PER_SEC;
-    int64_t startTimeNs = bucketStartTimeNs + 1;
-    int64_t endTimeNs = startTimeNs + 115 * NS_PER_SEC;
-
     int tagId = 1;
-    LogEvent event1(/*uid=*/0, /*pid=*/0);
-    makeLogEvent(&event1, startTimeNs, tagId);
-    LogEvent event2(/*uid=*/0, /*pid=*/0);
-    makeLogEvent(&event2, endTimeNs, tagId);
 
     DurationMetric metric;
     metric.set_id(1);
@@ -431,24 +460,39 @@
                                             3 /* stop_all index */, false /*nesting*/, wizard,
                                             dimensions, bucketStartTimeNs, bucketStartTimeNs);
 
+    int64_t startTimeNs = bucketStartTimeNs + 1;
+    LogEvent event1(/*uid=*/0, /*pid=*/0);
+    makeLogEvent(&event1, startTimeNs, tagId);
     durationProducer.onMatchedLogEvent(1 /* start index*/, event1);
     EXPECT_EQ(0UL, durationProducer.mPastBuckets.size());
     EXPECT_EQ(bucketStartTimeNs, durationProducer.mCurrentBucketStartTimeNs);
 
-    durationProducer.notifyAppUpgrade(eventUpgradeTimeNs, "ANY.APP", 1, 1);
+    int64_t partialBucketSplitTimeNs = bucketStartTimeNs + 65 * NS_PER_SEC;
+    switch (GetParam()) {
+        case APP_UPGRADE:
+            durationProducer.notifyAppUpgrade(partialBucketSplitTimeNs);
+            break;
+        case BOOT_COMPLETE:
+            durationProducer.onStatsdInitCompleted(partialBucketSplitTimeNs);
+            break;
+    }
     EXPECT_EQ(0UL, durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
-    EXPECT_EQ(eventUpgradeTimeNs, durationProducer.mCurrentBucketStartTimeNs);
+    EXPECT_EQ(partialBucketSplitTimeNs, durationProducer.mCurrentBucketStartTimeNs);
+    EXPECT_EQ(1, durationProducer.getCurrentBucketNum());
 
     // Stop occurs in the same partial bucket as created for the app upgrade.
+    int64_t endTimeNs = startTimeNs + 115 * NS_PER_SEC;
+    LogEvent event2(/*uid=*/0, /*pid=*/0);
+    makeLogEvent(&event2, endTimeNs, tagId);
     durationProducer.onMatchedLogEvent(2 /* stop index*/, event2);
     EXPECT_EQ(0UL, durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
-    EXPECT_EQ(eventUpgradeTimeNs, durationProducer.mCurrentBucketStartTimeNs);
+    EXPECT_EQ(partialBucketSplitTimeNs, durationProducer.mCurrentBucketStartTimeNs);
 
     durationProducer.flushIfNeededLocked(bucketStartTimeNs + 2 * bucketSizeNs + 1);
     std::vector<DurationBucket> buckets =
             durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY];
     EXPECT_EQ(1UL, buckets.size());
-    EXPECT_EQ(eventUpgradeTimeNs, buckets[0].mBucketStartNs);
+    EXPECT_EQ(partialBucketSplitTimeNs, buckets[0].mBucketStartNs);
     EXPECT_EQ(bucketStartTimeNs + 2 * bucketSizeNs, buckets[0].mBucketEndNs);
     EXPECT_EQ(endTimeNs - startTimeNs, buckets[0].mDuration);
 }
diff --git a/cmds/statsd/tests/metrics/GaugeMetricProducer_test.cpp b/cmds/statsd/tests/metrics/GaugeMetricProducer_test.cpp
index 42d0d5d..9d2ec88 100644
--- a/cmds/statsd/tests/metrics/GaugeMetricProducer_test.cpp
+++ b/cmds/statsd/tests/metrics/GaugeMetricProducer_test.cpp
@@ -42,6 +42,8 @@
 namespace os {
 namespace statsd {
 
+namespace {
+
 const ConfigKey kConfigKey(0, 12345);
 const int tagId = 1;
 const int64_t metricId = 123;
@@ -52,9 +54,8 @@
 const int64_t bucket2StartTimeNs = bucketStartTimeNs + bucketSizeNs;
 const int64_t bucket3StartTimeNs = bucketStartTimeNs + 2 * bucketSizeNs;
 const int64_t bucket4StartTimeNs = bucketStartTimeNs + 3 * bucketSizeNs;
-const int64_t eventUpgradeTimeNs = bucketStartTimeNs + 15 * NS_PER_SEC;
+const int64_t partialBucketSplitTimeNs = bucketStartTimeNs + 15 * NS_PER_SEC;
 
-namespace {
 shared_ptr<LogEvent> makeLogEvent(int32_t atomId, int64_t timestampNs, int32_t value1, string str1,
                                   int32_t value2) {
     AStatsEvent* statsEvent = AStatsEvent_obtain();
@@ -71,6 +72,13 @@
 }
 }  // anonymous namespace
 
+// Setup for parameterized tests.
+class GaugeMetricProducerTest_PartialBucket : public TestWithParam<BucketSplitEvent> {};
+
+INSTANTIATE_TEST_SUITE_P(GaugeMetricProducerTest_PartialBucket,
+                         GaugeMetricProducerTest_PartialBucket,
+                         testing::Values(APP_UPGRADE, BOOT_COMPLETE));
+
 /*
  * Tests that the first bucket works correctly
  */
@@ -194,7 +202,7 @@
     EXPECT_EQ(25L, it->mValue.int_value);
 }
 
-TEST(GaugeMetricProducerTest, TestPushedEventsWithUpgrade) {
+TEST_P(GaugeMetricProducerTest_PartialBucket, TestPushedEvents) {
     sp<AlarmMonitor> alarmMonitor;
     GaugeMetric metric;
     metric.set_id(metricId);
@@ -230,11 +238,22 @@
     gaugeProducer.onMatchedLogEvent(1 /*log matcher index*/, event1);
     EXPECT_EQ(1UL, (*gaugeProducer.mCurrentSlicedBucket).count(DEFAULT_METRIC_DIMENSION_KEY));
 
-    gaugeProducer.notifyAppUpgrade(eventUpgradeTimeNs, "ANY.APP", 1, 1);
+    switch (GetParam()) {
+        case APP_UPGRADE:
+            gaugeProducer.notifyAppUpgrade(partialBucketSplitTimeNs);
+            break;
+        case BOOT_COMPLETE:
+            gaugeProducer.onStatsdInitCompleted(partialBucketSplitTimeNs);
+            break;
+    }
     EXPECT_EQ(0UL, (*gaugeProducer.mCurrentSlicedBucket).count(DEFAULT_METRIC_DIMENSION_KEY));
     EXPECT_EQ(1UL, gaugeProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
+    EXPECT_EQ(bucketStartTimeNs,
+              gaugeProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][0].mBucketStartNs);
+    EXPECT_EQ(partialBucketSplitTimeNs,
+              gaugeProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][0].mBucketEndNs);
     EXPECT_EQ(0L, gaugeProducer.mCurrentBucketNum);
-    EXPECT_EQ(eventUpgradeTimeNs, gaugeProducer.mCurrentBucketStartTimeNs);
+    EXPECT_EQ(partialBucketSplitTimeNs, gaugeProducer.mCurrentBucketStartTimeNs);
     // Partial buckets are not sent to anomaly tracker.
     EXPECT_EQ(0, anomalyTracker->getSumOverPastBuckets(DEFAULT_METRIC_DIMENSION_KEY));
 
@@ -244,7 +263,11 @@
     gaugeProducer.onMatchedLogEvent(1 /*log matcher index*/, event2);
     EXPECT_EQ(0L, gaugeProducer.mCurrentBucketNum);
     EXPECT_EQ(1UL, gaugeProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
-    EXPECT_EQ((int64_t)eventUpgradeTimeNs, gaugeProducer.mCurrentBucketStartTimeNs);
+    EXPECT_EQ(bucketStartTimeNs,
+              gaugeProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][0].mBucketStartNs);
+    EXPECT_EQ(partialBucketSplitTimeNs,
+              gaugeProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][0].mBucketEndNs);
+    EXPECT_EQ((int64_t)partialBucketSplitTimeNs, gaugeProducer.mCurrentBucketStartTimeNs);
     // Partial buckets are not sent to anomaly tracker.
     EXPECT_EQ(0, anomalyTracker->getSumOverPastBuckets(DEFAULT_METRIC_DIMENSION_KEY));
 
@@ -267,7 +290,7 @@
     EXPECT_EQ(2, anomalyTracker->getSumOverPastBuckets(DEFAULT_METRIC_DIMENSION_KEY));
 }
 
-TEST(GaugeMetricProducerTest, TestPulledWithUpgrade) {
+TEST_P(GaugeMetricProducerTest_PartialBucket, TestPulled) {
     GaugeMetric metric;
     metric.set_id(metricId);
     metric.set_bucket(ONE_MINUTE);
@@ -293,7 +316,8 @@
             .WillOnce(Invoke(
                     [](int tagId, const ConfigKey&, vector<std::shared_ptr<LogEvent>>* data, bool) {
                         data->clear();
-                        data->push_back(CreateRepeatedValueLogEvent(tagId, eventUpgradeTimeNs, 2));
+                        data->push_back(
+                                CreateRepeatedValueLogEvent(tagId, partialBucketSplitTimeNs, 2));
                         return true;
                     }));
 
@@ -311,10 +335,21 @@
                          .mFields->begin()
                          ->mValue.int_value);
 
-    gaugeProducer.notifyAppUpgrade(eventUpgradeTimeNs, "ANY.APP", 1, 1);
+    switch (GetParam()) {
+        case APP_UPGRADE:
+            gaugeProducer.notifyAppUpgrade(partialBucketSplitTimeNs);
+            break;
+        case BOOT_COMPLETE:
+            gaugeProducer.onStatsdInitCompleted(partialBucketSplitTimeNs);
+            break;
+    }
     EXPECT_EQ(1UL, gaugeProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
+    EXPECT_EQ(bucketStartTimeNs,
+              gaugeProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][0].mBucketStartNs);
+    EXPECT_EQ(partialBucketSplitTimeNs,
+              gaugeProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][0].mBucketEndNs);
     EXPECT_EQ(0L, gaugeProducer.mCurrentBucketNum);
-    EXPECT_EQ((int64_t)eventUpgradeTimeNs, gaugeProducer.mCurrentBucketStartTimeNs);
+    EXPECT_EQ(partialBucketSplitTimeNs, gaugeProducer.mCurrentBucketStartTimeNs);
     EXPECT_EQ(1UL, gaugeProducer.mCurrentSlicedBucket->size());
     EXPECT_EQ(2, gaugeProducer.mCurrentSlicedBucket->begin()
                          ->second.front()
@@ -370,7 +405,7 @@
                          .mFields->begin()
                          ->mValue.int_value);
 
-    gaugeProducer.notifyAppUpgrade(eventUpgradeTimeNs, "ANY.APP", 1, 1);
+    gaugeProducer.notifyAppUpgrade(partialBucketSplitTimeNs);
     EXPECT_EQ(0UL, gaugeProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
     EXPECT_EQ(0L, gaugeProducer.mCurrentBucketNum);
     EXPECT_EQ(bucketStartTimeNs, gaugeProducer.mCurrentBucketStartTimeNs);
diff --git a/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp b/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp
index 3b4d646..f493cc4 100644
--- a/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp
+++ b/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp
@@ -41,6 +41,8 @@
 namespace os {
 namespace statsd {
 
+namespace {
+
 const ConfigKey kConfigKey(0, 12345);
 const int tagId = 1;
 const int64_t metricId = 123;
@@ -58,10 +60,18 @@
 static void assertPastBucketValuesSingleKey(
         const std::unordered_map<MetricDimensionKey, std::vector<ValueBucket>>& mPastBuckets,
         const std::initializer_list<int>& expectedValuesList,
-        const std::initializer_list<int64_t>& expectedDurationNsList) {
-    std::vector<int> expectedValues(expectedValuesList);
-    std::vector<int64_t> expectedDurationNs(expectedDurationNsList);
+        const std::initializer_list<int64_t>& expectedDurationNsList,
+        const std::initializer_list<int64_t>& expectedStartTimeNsList,
+        const std::initializer_list<int64_t>& expectedEndTimeNsList) {
+    vector<int> expectedValues(expectedValuesList);
+    vector<int64_t> expectedDurationNs(expectedDurationNsList);
+    vector<int64_t> expectedStartTimeNs(expectedStartTimeNsList);
+    vector<int64_t> expectedEndTimeNs(expectedEndTimeNsList);
+
     ASSERT_EQ(expectedValues.size(), expectedDurationNs.size());
+    ASSERT_EQ(expectedValues.size(), expectedStartTimeNs.size());
+    ASSERT_EQ(expectedValues.size(), expectedEndTimeNs.size());
+
     if (expectedValues.size() == 0) {
         ASSERT_EQ(0, mPastBuckets.size());
         return;
@@ -70,15 +80,21 @@
     ASSERT_EQ(1, mPastBuckets.size());
     ASSERT_EQ(expectedValues.size(), mPastBuckets.begin()->second.size());
 
-    auto buckets = mPastBuckets.begin()->second;
+    const vector<ValueBucket>& buckets = mPastBuckets.begin()->second;
     for (int i = 0; i < expectedValues.size(); i++) {
         EXPECT_EQ(expectedValues[i], buckets[i].values[0].long_value)
                 << "Values differ at index " << i;
         EXPECT_EQ(expectedDurationNs[i], buckets[i].mConditionTrueNs)
                 << "Condition duration value differ at index " << i;
+        EXPECT_EQ(expectedStartTimeNs[i], buckets[i].mBucketStartNs)
+                << "Start time differs at index " << i;
+        EXPECT_EQ(expectedEndTimeNs[i], buckets[i].mBucketEndNs)
+                << "End time differs at index " << i;
     }
 }
 
+}  // anonymous namespace
+
 class ValueMetricProducerTestHelper {
 public:
     static sp<ValueMetricProducer> createValueProducerNoConditions(
@@ -191,6 +207,13 @@
     }
 };
 
+// Setup for parameterized tests.
+class ValueMetricProducerTest_PartialBucket : public TestWithParam<BucketSplitEvent> {};
+
+INSTANTIATE_TEST_SUITE_P(ValueMetricProducerTest_PartialBucket,
+                         ValueMetricProducerTest_PartialBucket,
+                         testing::Values(APP_UPGRADE, BOOT_COMPLETE));
+
 /*
  * Tests that the first bucket works correctly
  */
@@ -325,9 +348,10 @@
     EXPECT_EQ(bucketSizeNs, valueProducer->mPastBuckets.begin()->second[2].mConditionTrueNs);
 }
 
-TEST(ValueMetricProducerTest, TestPartialBucketCreated) {
+TEST_P(ValueMetricProducerTest_PartialBucket, TestPartialBucketCreated) {
     ValueMetric metric = ValueMetricProducerTestHelper::createMetric();
     sp<MockStatsPullerManager> pullerManager = new StrictMock<MockStatsPullerManager>();
+    int64_t partialBucketSplitTimeNs = bucket2StartTimeNs + 2;
     EXPECT_CALL(*pullerManager, Pull(tagId, kConfigKey, _, _))
             // Initialize bucket.
             .WillOnce(Invoke([](int tagId, const ConfigKey&,
@@ -337,10 +361,12 @@
                 return true;
             }))
             // Partial bucket.
-            .WillOnce(Invoke([](int tagId, const ConfigKey&,
-                                vector<std::shared_ptr<LogEvent>>* data, bool) {
+            .WillOnce(Invoke([partialBucketSplitTimeNs](int tagId, const ConfigKey&,
+                                                        vector<std::shared_ptr<LogEvent>>* data,
+                                                        bool) {
                 data->clear();
-                data->push_back(CreateRepeatedValueLogEvent(tagId, bucket2StartTimeNs + 10, 5));
+                data->push_back(
+                        CreateRepeatedValueLogEvent(tagId, partialBucketSplitTimeNs + 8, 5));
                 return true;
             }));
 
@@ -354,19 +380,21 @@
     valueProducer->onDataPulled(allData, /** success */ true, bucket2StartTimeNs);
 
     // Partial buckets created in 2nd bucket.
-    valueProducer->notifyAppUpgrade(bucket2StartTimeNs + 2, "com.foo", 10000, 1);
+    switch (GetParam()) {
+        case APP_UPGRADE:
+            valueProducer->notifyAppUpgrade(partialBucketSplitTimeNs);
+            break;
+        case BOOT_COMPLETE:
+            valueProducer->onStatsdInitCompleted(partialBucketSplitTimeNs);
+            break;
+    }
+    EXPECT_EQ(partialBucketSplitTimeNs, valueProducer->mCurrentBucketStartTimeNs);
+    EXPECT_EQ(1, valueProducer->getCurrentBucketNum());
 
-    // One full bucket and one partial bucket.
-    EXPECT_EQ(1UL, valueProducer->mPastBuckets.size());
-    vector<ValueBucket> buckets = valueProducer->mPastBuckets.begin()->second;
-    EXPECT_EQ(2UL, buckets.size());
-    // Full bucket (2 - 1)
-    EXPECT_EQ(1, buckets[0].values[0].long_value);
-    EXPECT_EQ(bucketSizeNs, buckets[0].mConditionTrueNs);
-    // Full bucket (5 - 3)
-    EXPECT_EQ(3, buckets[1].values[0].long_value);
-    // partial bucket [bucket2StartTimeNs, bucket2StartTimeNs + 2]
-    EXPECT_EQ(2, buckets[1].mConditionTrueNs);
+    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {1, 3},
+                                    {bucketSizeNs, partialBucketSplitTimeNs - bucket2StartTimeNs},
+                                    {bucketStartTimeNs, bucket2StartTimeNs},
+                                    {bucket2StartTimeNs, partialBucketSplitTimeNs});
 }
 
 /*
@@ -613,7 +641,8 @@
     allData.clear();
     allData.push_back(CreateRepeatedValueLogEvent(tagId, bucket2StartTimeNs + 1, 110));
     valueProducer->onDataPulled(allData, /** succeed */ true, bucket2StartTimeNs);
-    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {10}, {bucketSizeNs - 8});
+    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {10}, {bucketSizeNs - 8},
+                                    {bucketStartTimeNs}, {bucket2StartTimeNs});
 
     // has one slice
     EXPECT_EQ(1UL, valueProducer->mCurrentSlicedBucket.size());
@@ -625,7 +654,8 @@
     EXPECT_EQ(10, curInterval.value.long_value);
 
     valueProducer->onConditionChanged(false, bucket2StartTimeNs + 1);
-    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {10}, {bucketSizeNs - 8});
+    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {10}, {bucketSizeNs - 8},
+                                    {bucketStartTimeNs}, {bucket2StartTimeNs});
 
     // has one slice
     EXPECT_EQ(1UL, valueProducer->mCurrentSlicedBucket.size());
@@ -636,10 +666,12 @@
     EXPECT_EQ(false, curBaseInfo.hasBase);
 
     valueProducer->onConditionChanged(true, bucket3StartTimeNs + 1);
-    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {10, 20}, {bucketSizeNs - 8, 1});
+    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {10, 20}, {bucketSizeNs - 8, 1},
+                                    {bucketStartTimeNs, bucket2StartTimeNs},
+                                    {bucket2StartTimeNs, bucket3StartTimeNs});
 }
 
-TEST(ValueMetricProducerTest, TestPushedEventsWithUpgrade) {
+TEST_P(ValueMetricProducerTest_PartialBucket, TestPushedEvents) {
     ValueMetric metric = ValueMetricProducerTestHelper::createMetric();
 
     UidMap uidMap;
@@ -660,25 +692,46 @@
     valueProducer.onMatchedLogEvent(1 /*log matcher index*/, event1);
     EXPECT_EQ(1UL, valueProducer.mCurrentSlicedBucket.size());
 
-    valueProducer.notifyAppUpgrade(bucketStartTimeNs + 150, "ANY.APP", 1, 1);
-    EXPECT_EQ(1UL, valueProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
-    EXPECT_EQ(bucketStartTimeNs + 150, valueProducer.mCurrentBucketStartTimeNs);
+    int64_t partialBucketSplitTimeNs = bucketStartTimeNs + 150;
+    switch (GetParam()) {
+        case APP_UPGRADE:
+            valueProducer.notifyAppUpgrade(partialBucketSplitTimeNs);
+            break;
+        case BOOT_COMPLETE:
+            valueProducer.onStatsdInitCompleted(partialBucketSplitTimeNs);
+            break;
+    }
+    assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {10},
+                                    {partialBucketSplitTimeNs - bucketStartTimeNs},
+                                    {bucketStartTimeNs}, {partialBucketSplitTimeNs});
+    EXPECT_EQ(partialBucketSplitTimeNs, valueProducer.mCurrentBucketStartTimeNs);
+    EXPECT_EQ(0, valueProducer.getCurrentBucketNum());
 
+    // Event arrives after the bucket split.
     LogEvent event2(/*uid=*/0, /*pid=*/0);
-    CreateRepeatedValueLogEvent(&event2, tagId, bucketStartTimeNs + 59 * NS_PER_SEC, 10);
+    CreateRepeatedValueLogEvent(&event2, tagId, bucketStartTimeNs + 59 * NS_PER_SEC, 20);
     valueProducer.onMatchedLogEvent(1 /*log matcher index*/, event2);
-    EXPECT_EQ(1UL, valueProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
-    EXPECT_EQ(bucketStartTimeNs + 150, valueProducer.mCurrentBucketStartTimeNs);
+
+    assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {10},
+                                    {partialBucketSplitTimeNs - bucketStartTimeNs},
+                                    {bucketStartTimeNs}, {partialBucketSplitTimeNs});
+    EXPECT_EQ(partialBucketSplitTimeNs, valueProducer.mCurrentBucketStartTimeNs);
+    EXPECT_EQ(0, valueProducer.getCurrentBucketNum());
 
     // Next value should create a new bucket.
     LogEvent event3(/*uid=*/0, /*pid=*/0);
-    CreateRepeatedValueLogEvent(&event3, tagId, bucketStartTimeNs + 65 * NS_PER_SEC, 10);
+    CreateRepeatedValueLogEvent(&event3, tagId, bucket2StartTimeNs + 5 * NS_PER_SEC, 10);
     valueProducer.onMatchedLogEvent(1 /*log matcher index*/, event3);
-    EXPECT_EQ(2UL, valueProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
+    assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {10, 20},
+                                    {partialBucketSplitTimeNs - bucketStartTimeNs,
+                                     bucket2StartTimeNs - partialBucketSplitTimeNs},
+                                    {bucketStartTimeNs, partialBucketSplitTimeNs},
+                                    {partialBucketSplitTimeNs, bucket2StartTimeNs});
     EXPECT_EQ(bucketStartTimeNs + bucketSizeNs, valueProducer.mCurrentBucketStartTimeNs);
+    EXPECT_EQ(1, valueProducer.getCurrentBucketNum());
 }
 
-TEST(ValueMetricProducerTest, TestPulledValueWithUpgrade) {
+TEST_P(ValueMetricProducerTest_PartialBucket, TestPulledValue) {
     ValueMetric metric = ValueMetricProducerTestHelper::createMetric();
 
     UidMap uidMap;
@@ -689,14 +742,16 @@
                     atomMatcherId, logEventMatcherIndex, atomMatcher, uidMap)});
     sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>();
     sp<MockStatsPullerManager> pullerManager = new StrictMock<MockStatsPullerManager>();
+    int64_t partialBucketSplitTimeNs = bucket2StartTimeNs + 150;
     EXPECT_CALL(*pullerManager, RegisterReceiver(tagId, kConfigKey, _, _, _)).WillOnce(Return());
     EXPECT_CALL(*pullerManager, UnRegisterReceiver(tagId, kConfigKey, _)).WillOnce(Return());
     EXPECT_CALL(*pullerManager, Pull(tagId, kConfigKey, _, _))
             .WillOnce(Return(true))
-            .WillOnce(Invoke([](int tagId, const ConfigKey&,
-                                vector<std::shared_ptr<LogEvent>>* data, bool) {
+            .WillOnce(Invoke([partialBucketSplitTimeNs](int tagId, const ConfigKey&,
+                                                        vector<std::shared_ptr<LogEvent>>* data,
+                                                        bool) {
                 data->clear();
-                data->push_back(CreateRepeatedValueLogEvent(tagId, bucket2StartTimeNs + 149, 120));
+                data->push_back(CreateRepeatedValueLogEvent(tagId, partialBucketSplitTimeNs, 120));
                 return true;
             }));
     ValueMetricProducer valueProducer(kConfigKey, metric, -1, wizard, logEventMatcherIndex,
@@ -711,20 +766,27 @@
     valueProducer.onDataPulled(allData, /** succeed */ true, bucket2StartTimeNs);
     EXPECT_EQ(1UL, valueProducer.mCurrentSlicedBucket.size());
 
-    valueProducer.notifyAppUpgrade(bucket2StartTimeNs + 150, "ANY.APP", 1, 1);
-    EXPECT_EQ(1UL, valueProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
-    EXPECT_EQ(bucket2StartTimeNs + 150, valueProducer.mCurrentBucketStartTimeNs);
-    assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {20}, {150});
+    switch (GetParam()) {
+        case APP_UPGRADE:
+            valueProducer.notifyAppUpgrade(partialBucketSplitTimeNs);
+            break;
+        case BOOT_COMPLETE:
+            valueProducer.onStatsdInitCompleted(partialBucketSplitTimeNs);
+            break;
+    }
+    EXPECT_EQ(partialBucketSplitTimeNs, valueProducer.mCurrentBucketStartTimeNs);
+    EXPECT_EQ(1, valueProducer.getCurrentBucketNum());
+    assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {20}, {150}, {bucket2StartTimeNs},
+                                    {partialBucketSplitTimeNs});
 
     allData.clear();
     allData.push_back(CreateRepeatedValueLogEvent(tagId, bucket3StartTimeNs + 1, 150));
     valueProducer.onDataPulled(allData, /** succeed */ true, bucket3StartTimeNs);
-    EXPECT_EQ(2UL, valueProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
     EXPECT_EQ(bucket3StartTimeNs, valueProducer.mCurrentBucketStartTimeNs);
-    EXPECT_EQ(20L,
-              valueProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][0].values[0].long_value);
-    assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {20, 30},
-                                    {150, bucketSizeNs - 150});
+    EXPECT_EQ(2, valueProducer.getCurrentBucketNum());
+    assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {20, 30}, {150, bucketSizeNs - 150},
+                                    {bucket2StartTimeNs, partialBucketSplitTimeNs},
+                                    {partialBucketSplitTimeNs, bucket3StartTimeNs});
 }
 
 TEST(ValueMetricProducerTest, TestPulledWithAppUpgradeDisabled) {
@@ -754,12 +816,12 @@
     valueProducer.onDataPulled(allData, /** succeed */ true, bucket2StartTimeNs);
     EXPECT_EQ(1UL, valueProducer.mCurrentSlicedBucket.size());
 
-    valueProducer.notifyAppUpgrade(bucket2StartTimeNs + 150, "ANY.APP", 1, 1);
+    valueProducer.notifyAppUpgrade(bucket2StartTimeNs + 150);
     EXPECT_EQ(0UL, valueProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
     EXPECT_EQ(bucket2StartTimeNs, valueProducer.mCurrentBucketStartTimeNs);
 }
 
-TEST(ValueMetricProducerTest, TestPulledValueWithUpgradeWhileConditionFalse) {
+TEST_P(ValueMetricProducerTest_PartialBucket, TestPulledValueWhileConditionFalse) {
     ValueMetric metric = ValueMetricProducerTestHelper::createMetricWithCondition();
 
     sp<MockStatsPullerManager> pullerManager = new StrictMock<MockStatsPullerManager>();
@@ -784,14 +846,21 @@
     valueProducer->onConditionChanged(false, bucket2StartTimeNs - 100);
     EXPECT_FALSE(valueProducer->mCondition);
 
-    valueProducer->notifyAppUpgrade(bucket2StartTimeNs - 50, "ANY.APP", 1, 1);
+    int64_t partialBucketSplitTimeNs = bucket2StartTimeNs - 50;
+    switch (GetParam()) {
+        case APP_UPGRADE:
+            valueProducer->notifyAppUpgrade(partialBucketSplitTimeNs);
+            break;
+        case BOOT_COMPLETE:
+            valueProducer->onStatsdInitCompleted(partialBucketSplitTimeNs);
+            break;
+    }
     // Expect one full buckets already done and starting a partial bucket.
-    EXPECT_EQ(bucket2StartTimeNs - 50, valueProducer->mCurrentBucketStartTimeNs);
-    EXPECT_EQ(1UL, valueProducer->mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
-    EXPECT_EQ(bucketStartTimeNs,
-              valueProducer->mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][0].mBucketStartNs);
+    EXPECT_EQ(partialBucketSplitTimeNs, valueProducer->mCurrentBucketStartTimeNs);
+    EXPECT_EQ(0, valueProducer->getCurrentBucketNum());
     assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {20},
-                                    {(bucket2StartTimeNs - 100) - (bucketStartTimeNs + 1)});
+                                    {(bucket2StartTimeNs - 100) - (bucketStartTimeNs + 1)},
+                                    {bucketStartTimeNs}, {partialBucketSplitTimeNs});
     EXPECT_FALSE(valueProducer->mCondition);
 }
 TEST(ValueMetricProducerTest, TestPushedEventsWithoutCondition) {
@@ -834,7 +903,8 @@
     EXPECT_EQ(30, curInterval.value.long_value);
 
     valueProducer.flushIfNeededLocked(bucket2StartTimeNs);
-    assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {30}, {bucketSizeNs});
+    assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {30}, {bucketSizeNs},
+                                    {bucketStartTimeNs}, {bucket2StartTimeNs});
 }
 
 TEST(ValueMetricProducerTest, TestPushedEventsWithCondition) {
@@ -895,7 +965,8 @@
     EXPECT_EQ(50, curInterval.value.long_value);
 
     valueProducer.flushIfNeededLocked(bucket2StartTimeNs);
-    assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {50}, {20});
+    assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {50}, {20}, {bucketStartTimeNs},
+                                    {bucket2StartTimeNs});
 }
 
 TEST(ValueMetricProducerTest, TestAnomalyDetection) {
@@ -1012,7 +1083,8 @@
     EXPECT_EQ(true, curBaseInfo.hasBase);
     EXPECT_EQ(23, curBaseInfo.base.long_value);
     EXPECT_EQ(false, curInterval.hasValue);
-    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {12}, {bucketSizeNs});
+    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {12}, {bucketSizeNs},
+                                    {bucket2StartTimeNs}, {bucket3StartTimeNs});
 
     // pull 3 come late.
     // The previous bucket gets closed with error. (Has start value 23, no ending)
@@ -1028,7 +1100,15 @@
     EXPECT_EQ(true, curBaseInfo.hasBase);
     EXPECT_EQ(36, curBaseInfo.base.long_value);
     EXPECT_EQ(false, curInterval.hasValue);
-    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {12}, {bucketSizeNs});
+    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {12}, {bucketSizeNs},
+                                    {bucket2StartTimeNs}, {bucket3StartTimeNs});
+    // The 3rd bucket is dropped due to multiple buckets being skipped.
+    ASSERT_EQ(1, valueProducer->mSkippedBuckets.size());
+    EXPECT_EQ(bucket3StartTimeNs, valueProducer->mSkippedBuckets[0].bucketStartTimeNs);
+    EXPECT_EQ(bucket4StartTimeNs, valueProducer->mSkippedBuckets[0].bucketEndTimeNs);
+    ASSERT_EQ(1, valueProducer->mSkippedBuckets[0].dropEvents.size());
+    EXPECT_EQ(MULTIPLE_BUCKETS_SKIPPED, valueProducer->mSkippedBuckets[0].dropEvents[0].reason);
+    EXPECT_EQ(bucket6StartTimeNs, valueProducer->mSkippedBuckets[0].dropEvents[0].dropTimeNs);
 }
 
 /*
@@ -1073,7 +1153,8 @@
     valueProducer->onConditionChanged(false, bucket2StartTimeNs + 1);
     curInterval = valueProducer->mCurrentSlicedBucket.begin()->second[0];
     curBaseInfo = valueProducer->mCurrentBaseInfo.begin()->second[0];
-    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {20}, {bucketSizeNs - 8});
+    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {20}, {bucketSizeNs - 8},
+                                    {bucketStartTimeNs}, {bucket2StartTimeNs});
     EXPECT_EQ(false, curBaseInfo.hasBase);
 
     // Now the alarm is delivered.
@@ -1082,7 +1163,8 @@
     allData.push_back(CreateRepeatedValueLogEvent(tagId, bucket2StartTimeNs + 30, 110));
     valueProducer->onDataPulled(allData, /** succeed */ true, bucket2StartTimeNs);
 
-    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {20}, {bucketSizeNs - 8});
+    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {20}, {bucketSizeNs - 8},
+                                    {bucketStartTimeNs}, {bucket2StartTimeNs});
     curInterval = valueProducer->mCurrentSlicedBucket.begin()->second[0];
     curBaseInfo = valueProducer->mCurrentBaseInfo.begin()->second[0];
     EXPECT_EQ(false, curBaseInfo.hasBase);
@@ -1090,10 +1172,9 @@
 }
 
 /*
-* Test pulled event with non sliced condition. The pull on boundary come late, after the
-condition
-* change to false, and then true again. This is due to alarm delivered late.
-*/
+ * Test pulled event with non sliced condition. The pull on boundary come late, after the condition
+ * change to false, and then true again. This is due to alarm delivered late.
+ */
 TEST(ValueMetricProducerTest, TestBucketBoundaryWithCondition2) {
     ValueMetric metric = ValueMetricProducerTestHelper::createMetricWithCondition();
 
@@ -1139,7 +1220,8 @@
 
     // pull on bucket boundary come late, condition change happens before it
     valueProducer->onConditionChanged(false, bucket2StartTimeNs + 1);
-    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {20}, {bucketSizeNs - 8});
+    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {20}, {bucketSizeNs - 8},
+                                    {bucketStartTimeNs}, {bucket2StartTimeNs});
     EXPECT_EQ(1UL, valueProducer->mCurrentSlicedBucket.size());
     curInterval = valueProducer->mCurrentSlicedBucket.begin()->second[0];
     curBaseInfo = valueProducer->mCurrentBaseInfo.begin()->second[0];
@@ -1148,7 +1230,8 @@
 
     // condition changed to true again, before the pull alarm is delivered
     valueProducer->onConditionChanged(true, bucket2StartTimeNs + 25);
-    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {20}, {bucketSizeNs - 8});
+    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {20}, {bucketSizeNs - 8},
+                                    {bucketStartTimeNs}, {bucket2StartTimeNs});
     curInterval = valueProducer->mCurrentSlicedBucket.begin()->second[0];
     curBaseInfo = valueProducer->mCurrentBaseInfo.begin()->second[0];
     EXPECT_EQ(true, curBaseInfo.hasBase);
@@ -1167,13 +1250,15 @@
     EXPECT_EQ(140, curBaseInfo.base.long_value);
     EXPECT_EQ(true, curInterval.hasValue);
     EXPECT_EQ(10, curInterval.value.long_value);
-    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {20}, {bucketSizeNs - 8});
+    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {20}, {bucketSizeNs - 8},
+                                    {bucketStartTimeNs}, {bucket2StartTimeNs});
 
     allData.clear();
     allData.push_back(CreateRepeatedValueLogEvent(tagId, bucket3StartTimeNs, 160));
     valueProducer->onDataPulled(allData, /** succeed */ true, bucket3StartTimeNs);
-    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {20, 30},
-                                    {bucketSizeNs - 8, bucketSizeNs - 24});
+    assertPastBucketValuesSingleKey(
+            valueProducer->mPastBuckets, {20, 30}, {bucketSizeNs - 8, bucketSizeNs - 24},
+            {bucketStartTimeNs, bucket2StartTimeNs}, {bucket2StartTimeNs, bucket3StartTimeNs});
 }
 
 TEST(ValueMetricProducerTest, TestPushedAggregateMin) {
@@ -1216,7 +1301,8 @@
     EXPECT_EQ(10, curInterval.value.long_value);
 
     valueProducer.flushIfNeededLocked(bucket2StartTimeNs);
-    assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {10}, {bucketSizeNs});
+    assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {10}, {bucketSizeNs},
+                                    {bucketStartTimeNs}, {bucket2StartTimeNs});
 }
 
 TEST(ValueMetricProducerTest, TestPushedAggregateMax) {
@@ -1239,9 +1325,6 @@
 
     LogEvent event1(/*uid=*/0, /*pid=*/0);
     CreateRepeatedValueLogEvent(&event1, tagId, bucketStartTimeNs + 10, 10);
-
-    LogEvent event2(/*uid=*/0, /*pid=*/0);
-    CreateRepeatedValueLogEvent(&event2, tagId, bucketStartTimeNs + 20, 20);
     valueProducer.onMatchedLogEvent(1 /*log matcher index*/, event1);
 
     // has one slice
@@ -1251,6 +1334,8 @@
     EXPECT_EQ(10, curInterval.value.long_value);
     EXPECT_EQ(true, curInterval.hasValue);
 
+    LogEvent event2(/*uid=*/0, /*pid=*/0);
+    CreateRepeatedValueLogEvent(&event2, tagId, bucketStartTimeNs + 20, 20);
     valueProducer.onMatchedLogEvent(1 /*log matcher index*/, event2);
 
     // has one slice
@@ -1258,10 +1343,9 @@
     curInterval = valueProducer.mCurrentSlicedBucket.begin()->second[0];
     EXPECT_EQ(20, curInterval.value.long_value);
 
-    valueProducer.flushIfNeededLocked(bucket3StartTimeNs);
-    /* EXPECT_EQ(1UL, valueProducer.mPastBuckets.size()); */
-    /* EXPECT_EQ(1UL, valueProducer.mPastBuckets.begin()->second.size()); */
-    /* EXPECT_EQ(20, valueProducer.mPastBuckets.begin()->second.back().values[0].long_value); */
+    valueProducer.flushIfNeededLocked(bucket2StartTimeNs);
+    assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {20}, {bucketSizeNs},
+                                    {bucketStartTimeNs}, {bucket2StartTimeNs});
 }
 
 TEST(ValueMetricProducerTest, TestPushedAggregateAvg) {
@@ -1351,7 +1435,8 @@
     EXPECT_EQ(25, curInterval.value.long_value);
 
     valueProducer.flushIfNeededLocked(bucket2StartTimeNs);
-    assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {25}, {bucketSizeNs});
+    assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {25}, {bucketSizeNs},
+                                    {bucketStartTimeNs}, {bucket2StartTimeNs});
 }
 
 TEST(ValueMetricProducerTest, TestSkipZeroDiffOutput) {
@@ -1375,10 +1460,8 @@
 
     LogEvent event1(/*uid=*/0, /*pid=*/0);
     CreateRepeatedValueLogEvent(&event1, tagId, bucketStartTimeNs + 10, 10);
-
-    LogEvent event2(/*uid=*/0, /*pid=*/0);
-    CreateRepeatedValueLogEvent(&event2, tagId, bucketStartTimeNs + 15, 15);
     valueProducer.onMatchedLogEvent(1 /*log matcher index*/, event1);
+
     // has one slice
     EXPECT_EQ(1UL, valueProducer.mCurrentSlicedBucket.size());
     ValueMetricProducer::Interval curInterval =
@@ -1388,6 +1471,8 @@
     EXPECT_EQ(10, curBaseInfo.base.long_value);
     EXPECT_EQ(false, curInterval.hasValue);
 
+    LogEvent event2(/*uid=*/0, /*pid=*/0);
+    CreateRepeatedValueLogEvent(&event2, tagId, bucketStartTimeNs + 15, 15);
     valueProducer.onMatchedLogEvent(1 /*log matcher index*/, event2);
 
     // has one slice
@@ -1400,12 +1485,14 @@
     LogEvent event3(/*uid=*/0, /*pid=*/0);
     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];
     curBaseInfo = valueProducer.mCurrentBaseInfo.begin()->second[0];
     EXPECT_EQ(true, curBaseInfo.hasBase);
     EXPECT_EQ(15, curBaseInfo.base.long_value);
     EXPECT_EQ(true, curInterval.hasValue);
+    EXPECT_EQ(0, curInterval.value.long_value);
 
     LogEvent event4(/*uid=*/0, /*pid=*/0);
     CreateRepeatedValueLogEvent(&event4, tagId, bucket2StartTimeNs + 15, 15);
@@ -1416,11 +1503,11 @@
     EXPECT_EQ(true, curBaseInfo.hasBase);
     EXPECT_EQ(15, curBaseInfo.base.long_value);
     EXPECT_EQ(true, curInterval.hasValue);
+    EXPECT_EQ(0, curInterval.value.long_value);
 
     valueProducer.flushIfNeededLocked(bucket3StartTimeNs);
-    EXPECT_EQ(1UL, valueProducer.mPastBuckets.size());
-    EXPECT_EQ(1UL, valueProducer.mPastBuckets.begin()->second.size());
-    assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {5}, {bucketSizeNs});
+    assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {5}, {bucketSizeNs},
+                                    {bucketStartTimeNs}, {bucket2StartTimeNs});
 }
 
 TEST(ValueMetricProducerTest, TestSkipZeroDiffOutputMultiValue) {
@@ -1740,8 +1827,8 @@
     EXPECT_EQ(3, baseInfo1.base.long_value);
     EXPECT_EQ(false, interval1.hasValue);
     EXPECT_EQ(0UL, valueProducer->mPastBuckets.size());
-    vector<shared_ptr<LogEvent>> allData;
 
+    vector<shared_ptr<LogEvent>> allData;
     allData.clear();
     allData.push_back(CreateTwoValueLogEvent(tagId, bucket2StartTimeNs + 1, 2, 4));
     allData.push_back(CreateTwoValueLogEvent(tagId, bucket2StartTimeNs + 1, 1, 11));
@@ -1753,7 +1840,8 @@
     EXPECT_EQ(false, interval1.hasValue);
     EXPECT_EQ(8, interval1.value.long_value);
     EXPECT_FALSE(interval1.seenNewData);
-    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {8}, {bucketSizeNs});
+    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {8}, {bucketSizeNs},
+                                    {bucketStartTimeNs}, {bucket2StartTimeNs});
 
     auto it = valueProducer->mCurrentSlicedBucket.begin();
     for (; it != valueProducer->mCurrentSlicedBucket.end(); it++) {
@@ -1769,14 +1857,13 @@
     }
     EXPECT_TRUE(it != iter);
     EXPECT_TRUE(itBase != iterBase);
-    auto& interval2 = it->second[0];
-    auto& baseInfo2 = itBase->second[0];
+    auto interval2 = it->second[0];
+    auto baseInfo2 = itBase->second[0];
     EXPECT_EQ(2, it->first.getDimensionKeyInWhat().getValues()[0].mValue.int_value);
     EXPECT_EQ(true, baseInfo2.hasBase);
     EXPECT_EQ(4, baseInfo2.base.long_value);
     EXPECT_EQ(false, interval2.hasValue);
     EXPECT_FALSE(interval2.seenNewData);
-    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {8}, {bucketSizeNs});
 
     // next pull somehow did not happen, skip to end of bucket 3
     allData.clear();
@@ -1791,7 +1878,8 @@
     EXPECT_EQ(5, baseInfo2.base.long_value);
     EXPECT_EQ(false, interval2.hasValue);
     EXPECT_FALSE(interval2.seenNewData);
-    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {8}, {bucketSizeNs});
+    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {8}, {bucketSizeNs},
+                                    {bucketStartTimeNs}, {bucket2StartTimeNs});
 
     allData.clear();
     allData.push_back(CreateTwoValueLogEvent(tagId, bucket5StartTimeNs + 1, 2, 14));
@@ -1805,9 +1893,13 @@
     EXPECT_FALSE(interval2.seenNewData);
     ASSERT_EQ(2UL, valueProducer->mPastBuckets.size());
     auto iterator = valueProducer->mPastBuckets.begin();
+    EXPECT_EQ(bucket4StartTimeNs, iterator->second[0].mBucketStartNs);
+    EXPECT_EQ(bucket5StartTimeNs, iterator->second[0].mBucketEndNs);
     EXPECT_EQ(9, iterator->second[0].values[0].long_value);
     EXPECT_EQ(bucketSizeNs, iterator->second[0].mConditionTrueNs);
     iterator++;
+    EXPECT_EQ(bucketStartTimeNs, iterator->second[0].mBucketStartNs);
+    EXPECT_EQ(bucket2StartTimeNs, iterator->second[0].mBucketEndNs);
     EXPECT_EQ(8, iterator->second[0].values[0].long_value);
     EXPECT_EQ(bucketSizeNs, iterator->second[0].mConditionTrueNs);
 }
@@ -2414,7 +2506,8 @@
     EXPECT_EQ(true, valueProducer->mHasGlobalBase);
 
     EXPECT_EQ(1UL, valueProducer->mPastBuckets.size());
-    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {1}, {bucketSizeNs - 12 + 1});
+    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {1}, {bucketSizeNs - 12 + 1},
+                                    {bucketStartTimeNs}, {bucket2StartTimeNs});
 }
 
 TEST(ValueMetricProducerTest, TestPartialResetOnBucketBoundaries) {
@@ -2461,10 +2554,11 @@
     EXPECT_EQ(true, valueProducer->mHasGlobalBase);
 }
 
-TEST(ValueMetricProducerTest, TestFullBucketResetWhenLastBucketInvalid) {
+TEST_P(ValueMetricProducerTest_PartialBucket, TestFullBucketResetWhenLastBucketInvalid) {
     ValueMetric metric = ValueMetricProducerTestHelper::createMetric();
 
     sp<MockStatsPullerManager> pullerManager = new StrictMock<MockStatsPullerManager>();
+    int64_t partialBucketSplitTimeNs = bucketStartTimeNs + bucketSizeNs / 2;
     EXPECT_CALL(*pullerManager, Pull(tagId, kConfigKey, _, _))
             // Initialization.
             .WillOnce(Invoke(
@@ -2474,23 +2568,41 @@
                         return true;
                     }))
             // notifyAppUpgrade.
-            .WillOnce(Invoke([](int tagId, const ConfigKey&,
-                                vector<std::shared_ptr<LogEvent>>* data, bool) {
+            .WillOnce(Invoke([partialBucketSplitTimeNs](int tagId, const ConfigKey&,
+                                                        vector<std::shared_ptr<LogEvent>>* data,
+                                                        bool) {
                 data->clear();
-                data->push_back(CreateRepeatedValueLogEvent(
-                        tagId, bucketStartTimeNs + bucketSizeNs / 2, 10));
+                data->push_back(CreateRepeatedValueLogEvent(tagId, partialBucketSplitTimeNs, 10));
                 return true;
             }));
     sp<ValueMetricProducer> valueProducer =
             ValueMetricProducerTestHelper::createValueProducerNoConditions(pullerManager, metric);
     ASSERT_EQ(0UL, valueProducer->mCurrentFullBucket.size());
 
-    valueProducer->notifyAppUpgrade(bucketStartTimeNs + bucketSizeNs / 2, "com.foo", 10000, 1);
+    switch (GetParam()) {
+        case APP_UPGRADE:
+            valueProducer->notifyAppUpgrade(partialBucketSplitTimeNs);
+            break;
+        case BOOT_COMPLETE:
+            valueProducer->onStatsdInitCompleted(partialBucketSplitTimeNs);
+            break;
+    }
+    EXPECT_EQ(partialBucketSplitTimeNs, valueProducer->mCurrentBucketStartTimeNs);
+    EXPECT_EQ(0, valueProducer->getCurrentBucketNum());
+    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {9},
+                                    {partialBucketSplitTimeNs - bucketStartTimeNs},
+                                    {bucketStartTimeNs}, {partialBucketSplitTimeNs});
     ASSERT_EQ(1UL, valueProducer->mCurrentFullBucket.size());
 
     vector<shared_ptr<LogEvent>> allData;
     allData.push_back(CreateRepeatedValueLogEvent(tagId, bucket3StartTimeNs + 1, 4));
     valueProducer->onDataPulled(allData, /** fails */ false, bucket3StartTimeNs + 1);
+    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {9},
+                                    {partialBucketSplitTimeNs - bucketStartTimeNs},
+                                    {bucketStartTimeNs}, {partialBucketSplitTimeNs});
+    ASSERT_EQ(1, valueProducer->mSkippedBuckets.size());
+    EXPECT_EQ(partialBucketSplitTimeNs, valueProducer->mSkippedBuckets[0].bucketStartTimeNs);
+    EXPECT_EQ(bucket2StartTimeNs, valueProducer->mSkippedBuckets[0].bucketEndTimeNs);
     ASSERT_EQ(0UL, valueProducer->mCurrentFullBucket.size());
 }
 
@@ -2537,7 +2649,8 @@
     valueProducer->onConditionChanged(false, bucket3StartTimeNs + 10);
 
     // Bucket should have been completed.
-    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {2}, {bucketSizeNs - 10});
+    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {2}, {bucketSizeNs - 10},
+                                    {bucket2StartTimeNs}, {bucket3StartTimeNs});
 }
 
 TEST(ValueMetricProducerTest, TestLateOnDataPulledWithoutDiff) {
@@ -2557,7 +2670,8 @@
     valueProducer->onDataPulled(allData, /** succeed */ true, bucket2StartTimeNs);
 
     // Bucket should have been completed.
-    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {30}, {bucketSizeNs});
+    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {30}, {bucketSizeNs},
+                                    {bucketStartTimeNs}, {bucket2StartTimeNs});
 }
 
 TEST(ValueMetricProducerTest, TestLateOnDataPulledWithDiff) {
@@ -2585,12 +2699,14 @@
     valueProducer->onDataPulled(allData, /** succeed */ true, bucket2StartTimeNs);
 
     // Bucket should have been completed.
-    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {19}, {bucketSizeNs});
+    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {19}, {bucketSizeNs},
+                                    {bucketStartTimeNs}, {bucket2StartTimeNs});
 }
 
-TEST(ValueMetricProducerTest, TestBucketBoundariesOnAppUpgrade) {
+TEST_P(ValueMetricProducerTest_PartialBucket, TestBucketBoundariesOnPartialBucket) {
     ValueMetric metric = ValueMetricProducerTestHelper::createMetric();
 
+    int64_t partialBucketSplitTimeNs = bucket2StartTimeNs + 2;
     sp<MockStatsPullerManager> pullerManager = new StrictMock<MockStatsPullerManager>();
     EXPECT_CALL(*pullerManager, Pull(tagId, kConfigKey, _, _))
             // Initialization.
@@ -2601,20 +2717,29 @@
                         return true;
                     }))
             // notifyAppUpgrade.
-            .WillOnce(Invoke([](int tagId, const ConfigKey&,
-                                vector<std::shared_ptr<LogEvent>>* data, bool) {
+            .WillOnce(Invoke([partialBucketSplitTimeNs](int tagId, const ConfigKey&,
+                                                        vector<std::shared_ptr<LogEvent>>* data,
+                                                        bool) {
                 data->clear();
-                data->push_back(CreateRepeatedValueLogEvent(tagId, bucket2StartTimeNs + 2, 10));
+                data->push_back(CreateRepeatedValueLogEvent(tagId, partialBucketSplitTimeNs, 10));
                 return true;
             }));
 
     sp<ValueMetricProducer> valueProducer =
             ValueMetricProducerTestHelper::createValueProducerNoConditions(pullerManager, metric);
 
-    valueProducer->notifyAppUpgrade(bucket2StartTimeNs + 2, "com.foo", 10000, 1);
+    switch (GetParam()) {
+        case APP_UPGRADE:
+            valueProducer->notifyAppUpgrade(partialBucketSplitTimeNs);
+            break;
+        case BOOT_COMPLETE:
+            valueProducer->onStatsdInitCompleted(partialBucketSplitTimeNs);
+            break;
+    }
 
     // Bucket should have been completed.
-    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {9}, {bucketSizeNs});
+    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {9}, {bucketSizeNs},
+                                    {bucketStartTimeNs}, {bucket2StartTimeNs});
 }
 
 TEST(ValueMetricProducerTest, TestDataIsNotUpdatedWhenNoConditionChanged) {
@@ -2642,7 +2767,7 @@
 
     valueProducer->onConditionChanged(true, bucketStartTimeNs + 8);
     valueProducer->onConditionChanged(false, bucketStartTimeNs + 10);
-    valueProducer->onConditionChanged(false, bucketStartTimeNs + 10);
+    valueProducer->onConditionChanged(false, bucketStartTimeNs + 12);
 
     EXPECT_EQ(1UL, valueProducer->mCurrentSlicedBucket.size());
     auto curInterval = valueProducer->mCurrentSlicedBucket.begin()->second[0];
@@ -2654,7 +2779,8 @@
     allData.push_back(CreateRepeatedValueLogEvent(tagId, bucket2StartTimeNs + 1, 10));
     valueProducer->onDataPulled(allData, /** succeed */ true, bucket2StartTimeNs + 1);
 
-    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {2}, {2});
+    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {2}, {2}, {bucketStartTimeNs},
+                                    {bucket2StartTimeNs});
 }
 
 // TODO: b/145705635 fix or delete this test
@@ -2705,7 +2831,7 @@
     valueProducer->onDataPulled(allData, /** succeed */ true, bucket2StartTimeNs);
 
     // There was not global base available so all buckets are invalid.
-    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {}, {});
+    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {}, {}, {}, {});
 }
 
 TEST(ValueMetricProducerTest, TestPullNeededFastDump) {
@@ -2849,7 +2975,8 @@
     valueProducer->onDataPulled(allData, /** succeed */ true, bucket2StartTimeNs + 30);
 
     // Bucket should have been completed.
-    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {10}, {bucketSizeNs});
+    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {10}, {bucketSizeNs},
+                                    {bucketStartTimeNs}, {bucket2StartTimeNs});
 }
 
 TEST(ValueMetricProducerTest, TestPulledData_noDiff_withMultipleConditionChanges) {
@@ -2892,7 +3019,8 @@
     allData.push_back(CreateRepeatedValueLogEvent(tagId, bucket2StartTimeNs + 30, 110));
     valueProducer->onDataPulled(allData, /** succeed */ true, bucket2StartTimeNs);
 
-    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {20}, {50 - 8});
+    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {20}, {50 - 8},
+                                    {bucketStartTimeNs}, {bucket2StartTimeNs});
     curInterval = valueProducer->mCurrentSlicedBucket.begin()->second[0];
     curBaseInfo = valueProducer->mCurrentBaseInfo.begin()->second[0];
     EXPECT_EQ(false, curBaseInfo.hasBase);
@@ -2923,7 +3051,8 @@
     allData.push_back(CreateRepeatedValueLogEvent(tagId, bucket2StartTimeNs + 30, 30));
     valueProducer->onDataPulled(allData, /** succeed */ true, bucket2StartTimeNs);
 
-    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {30}, {bucketSizeNs - 8});
+    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {30}, {bucketSizeNs - 8},
+                                    {bucketStartTimeNs}, {bucket2StartTimeNs});
     ValueMetricProducer::Interval curInterval =
             valueProducer->mCurrentSlicedBucket.begin()->second[0];
     ValueMetricProducer::BaseInfo curBaseInfo = valueProducer->mCurrentBaseInfo.begin()->second[0];
@@ -2946,7 +3075,7 @@
     valueProducer->onDataPulled(allData, /** succeed */ true, bucket2StartTimeNs);
 
     // Condition was always false.
-    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {}, {});
+    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {}, {}, {}, {});
 }
 
 TEST(ValueMetricProducerTest, TestPulledData_noDiff_withFailure) {
@@ -2976,7 +3105,7 @@
     valueProducer->onDataPulled(allData, /** succeed */ true, bucket2StartTimeNs);
 
     // No buckets, we had a failure.
-    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {}, {});
+    assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {}, {}, {}, {});
 }
 
 /*
diff --git a/cmds/statsd/tests/metrics/metrics_test_helper.h b/cmds/statsd/tests/metrics/metrics_test_helper.h
index 69f7e3f..be410b1 100644
--- a/cmds/statsd/tests/metrics/metrics_test_helper.h
+++ b/cmds/statsd/tests/metrics/metrics_test_helper.h
@@ -44,7 +44,8 @@
                             vector<std::shared_ptr<LogEvent>>* data, bool useUids));
     MOCK_METHOD2(RegisterPullUidProvider,
                  void(const ConfigKey& configKey, wp<PullUidProvider> provider));
-    MOCK_METHOD1(UnregisterPullUidProvider, void(const ConfigKey& configKey));
+    MOCK_METHOD2(UnregisterPullUidProvider,
+                 void(const ConfigKey& configKey, wp<PullUidProvider> provider));
 };
 
 class MockUidMap : public UidMap {
diff --git a/cmds/statsd/tests/statsd_test_util.h b/cmds/statsd/tests/statsd_test_util.h
index 37b9889..4d68ea2 100644
--- a/cmds/statsd/tests/statsd_test_util.h
+++ b/cmds/statsd/tests/statsd_test_util.h
@@ -42,6 +42,8 @@
 const int SCREEN_STATE_ATOM_ID = util::SCREEN_STATE_CHANGED;
 const int UID_PROCESS_STATE_ATOM_ID = util::UID_PROCESS_STATE_CHANGED;
 
+enum BucketSplitEvent { APP_UPGRADE, BOOT_COMPLETE };
+
 // Converts a ProtoOutputStream to a StatsLogReport proto.
 StatsLogReport outputStreamToProto(ProtoOutputStream* proto);
 
diff --git a/cmds/statsd/tests/utils/MultiConditionTrigger_test.cpp b/cmds/statsd/tests/utils/MultiConditionTrigger_test.cpp
new file mode 100644
index 0000000..db402a0
--- /dev/null
+++ b/cmds/statsd/tests/utils/MultiConditionTrigger_test.cpp
@@ -0,0 +1,174 @@
+/*
+ * 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.
+ */
+#include "utils/MultiConditionTrigger.h"
+
+#include <gtest/gtest.h>
+
+#include <chrono>
+#include <set>
+#include <thread>
+#include <vector>
+
+#ifdef __ANDROID__
+
+using namespace std;
+using std::this_thread::sleep_for;
+
+namespace android {
+namespace os {
+namespace statsd {
+
+TEST(MultiConditionTrigger, TestMultipleConditions) {
+    int numConditions = 5;
+    string t1 = "t1", t2 = "t2", t3 = "t3", t4 = "t4", t5 = "t5";
+    set<string> conditionNames = {t1, t2, t3, t4, t5};
+
+    mutex lock;
+    condition_variable cv;
+    bool triggerCalled = false;
+
+    // Mark done as true and notify in the done.
+    MultiConditionTrigger trigger(conditionNames, [&lock, &cv, &triggerCalled] {
+        {
+            lock_guard lg(lock);
+            triggerCalled = true;
+        }
+        cv.notify_all();
+    });
+
+    vector<thread> threads;
+    vector<bool> done(numConditions, false);
+
+    int i = 0;
+    for (const string& conditionName : conditionNames) {
+        threads.emplace_back([&done, &conditionName, &trigger, i] {
+            sleep_for(chrono::milliseconds(3));
+            done[i] = true;
+            trigger.markComplete(conditionName);
+        });
+        i++;
+    }
+
+    unique_lock<mutex> unique_lk(lock);
+    cv.wait(unique_lk, [&triggerCalled] {
+        return triggerCalled;
+    });
+
+    for (i = 0; i < numConditions; i++) {
+        EXPECT_EQ(done[i], 1);
+    }
+
+    for (i = 0; i < numConditions; i++) {
+        threads[i].join();
+    }
+}
+
+TEST(MultiConditionTrigger, TestNoConditions) {
+    mutex lock;
+    condition_variable cv;
+    bool triggerCalled = false;
+
+    MultiConditionTrigger trigger({}, [&lock, &cv, &triggerCalled] {
+        {
+            lock_guard lg(lock);
+            triggerCalled = true;
+        }
+        cv.notify_all();
+    });
+
+    unique_lock<mutex> unique_lk(lock);
+    cv.wait(unique_lk, [&triggerCalled] { return triggerCalled; });
+    EXPECT_TRUE(triggerCalled);
+    // Ensure that trigger occurs immediately if no events need to be completed.
+}
+
+TEST(MultiConditionTrigger, TestMarkCompleteCalledBySameCondition) {
+    string t1 = "t1", t2 = "t2";
+    set<string> conditionNames = {t1, t2};
+
+    mutex lock;
+    condition_variable cv;
+    bool triggerCalled = false;
+
+    MultiConditionTrigger trigger(conditionNames, [&lock, &cv, &triggerCalled] {
+        {
+            lock_guard lg(lock);
+            triggerCalled = true;
+        }
+        cv.notify_all();
+    });
+
+    trigger.markComplete(t1);
+    trigger.markComplete(t1);
+
+    // Ensure that the trigger still hasn't fired.
+    {
+        lock_guard lg(lock);
+        EXPECT_FALSE(triggerCalled);
+    }
+
+    trigger.markComplete(t2);
+    unique_lock<mutex> unique_lk(lock);
+    cv.wait(unique_lk, [&triggerCalled] { return triggerCalled; });
+    EXPECT_TRUE(triggerCalled);
+}
+
+TEST(MultiConditionTrigger, TestTriggerOnlyCalledOnce) {
+    string t1 = "t1";
+    set<string> conditionNames = {t1};
+
+    mutex lock;
+    condition_variable cv;
+    bool triggerCalled = false;
+    int triggerCount = 0;
+
+    MultiConditionTrigger trigger(conditionNames, [&lock, &cv, &triggerCalled, &triggerCount] {
+        {
+            lock_guard lg(lock);
+            triggerCount++;
+            triggerCalled = true;
+        }
+        cv.notify_all();
+    });
+
+    trigger.markComplete(t1);
+
+    // Ensure that the trigger fired.
+    {
+        unique_lock<mutex> unique_lk(lock);
+        cv.wait(unique_lk, [&triggerCalled] { return triggerCalled; });
+        EXPECT_TRUE(triggerCalled);
+        EXPECT_EQ(triggerCount, 1);
+        triggerCalled = false;
+    }
+
+    trigger.markComplete(t1);
+
+    // Ensure that the trigger does not fire again.
+    {
+        unique_lock<mutex> unique_lk(lock);
+        cv.wait_for(unique_lk, chrono::milliseconds(5), [&triggerCalled] { return triggerCalled; });
+        EXPECT_FALSE(triggerCalled);
+        EXPECT_EQ(triggerCount, 1);
+    }
+}
+
+}  // namespace statsd
+}  // namespace os
+}  // namespace android
+#else
+GTEST_LOG_(INFO) << "This test does nothing.\n";
+#endif
diff --git a/cmds/statsd/tests/utils/NamedLatch_test.cpp b/cmds/statsd/tests/utils/NamedLatch_test.cpp
deleted file mode 100644
index de48a13..0000000
--- a/cmds/statsd/tests/utils/NamedLatch_test.cpp
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * 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.
- */
-#include "utils/NamedLatch.h"
-
-#include <gtest/gtest.h>
-
-#include <chrono>
-#include <set>
-#include <thread>
-#include <vector>
-
-#ifdef __ANDROID__
-
-using namespace std;
-using std::this_thread::sleep_for;
-
-namespace android {
-namespace os {
-namespace statsd {
-
-TEST(NamedLatchTest, TestWait) {
-    int numEvents = 5;
-    string t1 = "t1", t2 = "t2", t3 = "t3", t4 = "t4", t5 = "t5";
-    set<string> eventNames = {t1, t2, t3, t4, t5};
-
-    NamedLatch latch(eventNames);
-    vector<thread> threads;
-    vector<bool> done(numEvents, false);
-
-    int i = 0;
-    for (const string& eventName : eventNames) {
-        threads.emplace_back([&done, &eventName, &latch, i] {
-            sleep_for(chrono::milliseconds(3));
-            done[i] = true;
-            latch.countDown(eventName);
-        });
-        i++;
-    }
-
-    latch.wait();
-
-    for (i = 0; i < numEvents; i++) {
-        EXPECT_EQ(done[i], 1);
-    }
-
-    for (i = 0; i < numEvents; i++) {
-        threads[i].join();
-    }
-}
-
-TEST(NamedLatchTest, TestNoWorkers) {
-    NamedLatch latch({});
-    latch.wait();
-    // Ensure that latch does not wait if no events need to countDown.
-}
-
-TEST(NamedLatchTest, TestCountDownCalledBySameEventName) {
-    string t1 = "t1", t2 = "t2";
-    set<string> eventNames = {t1, t2};
-
-    NamedLatch latch(eventNames);
-
-    thread waiterThread([&latch] { latch.wait(); });
-
-    latch.countDown(t1);
-    latch.countDown(t1);
-
-    // Ensure that the latch's remaining threads still has t2.
-    latch.mMutex.lock();
-    ASSERT_EQ(latch.mRemainingEventNames.size(), 1);
-    EXPECT_NE(latch.mRemainingEventNames.find(t2), latch.mRemainingEventNames.end());
-    latch.mMutex.unlock();
-
-    latch.countDown(t2);
-    waiterThread.join();
-}
-
-}  // namespace statsd
-}  // namespace os
-}  // namespace android
-#else
-GTEST_LOG_(INFO) << "This test does nothing.\n";
-#endif
diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java
index 67334f5..18e5c3d 100644
--- a/core/java/android/accessibilityservice/AccessibilityService.java
+++ b/core/java/android/accessibilityservice/AccessibilityService.java
@@ -494,6 +494,13 @@
      */
     public static final int GLOBAL_ACTION_TAKE_SCREENSHOT = 9;
 
+    /**
+     * Action to send the KEYCODE_HEADSETHOOK KeyEvent, which is used to answer/hang up calls and
+     * play/stop media
+     * @hide
+     */
+    public static final int GLOBAL_ACTION_KEYCODE_HEADSETHOOK = 10;
+
     private static final String LOG_TAG = "AccessibilityService";
 
     /**
diff --git a/core/java/android/accessibilityservice/AccessibilityShortcutInfo.java b/core/java/android/accessibilityservice/AccessibilityShortcutInfo.java
index 9a3dad2..623734e 100644
--- a/core/java/android/accessibilityservice/AccessibilityShortcutInfo.java
+++ b/core/java/android/accessibilityservice/AccessibilityShortcutInfo.java
@@ -30,7 +30,6 @@
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
 import android.graphics.drawable.Drawable;
-import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.Xml;
 
@@ -153,7 +152,7 @@
                     com.android.internal.R.styleable.AccessibilityShortcutTarget_settingsActivity);
             asAttributes.recycle();
 
-            if (mDescriptionResId == 0 || mSummaryResId == 0) {
+            if ((mDescriptionResId == 0 && mHtmlDescriptionRes == 0) || mSummaryResId == 0) {
                 throw new XmlPullParserException("No description or summary in meta-data");
             }
         } catch (PackageManager.NameNotFoundException e) {
@@ -243,7 +242,10 @@
     public String loadHtmlDescription(@NonNull PackageManager packageManager) {
         final String htmlDescription = loadResourceString(packageManager, mActivityInfo,
                 mHtmlDescriptionRes);
-        return TextUtils.isEmpty(htmlDescription) ? null : getFilteredHtmlText(htmlDescription);
+        if (htmlDescription != null) {
+            return getFilteredHtmlText(htmlDescription);
+        }
+        return null;
     }
 
     /**
diff --git a/core/java/android/app/ActivityTaskManager.java b/core/java/android/app/ActivityTaskManager.java
index 1b941de..1cc63da 100644
--- a/core/java/android/app/ActivityTaskManager.java
+++ b/core/java/android/app/ActivityTaskManager.java
@@ -23,8 +23,10 @@
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Rect;
+import android.os.Build;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -430,4 +432,14 @@
             throw e.rethrowFromSystemServer();
         }
     }
+
+    /** Returns whether the current UI mode supports error dialogs (ANR, crash, etc). */
+    public static boolean currentUiModeSupportsErrorDialogs(@NonNull Context context) {
+        final Configuration config = context.getResources().getConfiguration();
+        int modeType = config.uiMode & Configuration.UI_MODE_TYPE_MASK;
+        return (modeType != Configuration.UI_MODE_TYPE_CAR
+                && !(modeType == Configuration.UI_MODE_TYPE_WATCH && Build.IS_USER)
+                && modeType != Configuration.UI_MODE_TYPE_TELEVISION
+                && modeType != Configuration.UI_MODE_TYPE_VR_HEADSET);
+    }
 }
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 21b56d3..696ee9f 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -375,7 +375,6 @@
     String mInstrumentedLibDir = null;
     boolean mSystemThread = false;
     boolean mSomeActivitiesChanged = false;
-    boolean mUpdatingSystemConfig = false;
     /* package */ boolean mHiddenApiWarningShown = false;
 
     // These can be accessed by multiple threads; mResourcesManager is the lock.
@@ -587,8 +586,11 @@
                     throw new IllegalStateException(
                             "Received config update for non-existing activity");
                 }
+                // Given alwaysReportChange=false because the configuration is from view root, the
+                // activity may not be able to handle the changes. In that case the activity will be
+                // relaunched immediately, then Activity#onConfigurationChanged shouldn't be called.
                 activity.mMainThread.handleActivityConfigurationChanged(token, overrideConfig,
-                        newDisplayId);
+                        newDisplayId, false /* alwaysReportChange */);
             };
         }
 
@@ -2029,12 +2031,7 @@
                     break;
                 }
                 case APPLICATION_INFO_CHANGED:
-                    mUpdatingSystemConfig = true;
-                    try {
-                        handleApplicationInfoChanged((ApplicationInfo) msg.obj);
-                    } finally {
-                        mUpdatingSystemConfig = false;
-                    }
+                    handleApplicationInfoChanged((ApplicationInfo) msg.obj);
                     break;
                 case RUN_ISOLATED_ENTRY_POINT:
                     handleRunIsolatedEntryPoint((String) ((SomeArgs) msg.obj).arg1,
@@ -4467,7 +4464,8 @@
         // simply finishing, and we are not starting another activity.
         if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) {
             if (r.newConfig != null) {
-                performConfigurationChangedForActivity(r, r.newConfig);
+                performConfigurationChangedForActivity(r, r.newConfig,
+                        false /* alwaysReportChange */);
                 if (DEBUG_CONFIGURATION) {
                     Slog.v(TAG, "Resuming activity " + r.activityInfo.name + " with newConfig "
                             + r.activity.mCurrentConfig);
@@ -4787,7 +4785,8 @@
                     }
                 }
                 if (r.newConfig != null) {
-                    performConfigurationChangedForActivity(r, r.newConfig);
+                    performConfigurationChangedForActivity(r, r.newConfig,
+                            false /* alwaysReportChange */);
                     if (DEBUG_CONFIGURATION) Slog.v(TAG, "Updating activity vis "
                             + r.activityInfo.name + " with new config "
                             + r.activity.mCurrentConfig);
@@ -5447,11 +5446,12 @@
      * @param r ActivityClientRecord representing the Activity.
      * @param newBaseConfig The new configuration to use. This may be augmented with
      *                      {@link ActivityClientRecord#overrideConfig}.
+     * @param alwaysReportChange If the configuration is changed, always report to activity.
      */
     private void performConfigurationChangedForActivity(ActivityClientRecord r,
-            Configuration newBaseConfig) {
-        performConfigurationChangedForActivity(r, newBaseConfig,
-                r.activity.getDisplayId(), false /* movedToDifferentDisplay */);
+            Configuration newBaseConfig, boolean alwaysReportChange) {
+        performConfigurationChangedForActivity(r, newBaseConfig, r.activity.getDisplayId(),
+                false /* movedToDifferentDisplay */, alwaysReportChange);
     }
 
     /**
@@ -5464,16 +5464,19 @@
      *                      {@link ActivityClientRecord#overrideConfig}.
      * @param displayId The id of the display where the Activity currently resides.
      * @param movedToDifferentDisplay Indicates if the activity was moved to different display.
+     * @param alwaysReportChange If the configuration is changed, always report to activity.
      * @return {@link Configuration} instance sent to client, null if not sent.
      */
     private Configuration performConfigurationChangedForActivity(ActivityClientRecord r,
-            Configuration newBaseConfig, int displayId, boolean movedToDifferentDisplay) {
+            Configuration newBaseConfig, int displayId, boolean movedToDifferentDisplay,
+            boolean alwaysReportChange) {
         r.tmpConfig.setTo(newBaseConfig);
         if (r.overrideConfig != null) {
             r.tmpConfig.updateFrom(r.overrideConfig);
         }
         final Configuration reportedConfig = performActivityConfigurationChanged(r.activity,
-                r.tmpConfig, r.overrideConfig, displayId, movedToDifferentDisplay);
+                r.tmpConfig, r.overrideConfig, displayId, movedToDifferentDisplay,
+                alwaysReportChange);
         freeTextLayoutCachesIfNeeded(r.activity.mCurrentConfig.diff(r.tmpConfig));
         return reportedConfig;
     }
@@ -5529,11 +5532,12 @@
      *                         ActivityManager.
      * @param displayId Id of the display where activity currently resides.
      * @param movedToDifferentDisplay Indicates if the activity was moved to different display.
+     * @param alwaysReportChange If the configuration is changed, always report to activity.
      * @return Configuration sent to client, null if no changes and not moved to different display.
      */
     private Configuration performActivityConfigurationChanged(Activity activity,
             Configuration newConfig, Configuration amOverrideConfig, int displayId,
-            boolean movedToDifferentDisplay) {
+            boolean movedToDifferentDisplay, boolean alwaysReportChange) {
         if (activity == null) {
             throw new IllegalArgumentException("No activity provided.");
         }
@@ -5556,7 +5560,7 @@
                 // Always send the task-level config changes. For system-level configuration, if
                 // this activity doesn't handle any of the config changes, then don't bother
                 // calling onConfigurationChanged as we're going to destroy it.
-                if (!mUpdatingSystemConfig
+                if (alwaysReportChange
                         || (~activity.mActivityInfo.getRealConfigChanged() & diff) == 0
                         || !REPORT_TO_ACTIVITY) {
                     shouldChangeConfig = true;
@@ -5638,12 +5642,7 @@
     public void handleConfigurationChanged(Configuration config) {
         Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "configChanged");
         mCurDefaultDisplayDpi = config.densityDpi;
-        mUpdatingSystemConfig = true;
-        try {
-            handleConfigurationChanged(config, null /* compat */);
-        } finally {
-            mUpdatingSystemConfig = false;
-        }
+        handleConfigurationChanged(config, null /* compat */);
         Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
     }
 
@@ -5725,7 +5724,7 @@
                     // config and avoid onConfigurationChanged if it hasn't changed.
                     Activity a = (Activity) cb;
                     performConfigurationChangedForActivity(mActivities.get(a.getActivityToken()),
-                            config);
+                            config, false /* alwaysReportChange */);
                 } else if (!equivalent) {
                     performConfigurationChanged(cb, config);
                 } else {
@@ -5832,16 +5831,28 @@
         }
     }
 
+    @Override
+    public void handleActivityConfigurationChanged(IBinder activityToken,
+            Configuration overrideConfig, int displayId) {
+        handleActivityConfigurationChanged(activityToken, overrideConfig, displayId,
+                // This is the only place that uses alwaysReportChange=true. The entry point should
+                // be from ActivityConfigurationChangeItem or MoveToDisplayItem, so the server side
+                // has confirmed the activity should handle the configuration instead of relaunch.
+                // If Activity#onConfigurationChanged is called unexpectedly, then we can know it is
+                // something wrong from server side.
+                true /* alwaysReportChange */);
+    }
+
     /**
      * Handle new activity configuration and/or move to a different display.
      * @param activityToken Target activity token.
      * @param overrideConfig Activity override config.
      * @param displayId Id of the display where activity was moved to, -1 if there was no move and
      *                  value didn't change.
+     * @param alwaysReportChange If the configuration is changed, always report to activity.
      */
-    @Override
-    public void handleActivityConfigurationChanged(IBinder activityToken,
-            Configuration overrideConfig, int displayId) {
+    void handleActivityConfigurationChanged(IBinder activityToken, Configuration overrideConfig,
+            int displayId, boolean alwaysReportChange) {
         ActivityClientRecord r = mActivities.get(activityToken);
         // Check input params.
         if (r == null || r.activity == null) {
@@ -5880,14 +5891,15 @@
                     + ", config=" + overrideConfig);
 
             final Configuration reportedConfig = performConfigurationChangedForActivity(r,
-                    mCompatConfiguration, displayId, true /* movedToDifferentDisplay */);
+                    mCompatConfiguration, displayId, true /* movedToDifferentDisplay */,
+                    alwaysReportChange);
             if (viewRoot != null) {
                 viewRoot.onMovedToDisplay(displayId, reportedConfig);
             }
         } else {
             if (DEBUG_CONFIGURATION) Slog.v(TAG, "Handle activity config changed: "
                     + r.activityInfo.name + ", config=" + overrideConfig);
-            performConfigurationChangedForActivity(r, mCompatConfiguration);
+            performConfigurationChangedForActivity(r, mCompatConfiguration, alwaysReportChange);
         }
         // Notify the ViewRootImpl instance about configuration changes. It may have initiated this
         // update to make sure that resources are updated before updating itself.
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 8edf03d..af02783 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -2301,11 +2301,11 @@
 
         priority = parcel.readInt();
 
-        category = parcel.readString();
+        category = parcel.readString8();
 
-        mGroupKey = parcel.readString();
+        mGroupKey = parcel.readString8();
 
-        mSortKey = parcel.readString();
+        mSortKey = parcel.readString8();
 
         extras = Bundle.setDefusable(parcel.readBundle(), true); // may be null
         fixDuplicateExtras();
@@ -2329,12 +2329,12 @@
         color = parcel.readInt();
 
         if (parcel.readInt() != 0) {
-            mChannelId = parcel.readString();
+            mChannelId = parcel.readString8();
         }
         mTimeout = parcel.readLong();
 
         if (parcel.readInt() != 0) {
-            mShortcutId = parcel.readString();
+            mShortcutId = parcel.readString8();
         }
 
         if (parcel.readInt() != 0) {
@@ -2766,11 +2766,11 @@
 
         parcel.writeInt(priority);
 
-        parcel.writeString(category);
+        parcel.writeString8(category);
 
-        parcel.writeString(mGroupKey);
+        parcel.writeString8(mGroupKey);
 
-        parcel.writeString(mSortKey);
+        parcel.writeString8(mSortKey);
 
         parcel.writeBundle(extras); // null ok
 
@@ -2803,7 +2803,7 @@
 
         if (mChannelId != null) {
             parcel.writeInt(1);
-            parcel.writeString(mChannelId);
+            parcel.writeString8(mChannelId);
         } else {
             parcel.writeInt(0);
         }
@@ -2811,7 +2811,7 @@
 
         if (mShortcutId != null) {
             parcel.writeInt(1);
-            parcel.writeString(mShortcutId);
+            parcel.writeString8(mShortcutId);
         } else {
             parcel.writeInt(0);
         }
@@ -8873,7 +8873,7 @@
             }
             mDesiredHeightResId = in.readInt();
             if (in.readInt() != 0) {
-                mShortcutId = in.readString();
+                mShortcutId = in.readString8();
             }
         }
 
@@ -9029,7 +9029,7 @@
             out.writeInt(mDesiredHeightResId);
             out.writeInt(TextUtils.isEmpty(mShortcutId) ? 0 : 1);
             if (!TextUtils.isEmpty(mShortcutId)) {
-                out.writeString(mShortcutId);
+                out.writeString8(mShortcutId);
             }
         }
 
diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java
index 2feb927..9f8d3c4 100644
--- a/core/java/android/app/NotificationChannel.java
+++ b/core/java/android/app/NotificationChannel.java
@@ -102,7 +102,7 @@
     private static final String ATT_FG_SERVICE_SHOWN = "fgservice";
     private static final String ATT_GROUP = "group";
     private static final String ATT_BLOCKABLE_SYSTEM = "blockable_system";
-    private static final String ATT_ALLOW_BUBBLE = "allow_bubble";
+    private static final String ATT_ALLOW_BUBBLE = "allow_bubbles";
     private static final String ATT_ORIG_IMP = "orig_imp";
     private static final String ATT_PARENT_CHANNEL = "parent";
     private static final String ATT_CONVERSATION_ID = "conv_id";
@@ -168,7 +168,12 @@
             NotificationManager.IMPORTANCE_UNSPECIFIED;
     private static final boolean DEFAULT_DELETED = false;
     private static final boolean DEFAULT_SHOW_BADGE = true;
-    private static final boolean DEFAULT_ALLOW_BUBBLE = false;
+    /**
+     * @hide
+     */
+    public static final int DEFAULT_ALLOW_BUBBLE = -1;
+    private static final int ALLOW_BUBBLE_ON = 1;
+    private static final int ALLOW_BUBBLE_OFF = 0;
 
     @UnsupportedAppUsage
     private String mId;
@@ -193,7 +198,7 @@
     private AudioAttributes mAudioAttributes = Notification.AUDIO_ATTRIBUTES_DEFAULT;
     // If this is a blockable system notification channel.
     private boolean mBlockableSystem = false;
-    private boolean mAllowBubbles = DEFAULT_ALLOW_BUBBLE;
+    private int mAllowBubbles = DEFAULT_ALLOW_BUBBLE;
     private boolean mImportanceLockedByOEM;
     private boolean mImportanceLockedDefaultApp;
     private String mParentId = null;
@@ -261,7 +266,7 @@
         mAudioAttributes = in.readInt() > 0 ? AudioAttributes.CREATOR.createFromParcel(in) : null;
         mLightColor = in.readInt();
         mBlockableSystem = in.readBoolean();
-        mAllowBubbles = in.readBoolean();
+        mAllowBubbles = in.readInt();
         mImportanceLockedByOEM = in.readBoolean();
         mOriginalImportance = in.readInt();
         mParentId = in.readString();
@@ -320,7 +325,7 @@
         }
         dest.writeInt(mLightColor);
         dest.writeBoolean(mBlockableSystem);
-        dest.writeBoolean(mAllowBubbles);
+        dest.writeInt(mAllowBubbles);
         dest.writeBoolean(mImportanceLockedByOEM);
         dest.writeInt(mOriginalImportance);
         dest.writeString(mParentId);
@@ -550,7 +555,14 @@
      * @see Notification#getBubbleMetadata()
      */
     public void setAllowBubbles(boolean allowBubbles) {
-        mAllowBubbles = allowBubbles;
+        mAllowBubbles = allowBubbles ? ALLOW_BUBBLE_ON : ALLOW_BUBBLE_OFF;
+    }
+
+    /**
+     * @hide
+     */
+    public void setAllowBubbles(int allowed) {
+        mAllowBubbles = allowed;
     }
 
     /**
@@ -701,6 +713,13 @@
      * @see Notification#getBubbleMetadata()
      */
     public boolean canBubble() {
+        return mAllowBubbles == ALLOW_BUBBLE_ON;
+    }
+
+    /**
+     * @hide
+     */
+    public int getAllowBubbles() {
         return mAllowBubbles;
     }
 
@@ -872,7 +891,7 @@
         lockFields(safeInt(parser, ATT_USER_LOCKED, 0));
         setFgServiceShown(safeBool(parser, ATT_FG_SERVICE_SHOWN, false));
         setBlockable(safeBool(parser, ATT_BLOCKABLE_SYSTEM, false));
-        setAllowBubbles(safeBool(parser, ATT_ALLOW_BUBBLE, DEFAULT_ALLOW_BUBBLE));
+        setAllowBubbles(safeInt(parser, ATT_ALLOW_BUBBLE, DEFAULT_ALLOW_BUBBLE));
         setOriginalImportance(safeInt(parser, ATT_ORIG_IMP, DEFAULT_IMPORTANCE));
         setConversationId(parser.getAttributeValue(null, ATT_PARENT_CHANNEL),
                 parser.getAttributeValue(null, ATT_CONVERSATION_ID));
@@ -996,8 +1015,8 @@
         if (isBlockable()) {
             out.attribute(null, ATT_BLOCKABLE_SYSTEM, Boolean.toString(isBlockable()));
         }
-        if (canBubble() != DEFAULT_ALLOW_BUBBLE) {
-            out.attribute(null, ATT_ALLOW_BUBBLE, Boolean.toString(canBubble()));
+        if (getAllowBubbles() != DEFAULT_ALLOW_BUBBLE) {
+            out.attribute(null, ATT_ALLOW_BUBBLE, Integer.toString(getAllowBubbles()));
         }
         if (getOriginalImportance() != DEFAULT_IMPORTANCE) {
             out.attribute(null, ATT_ORIG_IMP, Integer.toString(getOriginalImportance()));
@@ -1059,7 +1078,7 @@
         record.put(ATT_DELETED, Boolean.toString(isDeleted()));
         record.put(ATT_GROUP, getGroup());
         record.put(ATT_BLOCKABLE_SYSTEM, isBlockable());
-        record.put(ATT_ALLOW_BUBBLE, canBubble());
+        record.put(ATT_ALLOW_BUBBLE, getAllowBubbles());
         // TODO: original importance
         return record;
     }
diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java
index 0173731..c7a2a1e 100644
--- a/core/java/android/app/TaskInfo.java
+++ b/core/java/android/app/TaskInfo.java
@@ -20,6 +20,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.TestApi;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.ComponentName;
 import android.content.Intent;
@@ -196,6 +197,20 @@
         return resizeMode != RESIZE_MODE_UNRESIZEABLE;
     }
 
+    /** @hide */
+    @NonNull
+    @TestApi
+    public WindowContainerToken getToken() {
+        return token;
+    }
+
+    /** @hide */
+    @NonNull
+    @TestApi
+    public Configuration getConfiguration() {
+        return configuration;
+    }
+
     /**
      * Reads the TaskInfo from a parcel.
      */
diff --git a/core/java/android/content/ClipData.java b/core/java/android/content/ClipData.java
index 9c806fa..1923bf3 100644
--- a/core/java/android/content/ClipData.java
+++ b/core/java/android/content/ClipData.java
@@ -1206,7 +1206,7 @@
             }
         } else {
             dest.writeInt(PARCEL_TYPE_STRING);
-            dest.writeString(text);
+            dest.writeString8(text);
         }
     }
 
@@ -1215,7 +1215,7 @@
      */
     private static String readHtmlTextFromParcel(Parcel in) {
         if (in.readInt() == PARCEL_TYPE_STRING) {
-            return in.readString();
+            return in.readString8();
         }
         ParcelFileDescriptor pfd =
                 in.readParcelable(ParcelFileDescriptor.class.getClassLoader());
diff --git a/core/java/android/content/ContentProviderOperation.java b/core/java/android/content/ContentProviderOperation.java
index 494d2ae..1fb426e 100644
--- a/core/java/android/content/ContentProviderOperation.java
+++ b/core/java/android/content/ContentProviderOperation.java
@@ -91,8 +91,8 @@
     private ContentProviderOperation(Parcel source) {
         mType = source.readInt();
         mUri = Uri.CREATOR.createFromParcel(source);
-        mMethod = source.readInt() != 0 ? source.readString() : null;
-        mArg = source.readInt() != 0 ? source.readString() : null;
+        mMethod = source.readInt() != 0 ? source.readString8() : null;
+        mArg = source.readInt() != 0 ? source.readString8() : null;
         final int valuesSize = source.readInt();
         if (valuesSize != -1) {
             mValues = new ArrayMap<>(valuesSize);
@@ -107,7 +107,7 @@
         } else {
             mExtras = null;
         }
-        mSelection = source.readInt() != 0 ? source.readString() : null;
+        mSelection = source.readInt() != 0 ? source.readString8() : null;
         mSelectionArgs = source.readSparseArray(null);
         mExpectedCount = source.readInt() != 0 ? source.readInt() : null;
         mYieldAllowed = source.readInt() != 0;
@@ -135,13 +135,13 @@
         Uri.writeToParcel(dest, mUri);
         if (mMethod != null) {
             dest.writeInt(1);
-            dest.writeString(mMethod);
+            dest.writeString8(mMethod);
         } else {
             dest.writeInt(0);
         }
         if (mArg != null) {
             dest.writeInt(1);
-            dest.writeString(mArg);
+            dest.writeString8(mArg);
         } else {
             dest.writeInt(0);
         }
@@ -159,7 +159,7 @@
         }
         if (mSelection != null) {
             dest.writeInt(1);
-            dest.writeString(mSelection);
+            dest.writeString8(mSelection);
         } else {
             dest.writeInt(0);
         }
@@ -591,7 +591,7 @@
         public BackReference(Parcel src) {
             this.fromIndex = src.readInt();
             if (src.readInt() != 0) {
-                this.fromKey = src.readString();
+                this.fromKey = src.readString8();
             } else {
                 this.fromKey = null;
             }
@@ -620,7 +620,7 @@
             dest.writeInt(fromIndex);
             if (fromKey != null) {
                 dest.writeInt(1);
-                dest.writeString(fromKey);
+                dest.writeString8(fromKey);
             } else {
                 dest.writeInt(0);
             }
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index b1d6c83..def150a 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -888,8 +888,8 @@
 
                 public ShortcutIconResource createFromParcel(Parcel source) {
                     ShortcutIconResource icon = new ShortcutIconResource();
-                    icon.packageName = source.readString();
-                    icon.resourceName = source.readString();
+                    icon.packageName = source.readString8();
+                    icon.resourceName = source.readString8();
                     return icon;
                 }
 
@@ -906,8 +906,8 @@
         }
 
         public void writeToParcel(Parcel dest, int flags) {
-            dest.writeString(packageName);
-            dest.writeString(resourceName);
+            dest.writeString8(packageName);
+            dest.writeString8(resourceName);
         }
 
         @Override
@@ -10807,12 +10807,12 @@
     }
 
     public void writeToParcel(Parcel out, int flags) {
-        out.writeString(mAction);
+        out.writeString8(mAction);
         Uri.writeToParcel(out, mData);
-        out.writeString(mType);
-        out.writeString(mIdentifier);
+        out.writeString8(mType);
+        out.writeString8(mIdentifier);
         out.writeInt(mFlags);
-        out.writeString(mPackage);
+        out.writeString8(mPackage);
         ComponentName.writeToParcel(mComponent, out);
 
         if (mSourceBounds != null) {
@@ -10826,7 +10826,7 @@
             final int N = mCategories.size();
             out.writeInt(N);
             for (int i=0; i<N; i++) {
-                out.writeString(mCategories.valueAt(i));
+                out.writeString8(mCategories.valueAt(i));
             }
         } else {
             out.writeInt(0);
@@ -10865,12 +10865,12 @@
     }
 
     public void readFromParcel(Parcel in) {
-        setAction(in.readString());
+        setAction(in.readString8());
         mData = Uri.CREATOR.createFromParcel(in);
-        mType = in.readString();
-        mIdentifier = in.readString();
+        mType = in.readString8();
+        mIdentifier = in.readString8();
         mFlags = in.readInt();
-        mPackage = in.readString();
+        mPackage = in.readString8();
         mComponent = ComponentName.readFromParcel(in);
 
         if (in.readInt() != 0) {
@@ -10882,7 +10882,7 @@
             mCategories = new ArraySet<String>();
             int i;
             for (i=0; i<N; i++) {
-                mCategories.add(in.readString().intern());
+                mCategories.add(in.readString8().intern());
             }
         } else {
             mCategories = null;
diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java
index f25ce76..b1f8869 100644
--- a/core/java/android/content/pm/ActivityInfo.java
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -1206,17 +1206,17 @@
         dest.writeInt(theme);
         dest.writeInt(launchMode);
         dest.writeInt(documentLaunchMode);
-        dest.writeString(permission);
-        dest.writeString(taskAffinity);
-        dest.writeString(targetActivity);
-        dest.writeString(launchToken);
+        dest.writeString8(permission);
+        dest.writeString8(taskAffinity);
+        dest.writeString8(targetActivity);
+        dest.writeString8(launchToken);
         dest.writeInt(flags);
         dest.writeInt(privateFlags);
         dest.writeInt(screenOrientation);
         dest.writeInt(configChanges);
         dest.writeInt(softInputMode);
         dest.writeInt(uiOptions);
-        dest.writeString(parentActivityName);
+        dest.writeString8(parentActivityName);
         dest.writeInt(persistableMode);
         dest.writeInt(maxRecents);
         dest.writeInt(lockTaskLaunchMode);
@@ -1227,7 +1227,7 @@
             dest.writeInt(0);
         }
         dest.writeInt(resizeMode);
-        dest.writeString(requestedVrComponent);
+        dest.writeString8(requestedVrComponent);
         dest.writeInt(rotationAnimation);
         dest.writeInt(colorMode);
         dest.writeFloat(maxAspectRatio);
@@ -1327,17 +1327,17 @@
         theme = source.readInt();
         launchMode = source.readInt();
         documentLaunchMode = source.readInt();
-        permission = source.readString();
-        taskAffinity = source.readString();
-        targetActivity = source.readString();
-        launchToken = source.readString();
+        permission = source.readString8();
+        taskAffinity = source.readString8();
+        targetActivity = source.readString8();
+        launchToken = source.readString8();
         flags = source.readInt();
         privateFlags = source.readInt();
         screenOrientation = source.readInt();
         configChanges = source.readInt();
         softInputMode = source.readInt();
         uiOptions = source.readInt();
-        parentActivityName = source.readString();
+        parentActivityName = source.readString8();
         persistableMode = source.readInt();
         maxRecents = source.readInt();
         lockTaskLaunchMode = source.readInt();
@@ -1345,7 +1345,7 @@
             windowLayout = new WindowLayout(source);
         }
         resizeMode = source.readInt();
-        requestedVrComponent = source.readString();
+        requestedVrComponent = source.readString8();
         rotationAnimation = source.readInt();
         colorMode = source.readInt();
         maxAspectRatio = source.readFloat();
@@ -1386,7 +1386,7 @@
             gravity = source.readInt();
             minWidth = source.readInt();
             minHeight = source.readInt();
-            windowLayoutAffinity = source.readString();
+            windowLayoutAffinity = source.readString8();
         }
 
         /**
@@ -1476,7 +1476,7 @@
             dest.writeInt(gravity);
             dest.writeInt(minWidth);
             dest.writeInt(minHeight);
-            dest.writeString(windowLayoutAffinity);
+            dest.writeString8(windowLayoutAffinity);
         }
     }
 }
diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java
index b6706011..b37521b 100644
--- a/core/java/android/content/pm/ApplicationInfo.java
+++ b/core/java/android/content/pm/ApplicationInfo.java
@@ -1704,10 +1704,10 @@
             return;
         }
         super.writeToParcel(dest, parcelableFlags);
-        dest.writeString(taskAffinity);
-        dest.writeString(permission);
-        dest.writeString(processName);
-        dest.writeString(className);
+        dest.writeString8(taskAffinity);
+        dest.writeString8(permission);
+        dest.writeString8(processName);
+        dest.writeString8(className);
         dest.writeInt(theme);
         dest.writeInt(flags);
         dest.writeInt(privateFlags);
@@ -1721,28 +1721,28 @@
         } else {
             dest.writeInt(0);
         }
-        dest.writeString(scanSourceDir);
-        dest.writeString(scanPublicSourceDir);
-        dest.writeString(sourceDir);
-        dest.writeString(publicSourceDir);
+        dest.writeString8(scanSourceDir);
+        dest.writeString8(scanPublicSourceDir);
+        dest.writeString8(sourceDir);
+        dest.writeString8(publicSourceDir);
         dest.writeStringArray(splitNames);
         dest.writeStringArray(splitSourceDirs);
         dest.writeStringArray(splitPublicSourceDirs);
         dest.writeSparseArray((SparseArray) splitDependencies);
-        dest.writeString(nativeLibraryDir);
-        dest.writeString(secondaryNativeLibraryDir);
-        dest.writeString(nativeLibraryRootDir);
+        dest.writeString8(nativeLibraryDir);
+        dest.writeString8(secondaryNativeLibraryDir);
+        dest.writeString8(nativeLibraryRootDir);
         dest.writeInt(nativeLibraryRootRequiresIsa ? 1 : 0);
-        dest.writeString(primaryCpuAbi);
-        dest.writeString(secondaryCpuAbi);
+        dest.writeString8(primaryCpuAbi);
+        dest.writeString8(secondaryCpuAbi);
         dest.writeStringArray(resourceDirs);
-        dest.writeString(seInfo);
-        dest.writeString(seInfoUser);
+        dest.writeString8(seInfo);
+        dest.writeString8(seInfoUser);
         dest.writeStringArray(sharedLibraryFiles);
         dest.writeTypedList(sharedLibraryInfos);
-        dest.writeString(dataDir);
-        dest.writeString(deviceProtectedDataDir);
-        dest.writeString(credentialProtectedDataDir);
+        dest.writeString8(dataDir);
+        dest.writeString8(deviceProtectedDataDir);
+        dest.writeString8(credentialProtectedDataDir);
         dest.writeInt(uid);
         dest.writeInt(minSdkVersion);
         dest.writeInt(targetSdkVersion);
@@ -1750,8 +1750,8 @@
         dest.writeInt(enabled ? 1 : 0);
         dest.writeInt(enabledSetting);
         dest.writeInt(installLocation);
-        dest.writeString(manageSpaceActivityName);
-        dest.writeString(backupAgentName);
+        dest.writeString8(manageSpaceActivityName);
+        dest.writeString8(backupAgentName);
         dest.writeInt(descriptionRes);
         dest.writeInt(uiOptions);
         dest.writeInt(fullBackupContent);
@@ -1759,16 +1759,16 @@
         dest.writeInt(networkSecurityConfigRes);
         dest.writeInt(category);
         dest.writeInt(targetSandboxVersion);
-        dest.writeString(classLoaderName);
+        dest.writeString8(classLoaderName);
         dest.writeStringArray(splitClassLoaderNames);
         dest.writeInt(compileSdkVersion);
-        dest.writeString(compileSdkVersionCodename);
-        dest.writeString(appComponentFactory);
+        dest.writeString8(compileSdkVersionCodename);
+        dest.writeString8(appComponentFactory);
         dest.writeInt(iconRes);
         dest.writeInt(roundIconRes);
         dest.writeInt(mHiddenApiPolicy);
         dest.writeInt(hiddenUntilInstalled ? 1 : 0);
-        dest.writeString(zygotePreloadName);
+        dest.writeString8(zygotePreloadName);
         dest.writeInt(gwpAsanMode);
     }
 
@@ -1788,10 +1788,10 @@
     @SuppressWarnings("unchecked")
     private ApplicationInfo(Parcel source) {
         super(source);
-        taskAffinity = source.readString();
-        permission = source.readString();
-        processName = source.readString();
-        className = source.readString();
+        taskAffinity = source.readString8();
+        permission = source.readString8();
+        processName = source.readString8();
+        className = source.readString8();
         theme = source.readInt();
         flags = source.readInt();
         privateFlags = source.readInt();
@@ -1802,28 +1802,28 @@
             storageUuid = new UUID(source.readLong(), source.readLong());
             volumeUuid = StorageManager.convert(storageUuid);
         }
-        scanSourceDir = source.readString();
-        scanPublicSourceDir = source.readString();
-        sourceDir = source.readString();
-        publicSourceDir = source.readString();
+        scanSourceDir = source.readString8();
+        scanPublicSourceDir = source.readString8();
+        sourceDir = source.readString8();
+        publicSourceDir = source.readString8();
         splitNames = source.readStringArray();
         splitSourceDirs = source.readStringArray();
         splitPublicSourceDirs = source.readStringArray();
         splitDependencies = source.readSparseArray(null);
-        nativeLibraryDir = source.readString();
-        secondaryNativeLibraryDir = source.readString();
-        nativeLibraryRootDir = source.readString();
+        nativeLibraryDir = source.readString8();
+        secondaryNativeLibraryDir = source.readString8();
+        nativeLibraryRootDir = source.readString8();
         nativeLibraryRootRequiresIsa = source.readInt() != 0;
-        primaryCpuAbi = source.readString();
-        secondaryCpuAbi = source.readString();
+        primaryCpuAbi = source.readString8();
+        secondaryCpuAbi = source.readString8();
         resourceDirs = source.readStringArray();
-        seInfo = source.readString();
-        seInfoUser = source.readString();
+        seInfo = source.readString8();
+        seInfoUser = source.readString8();
         sharedLibraryFiles = source.readStringArray();
         sharedLibraryInfos = source.createTypedArrayList(SharedLibraryInfo.CREATOR);
-        dataDir = source.readString();
-        deviceProtectedDataDir = source.readString();
-        credentialProtectedDataDir = source.readString();
+        dataDir = source.readString8();
+        deviceProtectedDataDir = source.readString8();
+        credentialProtectedDataDir = source.readString8();
         uid = source.readInt();
         minSdkVersion = source.readInt();
         targetSdkVersion = source.readInt();
@@ -1831,8 +1831,8 @@
         enabled = source.readInt() != 0;
         enabledSetting = source.readInt();
         installLocation = source.readInt();
-        manageSpaceActivityName = source.readString();
-        backupAgentName = source.readString();
+        manageSpaceActivityName = source.readString8();
+        backupAgentName = source.readString8();
         descriptionRes = source.readInt();
         uiOptions = source.readInt();
         fullBackupContent = source.readInt();
@@ -1840,16 +1840,16 @@
         networkSecurityConfigRes = source.readInt();
         category = source.readInt();
         targetSandboxVersion = source.readInt();
-        classLoaderName = source.readString();
+        classLoaderName = source.readString8();
         splitClassLoaderNames = source.readStringArray();
         compileSdkVersion = source.readInt();
-        compileSdkVersionCodename = source.readString();
-        appComponentFactory = source.readString();
+        compileSdkVersionCodename = source.readString8();
+        appComponentFactory = source.readString8();
         iconRes = source.readInt();
         roundIconRes = source.readInt();
         mHiddenApiPolicy = source.readInt();
         hiddenUntilInstalled = source.readInt() != 0;
-        zygotePreloadName = source.readString();
+        zygotePreloadName = source.readString8();
         gwpAsanMode = source.readInt();
     }
 
diff --git a/core/java/android/content/pm/ComponentInfo.java b/core/java/android/content/pm/ComponentInfo.java
index 362098c..628bcd7 100644
--- a/core/java/android/content/pm/ComponentInfo.java
+++ b/core/java/android/content/pm/ComponentInfo.java
@@ -197,8 +197,8 @@
     public void writeToParcel(Parcel dest, int parcelableFlags) {
         super.writeToParcel(dest, parcelableFlags);
         applicationInfo.writeToParcel(dest, parcelableFlags);
-        dest.writeString(processName);
-        dest.writeString(splitName);
+        dest.writeString8(processName);
+        dest.writeString8(splitName);
         dest.writeInt(descriptionRes);
         dest.writeInt(enabled ? 1 : 0);
         dest.writeInt(exported ? 1 : 0);
@@ -208,8 +208,8 @@
     protected ComponentInfo(Parcel source) {
         super(source);
         applicationInfo = ApplicationInfo.CREATOR.createFromParcel(source);
-        processName = source.readString();
-        splitName = source.readString();
+        processName = source.readString8();
+        splitName = source.readString8();
         descriptionRes = source.readInt();
         enabled = (source.readInt() != 0);
         exported = (source.readInt() != 0);
diff --git a/core/java/android/content/pm/FeatureInfo.java b/core/java/android/content/pm/FeatureInfo.java
index 9f3ab77..89269e0 100644
--- a/core/java/android/content/pm/FeatureInfo.java
+++ b/core/java/android/content/pm/FeatureInfo.java
@@ -108,7 +108,7 @@
 
     @Override
     public void writeToParcel(Parcel dest, int parcelableFlags) {
-        dest.writeString(name);
+        dest.writeString8(name);
         dest.writeInt(version);
         dest.writeInt(reqGlEsVersion);
         dest.writeInt(flags);
@@ -138,7 +138,7 @@
     };
 
     private FeatureInfo(Parcel source) {
-        name = source.readString();
+        name = source.readString8();
         version = source.readInt();
         reqGlEsVersion = source.readInt();
         flags = source.readInt();
diff --git a/core/java/android/content/pm/IDataLoaderStatusListener.aidl b/core/java/android/content/pm/IDataLoaderStatusListener.aidl
index 9819b5d..ffe8b18 100644
--- a/core/java/android/content/pm/IDataLoaderStatusListener.aidl
+++ b/core/java/android/content/pm/IDataLoaderStatusListener.aidl
@@ -21,16 +21,29 @@
  * @hide
  */
 oneway interface IDataLoaderStatusListener {
-    /** Data loader status */
+    /** When this status is returned from DataLoader, it means that the DataLoader
+    *   process is running, bound to and has handled onCreate(). */
     const int DATA_LOADER_CREATED = 0;
+    /** Listener will receive this status when the DataLoader process died,
+    *   binder disconnected or class destroyed. */
     const int DATA_LOADER_DESTROYED = 1;
 
+    /** DataLoader can receive missing pages and read pages notifications,
+     *  and ready to provide data. */
     const int DATA_LOADER_STARTED = 2;
+    /** DataLoader no longer ready to provide data and is not receiving
+    *   any notifications from IncFS. */
     const int DATA_LOADER_STOPPED = 3;
 
+    /** DataLoader streamed everything necessary to continue installation. */
     const int DATA_LOADER_IMAGE_READY = 4;
+    /** Installation can't continue as DataLoader failed to stream necessary data. */
     const int DATA_LOADER_IMAGE_NOT_READY = 5;
 
+    /** DataLoader reports that this instance is invalid and can never be restored.
+    *   Warning: this is a terminal status that data loader should use carefully and
+    *            the system should almost never use - e.g. only if all recovery attempts
+    *            fail and all retry limits are exceeded. */
     const int DATA_LOADER_UNRECOVERABLE = 6;
 
     /** Data loader status callback */
diff --git a/core/java/android/content/pm/InstrumentationInfo.java b/core/java/android/content/pm/InstrumentationInfo.java
index 574a1ee..745a6c1 100644
--- a/core/java/android/content/pm/InstrumentationInfo.java
+++ b/core/java/android/content/pm/InstrumentationInfo.java
@@ -157,21 +157,21 @@
 
     public void writeToParcel(Parcel dest, int parcelableFlags) {
         super.writeToParcel(dest, parcelableFlags);
-        dest.writeString(targetPackage);
-        dest.writeString(targetProcesses);
-        dest.writeString(sourceDir);
-        dest.writeString(publicSourceDir);
+        dest.writeString8(targetPackage);
+        dest.writeString8(targetProcesses);
+        dest.writeString8(sourceDir);
+        dest.writeString8(publicSourceDir);
         dest.writeStringArray(splitNames);
         dest.writeStringArray(splitSourceDirs);
         dest.writeStringArray(splitPublicSourceDirs);
         dest.writeSparseArray((SparseArray) splitDependencies);
-        dest.writeString(dataDir);
-        dest.writeString(deviceProtectedDataDir);
-        dest.writeString(credentialProtectedDataDir);
-        dest.writeString(primaryCpuAbi);
-        dest.writeString(secondaryCpuAbi);
-        dest.writeString(nativeLibraryDir);
-        dest.writeString(secondaryNativeLibraryDir);
+        dest.writeString8(dataDir);
+        dest.writeString8(deviceProtectedDataDir);
+        dest.writeString8(credentialProtectedDataDir);
+        dest.writeString8(primaryCpuAbi);
+        dest.writeString8(secondaryCpuAbi);
+        dest.writeString8(nativeLibraryDir);
+        dest.writeString8(secondaryNativeLibraryDir);
         dest.writeInt((handleProfiling == false) ? 0 : 1);
         dest.writeInt((functionalTest == false) ? 0 : 1);
     }
@@ -189,21 +189,21 @@
     @SuppressWarnings("unchecked")
     private InstrumentationInfo(Parcel source) {
         super(source);
-        targetPackage = source.readString();
-        targetProcesses = source.readString();
-        sourceDir = source.readString();
-        publicSourceDir = source.readString();
+        targetPackage = source.readString8();
+        targetProcesses = source.readString8();
+        sourceDir = source.readString8();
+        publicSourceDir = source.readString8();
         splitNames = source.readStringArray();
         splitSourceDirs = source.readStringArray();
         splitPublicSourceDirs = source.readStringArray();
         splitDependencies = source.readSparseArray(null);
-        dataDir = source.readString();
-        deviceProtectedDataDir = source.readString();
-        credentialProtectedDataDir = source.readString();
-        primaryCpuAbi = source.readString();
-        secondaryCpuAbi = source.readString();
-        nativeLibraryDir = source.readString();
-        secondaryNativeLibraryDir = source.readString();
+        dataDir = source.readString8();
+        deviceProtectedDataDir = source.readString8();
+        credentialProtectedDataDir = source.readString8();
+        primaryCpuAbi = source.readString8();
+        secondaryCpuAbi = source.readString8();
+        nativeLibraryDir = source.readString8();
+        secondaryNativeLibraryDir = source.readString8();
         handleProfiling = source.readInt() != 0;
         functionalTest = source.readInt() != 0;
     }
diff --git a/core/java/android/content/pm/PackageInfo.java b/core/java/android/content/pm/PackageInfo.java
index 85c698f..bb56ef7 100644
--- a/core/java/android/content/pm/PackageInfo.java
+++ b/core/java/android/content/pm/PackageInfo.java
@@ -441,14 +441,14 @@
     public void writeToParcel(Parcel dest, int parcelableFlags) {
         // Allow ApplicationInfo to be squashed.
         final boolean prevAllowSquashing = dest.allowSquashing();
-        dest.writeString(packageName);
+        dest.writeString8(packageName);
         dest.writeStringArray(splitNames);
         dest.writeInt(versionCode);
         dest.writeInt(versionCodeMajor);
-        dest.writeString(versionName);
+        dest.writeString8(versionName);
         dest.writeInt(baseRevisionCode);
         dest.writeIntArray(splitRevisionCodes);
-        dest.writeString(sharedUserId);
+        dest.writeString8(sharedUserId);
         dest.writeInt(sharedUserLabel);
         if (applicationInfo != null) {
             dest.writeInt(1);
@@ -475,14 +475,14 @@
         dest.writeInt(isStub ? 1 : 0);
         dest.writeInt(coreApp ? 1 : 0);
         dest.writeInt(requiredForAllUsers ? 1 : 0);
-        dest.writeString(restrictedAccountType);
-        dest.writeString(requiredAccountType);
-        dest.writeString(overlayTarget);
-        dest.writeString(overlayCategory);
+        dest.writeString8(restrictedAccountType);
+        dest.writeString8(requiredAccountType);
+        dest.writeString8(overlayTarget);
+        dest.writeString8(overlayCategory);
         dest.writeInt(overlayPriority);
         dest.writeBoolean(mOverlayIsStatic);
         dest.writeInt(compileSdkVersion);
-        dest.writeString(compileSdkVersionCodename);
+        dest.writeString8(compileSdkVersionCodename);
         if (signingInfo != null) {
             dest.writeInt(1);
             signingInfo.writeToParcel(dest, parcelableFlags);
@@ -508,14 +508,14 @@
 
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
     private PackageInfo(Parcel source) {
-        packageName = source.readString();
+        packageName = source.readString8();
         splitNames = source.createStringArray();
         versionCode = source.readInt();
         versionCodeMajor = source.readInt();
-        versionName = source.readString();
+        versionName = source.readString8();
         baseRevisionCode = source.readInt();
         splitRevisionCodes = source.createIntArray();
-        sharedUserId = source.readString();
+        sharedUserId = source.readString8();
         sharedUserLabel = source.readInt();
         int hasApp = source.readInt();
         if (hasApp != 0) {
@@ -540,14 +540,14 @@
         isStub = source.readInt() != 0;
         coreApp = source.readInt() != 0;
         requiredForAllUsers = source.readInt() != 0;
-        restrictedAccountType = source.readString();
-        requiredAccountType = source.readString();
-        overlayTarget = source.readString();
-        overlayCategory = source.readString();
+        restrictedAccountType = source.readString8();
+        requiredAccountType = source.readString8();
+        overlayTarget = source.readString8();
+        overlayCategory = source.readString8();
         overlayPriority = source.readInt();
         mOverlayIsStatic = source.readBoolean();
         compileSdkVersion = source.readInt();
-        compileSdkVersionCodename = source.readString();
+        compileSdkVersionCodename = source.readString8();
         int hasSigningInfo = source.readInt();
         if (hasSigningInfo != 0) {
             signingInfo = SigningInfo.CREATOR.createFromParcel(source);
diff --git a/core/java/android/content/pm/PackageItemInfo.java b/core/java/android/content/pm/PackageItemInfo.java
index 7fd5531..d41ace5 100644
--- a/core/java/android/content/pm/PackageItemInfo.java
+++ b/core/java/android/content/pm/PackageItemInfo.java
@@ -422,8 +422,8 @@
     }
 
     public void writeToParcel(Parcel dest, int parcelableFlags) {
-        dest.writeString(name);
-        dest.writeString(packageName);
+        dest.writeString8(name);
+        dest.writeString8(packageName);
         dest.writeInt(labelRes);
         TextUtils.writeToParcel(nonLocalizedLabel, dest, parcelableFlags);
         dest.writeInt(icon);
@@ -452,8 +452,8 @@
     }
 
     protected PackageItemInfo(Parcel source) {
-        name = source.readString();
-        packageName = source.readString();
+        name = source.readString8();
+        packageName = source.readString8();
         labelRes = source.readInt();
         nonLocalizedLabel
                 = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java
index 1dadbda..8a57f82 100644
--- a/core/java/android/content/pm/PackageParser.java
+++ b/core/java/android/content/pm/PackageParser.java
@@ -1695,7 +1695,7 @@
         }
 
         // Check to see if overlay should be excluded based on system property condition
-        if (!checkRequiredSystemProperty(requiredSystemPropertyName,
+        if (!checkRequiredSystemProperties(requiredSystemPropertyName,
                 requiredSystemPropertyValue)) {
             Slog.i(TAG, "Skipping target and overlay pair " + targetPackage + " and "
                     + codePath + ": overlay ignored due to required system property: "
@@ -1997,7 +1997,7 @@
                 }
 
                 // check to see if overlay should be excluded based on system property condition
-                if (!checkRequiredSystemProperty(propName, propValue)) {
+                if (!checkRequiredSystemProperties(propName, propValue)) {
                     Slog.i(TAG, "Skipping target and overlay pair " + pkg.mOverlayTarget + " and "
                         + pkg.baseCodePath+ ": overlay ignored due to required system property: "
                         + propName + " with value: " + propValue);
@@ -2427,24 +2427,42 @@
 
     /**
      * Returns {@code true} if both the property name and value are empty or if the given system
-     * property is set to the specified value. In all other cases, returns {@code false}
+     * property is set to the specified value. Properties can be one or more, and if properties are
+     * more than one, they must be separated by comma, and count of names and values must be equal,
+     * and also every given system property must be set to the corresponding value.
+     * In all other cases, returns {@code false}
      */
-    public static boolean checkRequiredSystemProperty(String propName, String propValue) {
-        if (TextUtils.isEmpty(propName) || TextUtils.isEmpty(propValue)) {
-            if (!TextUtils.isEmpty(propName) || !TextUtils.isEmpty(propValue)) {
+    public static boolean checkRequiredSystemProperties(@Nullable String rawPropNames,
+            @Nullable String rawPropValues) {
+        if (TextUtils.isEmpty(rawPropNames) || TextUtils.isEmpty(rawPropValues)) {
+            if (!TextUtils.isEmpty(rawPropNames) || !TextUtils.isEmpty(rawPropValues)) {
                 // malformed condition - incomplete
-                Slog.w(TAG, "Disabling overlay - incomplete property :'" + propName
-                    + "=" + propValue + "' - require both requiredSystemPropertyName"
-                    + " AND requiredSystemPropertyValue to be specified.");
+                Slog.w(TAG, "Disabling overlay - incomplete property :'" + rawPropNames
+                        + "=" + rawPropValues + "' - require both requiredSystemPropertyName"
+                        + " AND requiredSystemPropertyValue to be specified.");
                 return false;
             }
             // no valid condition set - so no exclusion criteria, overlay will be included.
             return true;
         }
 
-        // check property value - make sure it is both set and equal to expected value
-        final String currValue = SystemProperties.get(propName);
-        return (currValue != null && currValue.equals(propValue));
+        final String[] propNames = rawPropNames.split(",");
+        final String[] propValues = rawPropValues.split(",");
+
+        if (propNames.length != propValues.length) {
+            Slog.w(TAG, "Disabling overlay - property :'" + rawPropNames
+                    + "=" + rawPropValues + "' - require both requiredSystemPropertyName"
+                    + " AND requiredSystemPropertyValue lists to have the same size.");
+            return false;
+        }
+        for (int i = 0; i < propNames.length; i++) {
+            // Check property value: make sure it is both set and equal to expected value
+            final String currValue = SystemProperties.get(propNames[i]);
+            if (!TextUtils.equals(currValue, propValues[i])) {
+                return false;
+            }
+        }
+        return true;
     }
 
     /**
@@ -6855,9 +6873,9 @@
 
         /** @hide */
         public boolean canHaveOatDir() {
-            // The following app types CANNOT have oat directory
-            // - non-updated system apps
-            return !isSystem() || isUpdatedSystemApp();
+            // Nobody should be calling this method ever, but we can't rely on this.
+            // Thus no logic here and a reasonable return value.
+            return true;
         }
 
         public boolean isMatch(int flags) {
diff --git a/core/java/android/content/pm/PackageParserCacheHelper.java b/core/java/android/content/pm/PackageParserCacheHelper.java
index 44def33..8212224 100644
--- a/core/java/android/content/pm/PackageParserCacheHelper.java
+++ b/core/java/android/content/pm/PackageParserCacheHelper.java
@@ -78,10 +78,19 @@
         /**
          * Read an string index from a parcel, and returns the corresponding string from the pool.
          */
-        @Override
         public String readString(Parcel p) {
             return mStrings.get(p.readInt());
         }
+
+        @Override
+        public String readString8(Parcel p) {
+            return readString(p);
+        }
+
+        @Override
+        public String readString16(Parcel p) {
+            return readString(p);
+        }
     }
 
     /**
@@ -110,7 +119,6 @@
          * Instead of writing a string directly to a parcel, this method adds it to the pool,
          * and write the index in the pool to the parcel.
          */
-        @Override
         public void writeString(Parcel p, String s) {
             final Integer cur = mIndexes.get(s);
             if (cur != null) {
@@ -133,6 +141,16 @@
             }
         }
 
+        @Override
+        public void writeString8(Parcel p, String s) {
+            writeString(p, s);
+        }
+
+        @Override
+        public void writeString16(Parcel p, String s) {
+            writeString(p, s);
+        }
+
         /**
          * Closes a parcel by appending the string pool at the end and updating the pool offset,
          * which it assumes is at the first byte.  It also uninstalls itself as a read-write helper.
diff --git a/core/java/android/content/pm/ProviderInfo.java b/core/java/android/content/pm/ProviderInfo.java
index 07d42dc..3984ade 100644
--- a/core/java/android/content/pm/ProviderInfo.java
+++ b/core/java/android/content/pm/ProviderInfo.java
@@ -145,9 +145,9 @@
 
     @Override public void writeToParcel(Parcel out, int parcelableFlags) {
         super.writeToParcel(out, parcelableFlags);
-        out.writeString(authority);
-        out.writeString(readPermission);
-        out.writeString(writePermission);
+        out.writeString8(authority);
+        out.writeString8(readPermission);
+        out.writeString8(writePermission);
         out.writeInt(grantUriPermissions ? 1 : 0);
         out.writeInt(forceUriPermissions ? 1 : 0);
         out.writeTypedArray(uriPermissionPatterns, parcelableFlags);
@@ -175,9 +175,9 @@
 
     private ProviderInfo(Parcel in) {
         super(in);
-        authority = in.readString();
-        readPermission = in.readString();
-        writePermission = in.readString();
+        authority = in.readString8();
+        readPermission = in.readString8();
+        writePermission = in.readString8();
         grantUriPermissions = in.readInt() != 0;
         forceUriPermissions = in.readInt() != 0;
         uriPermissionPatterns = in.createTypedArray(PatternMatcher.CREATOR);
diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java
index 5f90b6c4..d3f9e24 100644
--- a/core/java/android/content/pm/ServiceInfo.java
+++ b/core/java/android/content/pm/ServiceInfo.java
@@ -244,7 +244,7 @@
 
     public void writeToParcel(Parcel dest, int parcelableFlags) {
         super.writeToParcel(dest, parcelableFlags);
-        dest.writeString(permission);
+        dest.writeString8(permission);
         dest.writeInt(flags);
         dest.writeInt(mForegroundServiceType);
     }
@@ -261,7 +261,7 @@
 
     private ServiceInfo(Parcel source) {
         super(source);
-        permission = source.readString();
+        permission = source.readString8();
         flags = source.readInt();
         mForegroundServiceType = source.readInt();
     }
diff --git a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
index 27399e4..2f416a2 100644
--- a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
+++ b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
@@ -413,7 +413,7 @@
         }
 
         // Check to see if overlay should be excluded based on system property condition
-        if (!PackageParser.checkRequiredSystemProperty(requiredSystemPropertyName,
+        if (!PackageParser.checkRequiredSystemProperties(requiredSystemPropertyName,
                 requiredSystemPropertyValue)) {
             Slog.i(TAG, "Skipping target and overlay pair " + targetPackage + " and "
                     + codePath + ": overlay ignored due to required system property: "
diff --git a/core/java/android/content/pm/parsing/ParsingPackageUtils.java b/core/java/android/content/pm/parsing/ParsingPackageUtils.java
index 29ece49..88f4c31 100644
--- a/core/java/android/content/pm/parsing/ParsingPackageUtils.java
+++ b/core/java/android/content/pm/parsing/ParsingPackageUtils.java
@@ -84,7 +84,6 @@
 import android.os.Bundle;
 import android.os.FileUtils;
 import android.os.RemoteException;
-import android.os.SystemProperties;
 import android.os.Trace;
 import android.os.ext.SdkExtensions;
 import android.text.TextUtils;
@@ -2348,7 +2347,7 @@
                     R.styleable.AndroidManifestResourceOverlay_requiredSystemPropertyName);
             String propValue = sa.getString(
                     R.styleable.AndroidManifestResourceOverlay_requiredSystemPropertyValue);
-            if (!checkOverlayRequiredSystemProperty(propName, propValue)) {
+            if (!PackageParser.checkRequiredSystemProperties(propName, propValue)) {
                 Slog.i(TAG, "Skipping target and overlay pair " + target + " and "
                         + pkg.getBaseCodePath()
                         + ": overlay ignored due to required system property: "
@@ -2522,24 +2521,6 @@
         }
     }
 
-    private static boolean checkOverlayRequiredSystemProperty(String propName, String propValue) {
-        if (TextUtils.isEmpty(propName) || TextUtils.isEmpty(propValue)) {
-            if (!TextUtils.isEmpty(propName) || !TextUtils.isEmpty(propValue)) {
-                // malformed condition - incomplete
-                Slog.w(TAG, "Disabling overlay - incomplete property :'" + propName
-                        + "=" + propValue + "' - require both requiredSystemPropertyName"
-                        + " AND requiredSystemPropertyValue to be specified.");
-                return false;
-            }
-            // no valid condition set - so no exclusion criteria, overlay will be included.
-            return true;
-        }
-
-        // check property value - make sure it is both set and equal to expected value
-        final String currValue = SystemProperties.get(propName);
-        return (currValue != null && currValue.equals(propValue));
-    }
-
     /**
      * This is a pre-density application which will get scaled - instead of being pixel perfect.
      * This type of application is not resizable.
diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java
index f7c96a3..2f67f6d 100644
--- a/core/java/android/database/sqlite/SQLiteConnection.java
+++ b/core/java/android/database/sqlite/SQLiteConnection.java
@@ -228,19 +228,26 @@
         } catch (SQLiteCantOpenDatabaseException e) {
             String message = String.format("Cannot open database '%s'", file);
 
-            final Path path = FileSystems.getDefault().getPath(file);
-            final Path dir = path.getParent();
+            try {
+                // Try to diagnose for common reasons. If something fails in here, that's fine;
+                // just swallow the exception.
 
-            if (!Files.isDirectory(dir)) {
-                message += ": Directory " + dir + " doesn't exist";
-            } else if (!Files.exists(path)) {
-                message += ": File " + path + " doesn't exist";
-            } else if (!Files.isReadable(path)) {
-                message += ": File " + path + " is not readable";
-            } else if (Files.isDirectory(path)) {
-                message += ": Path " + path + " is a directory";
-            } else {
-                message += ": Unknown reason";
+                final Path path = FileSystems.getDefault().getPath(file);
+                final Path dir = path.getParent();
+
+                if (!Files.isDirectory(dir)) {
+                    message += ": Directory " + dir + " doesn't exist";
+                } else if (!Files.exists(path)) {
+                    message += ": File " + path + " doesn't exist";
+                } else if (!Files.isReadable(path)) {
+                    message += ": File " + path + " is not readable";
+                } else if (Files.isDirectory(path)) {
+                    message += ": Path " + path + " is a directory";
+                } else {
+                    message += ": Unknown reason";
+                }
+            } catch (Throwable th) {
+                message += ": Unknown reason; cannot examine filesystem: " + th.getMessage();
             }
             throw new SQLiteCantOpenDatabaseException(message, e);
         } finally {
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index 462110f..ad9bf07 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -65,21 +65,23 @@
     public abstract boolean isProximitySensorAvailable();
 
     /**
-     * Take a screenshot of the specified display and return a buffer.
+     * Screenshot for internal system-only use such as rotation, etc.  This method includes
+     * secure layers and the result should never be exposed to non-system applications.
+     * This method does not apply any rotation and provides the output in natural orientation.
      *
      * @param displayId The display id to take the screenshot of.
      * @return The buffer or null if we have failed.
      */
-    public abstract SurfaceControl.ScreenshotGraphicBuffer screenshot(int displayId);
+    public abstract SurfaceControl.ScreenshotGraphicBuffer systemScreenshot(int displayId);
 
     /**
-     * Take a screenshot without secure layer of the specified display and return a buffer.
+     * General screenshot functionality that excludes secure layers and applies appropriate
+     * rotation that the device is currently in.
      *
      * @param displayId The display id to take the screenshot of.
      * @return The buffer or null if we have failed.
      */
-    public abstract SurfaceControl.ScreenshotGraphicBuffer screenshotWithoutSecureLayer(
-            int displayId);
+    public abstract SurfaceControl.ScreenshotGraphicBuffer userScreenshot(int displayId);
 
     /**
      * Returns information about the specified logical display.
diff --git a/core/java/android/net/Uri.java b/core/java/android/net/Uri.java
index 525bbfd..1cb4fe8 100644
--- a/core/java/android/net/Uri.java
+++ b/core/java/android/net/Uri.java
@@ -500,7 +500,7 @@
         }
 
         static Uri readFrom(Parcel parcel) {
-            return new StringUri(parcel.readString());
+            return new StringUri(parcel.readString8());
         }
 
         public int describeContents() {
@@ -509,7 +509,7 @@
 
         public void writeToParcel(Parcel parcel, int flags) {
             parcel.writeInt(TYPE_ID);
-            parcel.writeString(uriString);
+            parcel.writeString8(uriString);
         }
 
         /** Cached scheme separator index. */
@@ -875,7 +875,7 @@
 
         static Uri readFrom(Parcel parcel) {
             return new OpaqueUri(
-                parcel.readString(),
+                parcel.readString8(),
                 Part.readFrom(parcel),
                 Part.readFrom(parcel)
             );
@@ -887,7 +887,7 @@
 
         public void writeToParcel(Parcel parcel, int flags) {
             parcel.writeInt(TYPE_ID);
-            parcel.writeString(scheme);
+            parcel.writeString8(scheme);
             ssp.writeTo(parcel);
             fragment.writeTo(parcel);
         }
@@ -1195,7 +1195,7 @@
 
         static Uri readFrom(Parcel parcel) {
             return new HierarchicalUri(
-                parcel.readString(),
+                parcel.readString8(),
                 Part.readFrom(parcel),
                 PathPart.readFrom(parcel),
                 Part.readFrom(parcel),
@@ -1209,7 +1209,7 @@
 
         public void writeToParcel(Parcel parcel, int flags) {
             parcel.writeInt(TYPE_ID);
-            parcel.writeString(scheme);
+            parcel.writeString8(scheme);
             authority.writeTo(parcel);
             path.writeTo(parcel);
             query.writeTo(parcel);
@@ -2028,7 +2028,7 @@
                     + mCanonicalRepresentation + ")");
             }
             parcel.writeInt(mCanonicalRepresentation);
-            parcel.writeString(canonicalValue);
+            parcel.writeString8(canonicalValue);
         }
     }
 
@@ -2060,7 +2060,7 @@
 
         static Part readFrom(Parcel parcel) {
             int representation = parcel.readInt();
-            String value = parcel.readString();
+            String value = parcel.readString8();
             switch (representation) {
                 case REPRESENTATION_ENCODED:
                     return fromEncoded(value);
@@ -2251,9 +2251,9 @@
             int representation = parcel.readInt();
             switch (representation) {
                 case REPRESENTATION_ENCODED:
-                    return fromEncoded(parcel.readString());
+                    return fromEncoded(parcel.readString8());
                 case REPRESENTATION_DECODED:
-                    return fromDecoded(parcel.readString());
+                    return fromDecoded(parcel.readString8());
                 default:
                     throw new IllegalArgumentException("Unknown representation: " + representation);
             }
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index f0b7b5f..93f6607 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -307,7 +307,9 @@
     @FastNative
     private static native void nativeWriteDouble(long nativePtr, double val);
     @FastNative
-    static native void nativeWriteString(long nativePtr, String val);
+    private static native void nativeWriteString8(long nativePtr, String val);
+    @FastNative
+    private static native void nativeWriteString16(long nativePtr, String val);
     @FastNative
     private static native void nativeWriteStrongBinder(long nativePtr, IBinder val);
     @FastNative
@@ -325,7 +327,9 @@
     @CriticalNative
     private static native double nativeReadDouble(long nativePtr);
     @FastNative
-    static native String nativeReadString(long nativePtr);
+    private static native String nativeReadString8(long nativePtr);
+    @FastNative
+    private static native String nativeReadString16(long nativePtr);
     @FastNative
     private static native IBinder nativeReadStrongBinder(long nativePtr);
     @FastNative
@@ -386,8 +390,12 @@
          * must use {@link #writeStringNoHelper(String)} to avoid
          * infinity recursive calls.
          */
-        public void writeString(Parcel p, String s) {
-            nativeWriteString(p.mNativePtr, s);
+        public void writeString8(Parcel p, String s) {
+            p.writeString8NoHelper(s);
+        }
+
+        public void writeString16(Parcel p, String s) {
+            p.writeString16NoHelper(s);
         }
 
         /**
@@ -395,8 +403,12 @@
          * must use {@link #readStringNoHelper()} to avoid
          * infinity recursive calls.
          */
-        public String readString(Parcel p) {
-            return nativeReadString(p.mNativePtr);
+        public String readString8(Parcel p) {
+            return p.readString8NoHelper();
+        }
+
+        public String readString16(Parcel p) {
+            return p.readString16NoHelper();
         }
     }
 
@@ -759,7 +771,17 @@
      * growing dataCapacity() if needed.
      */
     public final void writeString(@Nullable String val) {
-        mReadWriteHelper.writeString(this, val);
+        writeString16(val);
+    }
+
+    /** {@hide} */
+    public final void writeString8(@Nullable String val) {
+        mReadWriteHelper.writeString8(this, val);
+    }
+
+    /** {@hide} */
+    public final void writeString16(@Nullable String val) {
+        mReadWriteHelper.writeString16(this, val);
     }
 
     /**
@@ -770,7 +792,17 @@
      * @hide
      */
     public void writeStringNoHelper(@Nullable String val) {
-        nativeWriteString(mNativePtr, val);
+        writeString16NoHelper(val);
+    }
+
+    /** {@hide} */
+    public void writeString8NoHelper(@Nullable String val) {
+        nativeWriteString8(mNativePtr, val);
+    }
+
+    /** {@hide} */
+    public void writeString16NoHelper(@Nullable String val) {
+        nativeWriteString16(mNativePtr, val);
     }
 
     /**
@@ -2337,7 +2369,17 @@
      */
     @Nullable
     public final String readString() {
-        return mReadWriteHelper.readString(this);
+        return readString16();
+    }
+
+    /** {@hide} */
+    public final @Nullable String readString8() {
+        return mReadWriteHelper.readString8(this);
+    }
+
+    /** {@hide} */
+    public final @Nullable String readString16() {
+        return mReadWriteHelper.readString16(this);
     }
 
     /**
@@ -2347,9 +2389,18 @@
      *
      * @hide
      */
-    @Nullable
-    public String readStringNoHelper() {
-        return nativeReadString(mNativePtr);
+    public @Nullable String readStringNoHelper() {
+        return readString16NoHelper();
+    }
+
+    /** {@hide} */
+    public @Nullable String readString8NoHelper() {
+        return nativeReadString8(mNativePtr);
+    }
+
+    /** {@hide} */
+    public @Nullable String readString16NoHelper() {
+        return nativeReadString16(mNativePtr);
     }
 
     /**
diff --git a/core/java/android/os/incremental/IncrementalFileStorages.java b/core/java/android/os/incremental/IncrementalFileStorages.java
index 251995a..321dc9e 100644
--- a/core/java/android/os/incremental/IncrementalFileStorages.java
+++ b/core/java/android/os/incremental/IncrementalFileStorages.java
@@ -38,16 +38,10 @@
 import android.content.pm.IDataLoaderStatusListener;
 import android.content.pm.InstallationFileParcel;
 import android.text.TextUtils;
-import android.util.Slog;
 
 import java.io.File;
 import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.util.List;
-import java.util.Objects;
-import java.util.Random;
 
 /**
  * This class manages storage instances used during a package installation session.
@@ -56,13 +50,9 @@
 public final class IncrementalFileStorages {
     private static final String TAG = "IncrementalFileStorages";
 
-    private static final String TMP_DIR_ROOT = "/data/incremental/tmp";
-    private static final Random TMP_DIR_RANDOM = new Random();
-
+    private @NonNull final IncrementalManager mIncrementalManager;
+    private @NonNull final File mStageDir;
     private @Nullable IncrementalStorage mDefaultStorage;
-    private @Nullable String mDefaultDir;
-    private @NonNull IncrementalManager mIncrementalManager;
-    private @NonNull File mStageDir;
 
     /**
      * Set up files and directories used in an installation session. Only used by Incremental.
@@ -85,72 +75,63 @@
             throw new IOException("Failed to obtain incrementalManager.");
         }
 
-        IncrementalFileStorages result = null;
-        try {
-            result = new IncrementalFileStorages(stageDir, incrementalManager, dataLoaderParams,
-                    dataLoaderStatusListener);
-
-            if (!addedFiles.isEmpty()) {
-                result.mDefaultStorage.bind(stageDir.getAbsolutePath());
-            }
-
-            for (InstallationFileParcel file : addedFiles) {
-                if (file.location == LOCATION_DATA_APP) {
-                    try {
-                        result.addApkFile(file);
-                    } catch (IOException e) {
-                        // TODO(b/146080380): add incremental-specific error code
-                        throw new IOException(
-                                "Failed to add file to IncFS: " + file.name + ", reason: ", e);
-                    }
-                } else {
-                    throw new IOException("Unknown file location: " + file.location);
+        final IncrementalFileStorages result =
+                new IncrementalFileStorages(stageDir, incrementalManager, dataLoaderParams,
+                                            dataLoaderStatusListener);
+        for (InstallationFileParcel file : addedFiles) {
+            if (file.location == LOCATION_DATA_APP) {
+                try {
+                    result.addApkFile(file);
+                } catch (IOException e) {
+                    // TODO(b/146080380): add incremental-specific error code
+                    throw new IOException(
+                            "Failed to add file to IncFS: " + file.name + ", reason: ", e);
                 }
+            } else {
+                throw new IOException("Unknown file location: " + file.location);
             }
-
-            // TODO(b/146080380): remove 5 secs wait in startLoading
-            if (!result.mDefaultStorage.startLoading()) {
-                // TODO(b/146080380): add incremental-specific error code
-                throw new IOException("Failed to start loading data for Incremental installation.");
-            }
-
-            return result;
-        } catch (IOException e) {
-            Slog.e(TAG, "Failed to initialize Incremental file storages. Cleaning up...", e);
-            if (result != null) {
-                result.cleanUp();
-            }
-            throw e;
         }
+
+        if (!result.mDefaultStorage.startLoading()) {
+            // TODO(b/146080380): add incremental-specific error code
+            throw new IOException("Failed to start loading data for Incremental installation.");
+        }
+
+        return result;
     }
 
     private IncrementalFileStorages(@NonNull File stageDir,
             @NonNull IncrementalManager incrementalManager,
             @NonNull DataLoaderParams dataLoaderParams,
             @Nullable IDataLoaderStatusListener dataLoaderStatusListener) throws IOException {
-        mStageDir = stageDir;
-        mIncrementalManager = incrementalManager;
-        if (dataLoaderParams.getComponentName().getPackageName().equals("local")) {
-            final String incrementalPath = dataLoaderParams.getArguments();
-            mDefaultDir = incrementalPath;
-            if (TextUtils.isEmpty(mDefaultDir)) {
-                throw new IOException("Failed to create storage: incrementalPath is empty");
+        try {
+            mStageDir = stageDir;
+            mIncrementalManager = incrementalManager;
+            if (dataLoaderParams.getComponentName().getPackageName().equals("local")) {
+                final String incrementalPath = dataLoaderParams.getArguments();
+                if (TextUtils.isEmpty(incrementalPath)) {
+                    throw new IOException("Failed to create storage: incrementalPath is empty");
+                }
+                mDefaultStorage = mIncrementalManager.openStorage(incrementalPath);
+                if (mDefaultStorage == null) {
+                    throw new IOException(
+                            "Couldn't open incremental storage at " + incrementalPath);
+                }
+                mDefaultStorage.bind(stageDir.getAbsolutePath());
+            } else {
+                mDefaultStorage = mIncrementalManager.createStorage(stageDir.getAbsolutePath(),
+                        dataLoaderParams,
+                        dataLoaderStatusListener,
+                        IncrementalManager.CREATE_MODE_CREATE
+                                | IncrementalManager.CREATE_MODE_TEMPORARY_BIND, false);
+                if (mDefaultStorage == null) {
+                    throw new IOException(
+                            "Couldn't create incremental storage at " + stageDir);
+                }
             }
-            mDefaultStorage = mIncrementalManager.openStorage(incrementalPath);
-        } else {
-            mDefaultDir = getTempDir();
-            if (mDefaultDir == null) {
-                throw new IOException("Failed to create storage: tempDir is empty");
-            }
-            mDefaultStorage = mIncrementalManager.createStorage(mDefaultDir,
-                    dataLoaderParams,
-                    dataLoaderStatusListener,
-                    IncrementalManager.CREATE_MODE_CREATE
-                            | IncrementalManager.CREATE_MODE_TEMPORARY_BIND, false);
-        }
-
-        if (mDefaultStorage == null) {
-            throw new IOException("Failed to create storage");
+        } catch (IOException e) {
+            cleanUp();
+            throw e;
         }
     }
 
@@ -167,27 +148,14 @@
      * TODO(b/136132412): make sure unnecessary binds are removed but useful storages are kept
      */
     public void cleanUp() {
-        Objects.requireNonNull(mDefaultStorage);
+        if (mDefaultStorage == null) {
+            return;
+        }
 
         try {
-            mDefaultStorage.unBind(mDefaultDir);
             mDefaultStorage.unBind(mStageDir.getAbsolutePath());
         } catch (IOException ignored) {
         }
-
-        mDefaultDir = null;
         mDefaultStorage = null;
     }
-
-    private static String getTempDir() {
-        final Path tmpDir = Paths.get(TMP_DIR_ROOT,
-                String.valueOf(TMP_DIR_RANDOM.nextInt(Integer.MAX_VALUE - 1)));
-        try {
-            Files.createDirectories(tmpDir);
-        } catch (Exception ex) {
-            Slog.e(TAG, "Failed to create dir", ex);
-            return null;
-        }
-        return tmpDir.toAbsolutePath().toString();
-    }
 }
diff --git a/core/java/android/os/incremental/IncrementalManager.java b/core/java/android/os/incremental/IncrementalManager.java
index 35518db..916edfa 100644
--- a/core/java/android/os/incremental/IncrementalManager.java
+++ b/core/java/android/os/incremental/IncrementalManager.java
@@ -32,8 +32,12 @@
 import java.io.IOException;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
 
 /**
  * Provides operations to open or create an IncrementalStorage, using IIncrementalService
@@ -176,25 +180,6 @@
     }
 
     /**
-     * Iterates through path parents to find the base dir of an Incremental Storage.
-     *
-     * @param file Target file to search storage for.
-     * @return Absolute path which is a bind-mount point of Incremental File System.
-     */
-    @Nullable
-    private Path getStoragePathForFile(File file) {
-        File currentPath = new File(file.getParent());
-        while (currentPath.getParent() != null) {
-            IncrementalStorage storage = openStorage(currentPath.getAbsolutePath());
-            if (storage != null) {
-                return currentPath.toPath();
-            }
-            currentPath = new File(currentPath.getParent());
-        }
-        return null;
-    }
-
-    /**
      * Set up an app's code path. The expected outcome of this method is:
      * 1) The actual apk directory under /data/incremental is bind-mounted to the parent directory
      * of {@code afterCodeFile}.
@@ -212,29 +197,27 @@
      */
     public void renameCodePath(File beforeCodeFile, File afterCodeFile)
             throws IllegalArgumentException, IOException {
-        final String beforeCodePath = beforeCodeFile.getAbsolutePath();
-        final String afterCodePathParent = afterCodeFile.getParentFile().getAbsolutePath();
-        if (!isIncrementalPath(beforeCodePath)) {
-            throw new IllegalArgumentException("Not an Incremental path: " + beforeCodePath);
-        }
-        final String afterCodePathName = afterCodeFile.getName();
-        final Path apkStoragePath = Paths.get(beforeCodePath);
-        if (apkStoragePath == null || apkStoragePath.toAbsolutePath() == null) {
-            throw new IOException("Invalid source storage path for: " + beforeCodePath);
-        }
-        final IncrementalStorage apkStorage =
-                openStorage(apkStoragePath.toAbsolutePath().toString());
+        final File beforeCodeAbsolute = beforeCodeFile.getAbsoluteFile();
+        final IncrementalStorage apkStorage = openStorage(beforeCodeAbsolute.toString());
         if (apkStorage == null) {
-            throw new IOException("Failed to retrieve storage from Incremental Service.");
+            throw new IllegalArgumentException("Not an Incremental path: " + beforeCodeAbsolute);
         }
-        final IncrementalStorage linkedApkStorage = createStorage(afterCodePathParent, apkStorage,
-                IncrementalManager.CREATE_MODE_CREATE
-                        | IncrementalManager.CREATE_MODE_PERMANENT_BIND);
+        final String targetStorageDir = afterCodeFile.getAbsoluteFile().getParent();
+        final IncrementalStorage linkedApkStorage =
+                createStorage(targetStorageDir, apkStorage,
+                        IncrementalManager.CREATE_MODE_CREATE
+                                | IncrementalManager.CREATE_MODE_PERMANENT_BIND);
         if (linkedApkStorage == null) {
-            throw new IOException("Failed to create linked storage at dir: " + afterCodePathParent);
+            throw new IOException("Failed to create linked storage at dir: " + targetStorageDir);
         }
-        linkFiles(apkStorage, beforeCodeFile, "", linkedApkStorage, afterCodePathName);
-        apkStorage.unBind(beforeCodePath);
+        try {
+            final String afterCodePathName = afterCodeFile.getName();
+            linkFiles(apkStorage, beforeCodeAbsolute, "", linkedApkStorage, afterCodePathName);
+            apkStorage.unBind(beforeCodeAbsolute.toString());
+        } catch (Exception e) {
+            linkedApkStorage.unBind(targetStorageDir);
+            throw e;
+        }
     }
 
     /**
@@ -252,22 +235,27 @@
     private void linkFiles(IncrementalStorage sourceStorage, File sourceAbsolutePath,
             String sourceRelativePath, IncrementalStorage targetStorage,
             String targetRelativePath) throws IOException {
-        targetStorage.makeDirectory(targetRelativePath);
-        final File[] entryList = sourceAbsolutePath.listFiles();
-        for (int i = 0; i < entryList.length; i++) {
-            final File entry = entryList[i];
-            final String entryName = entryList[i].getName();
-            final String sourceEntryRelativePath =
-                    sourceRelativePath.isEmpty() ? entryName : sourceRelativePath + "/" + entryName;
-            final String targetEntryRelativePath = targetRelativePath + "/" + entryName;
-            if (entry.isFile()) {
-                sourceStorage.makeLink(
-                        sourceEntryRelativePath, targetStorage, targetEntryRelativePath);
-            } else if (entry.isDirectory()) {
-                linkFiles(sourceStorage, entry, sourceEntryRelativePath, targetStorage,
-                        targetEntryRelativePath);
+        final Path sourceBase = sourceAbsolutePath.toPath().resolve(sourceRelativePath);
+        final Path targetRelative = Paths.get(targetRelativePath);
+        Files.walkFileTree(sourceAbsolutePath.toPath(), new SimpleFileVisitor<Path>() {
+            @Override
+            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
+                    throws IOException {
+                final Path relativeDir = sourceBase.relativize(dir);
+                targetStorage.makeDirectory(targetRelative.resolve(relativeDir).toString());
+                return FileVisitResult.CONTINUE;
             }
-        }
+
+            @Override
+            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
+                    throws IOException {
+                final Path relativeFile = sourceBase.relativize(file);
+                sourceStorage.makeLink(
+                        file.toAbsolutePath().toString(), targetStorage,
+                        targetRelative.resolve(relativeFile).toString());
+                return FileVisitResult.CONTINUE;
+            }
+        });
     }
 
     /**
diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java
index aee32ed..a1a11ed 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -2447,7 +2447,12 @@
         final String filePath = path.getCanonicalPath();
         final StorageVolume volume = getStorageVolume(path);
         if (volume == null) {
-            throw new IllegalStateException("Failed to update quota type for " + filePath);
+            Log.w(TAG, "Failed to update quota type for " + filePath);
+            return;
+        }
+        if (!volume.isEmulated()) {
+            // We only support quota tracking on emulated filesystems
+            return;
         }
 
         final int userId = volume.getOwner().getIdentifier();
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index af91596..1f6555c 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -102,8 +102,8 @@
     private static final boolean DEFAULT_ALLOW_REMINDERS = false;
     private static final boolean DEFAULT_ALLOW_EVENTS = false;
     private static final boolean DEFAULT_ALLOW_REPEAT_CALLERS = true;
-    private static final boolean DEFAULT_ALLOW_CONV = true;
-    private static final int DEFAULT_ALLOW_CONV_FROM = ZenPolicy.CONVERSATION_SENDERS_IMPORTANT;
+    private static final boolean DEFAULT_ALLOW_CONV = false;
+    private static final int DEFAULT_ALLOW_CONV_FROM = ZenPolicy.CONVERSATION_SENDERS_NONE;
     private static final boolean DEFAULT_CHANNELS_BYPASSING_DND = false;
     private static final int DEFAULT_SUPPRESSED_VISUAL_EFFECTS = 0;
 
@@ -125,8 +125,8 @@
     private static final String ALLOW_ATT_EVENTS = "events";
     private static final String ALLOW_ATT_SCREEN_OFF = "visualScreenOff";
     private static final String ALLOW_ATT_SCREEN_ON = "visualScreenOn";
-    private static final String ALLOW_ATT_CONV = "conv";
-    private static final String ALLOW_ATT_CONV_FROM = "convFrom";
+    private static final String ALLOW_ATT_CONV = "convos";
+    private static final String ALLOW_ATT_CONV_FROM = "convosFrom";
     private static final String DISALLOW_TAG = "disallow";
     private static final String DISALLOW_ATT_VISUAL_EFFECTS = "visualEffects";
     private static final String STATE_TAG = "state";
diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java
index df4ead1..28639b3 100644
--- a/core/java/android/text/TextUtils.java
+++ b/core/java/android/text/TextUtils.java
@@ -748,7 +748,7 @@
             int parcelableFlags) {
         if (cs instanceof Spanned) {
             p.writeInt(0);
-            p.writeString(cs.toString());
+            p.writeString8(cs.toString());
 
             Spanned sp = (Spanned) cs;
             Object[] os = sp.getSpans(0, cs.length(), Object.class);
@@ -785,9 +785,9 @@
         } else {
             p.writeInt(1);
             if (cs != null) {
-                p.writeString(cs.toString());
+                p.writeString8(cs.toString());
             } else {
-                p.writeString(null);
+                p.writeString8(null);
             }
         }
     }
@@ -807,7 +807,7 @@
         public CharSequence createFromParcel(Parcel p) {
             int kind = p.readInt();
 
-            String string = p.readString();
+            String string = p.readString8();
             if (string == null) {
                 return null;
             }
diff --git a/core/java/android/text/style/DynamicDrawableSpan.java b/core/java/android/text/style/DynamicDrawableSpan.java
index f37e423..d6d99f8 100644
--- a/core/java/android/text/style/DynamicDrawableSpan.java
+++ b/core/java/android/text/style/DynamicDrawableSpan.java
@@ -166,7 +166,7 @@
         if (mVerticalAlignment == ALIGN_BASELINE) {
             transY -= paint.getFontMetricsInt().descent;
         } else if (mVerticalAlignment == ALIGN_CENTER) {
-            transY = (bottom - top) / 2 - b.getBounds().height() / 2;
+            transY = top + (bottom - top) / 2 - b.getBounds().height() / 2;
         }
 
         canvas.translate(x, transY);
diff --git a/core/java/android/view/IScrollCaptureClient.aidl b/core/java/android/view/IScrollCaptureClient.aidl
new file mode 100644
index 0000000..5f135a37
--- /dev/null
+++ b/core/java/android/view/IScrollCaptureClient.aidl
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.graphics.Rect;
+import android.view.Surface;
+
+
+ /**
+   * Interface implemented by a client of the Scroll Capture framework to receive requests
+   * to start, capture images and end the session.
+   *
+   * {@hide}
+   */
+interface IScrollCaptureClient {
+
+    /**
+     * Informs the client that it has been selected for scroll capture and should prepare to
+     * to begin handling capture requests.
+     */
+    oneway void startCapture(in Surface surface);
+
+    /**
+     * Request the client capture an image within the provided rectangle.
+     *
+     * @see android.view.ScrollCaptureCallback#onScrollCaptureRequest
+     */
+    oneway void requestImage(in Rect captureArea);
+
+    /**
+     * Inform the client that capture has ended. The client should shut down and release all
+     * local resources in use and prepare for return to normal interactive usage.
+     */
+    oneway void endCapture();
+}
diff --git a/core/java/android/view/IScrollCaptureController.aidl b/core/java/android/view/IScrollCaptureController.aidl
new file mode 100644
index 0000000..8474a00
--- /dev/null
+++ b/core/java/android/view/IScrollCaptureController.aidl
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.Surface;
+
+import android.view.IScrollCaptureClient;
+
+/**
+ * Interface to a controller passed to the {@link IScrollCaptureClient} which provides the client an
+ * asynchronous callback channel for responses.
+ *
+ * {@hide}
+ */
+interface IScrollCaptureController {
+    /**
+     * Scroll capture is available, and a client connect has been returned.
+     *
+     * @param client interface to a ScrollCaptureCallback in the window process
+     * @param scrollAreaInWindow the location of scrolling in global (window) coordinate space
+     */
+    oneway void onClientConnected(in IScrollCaptureClient client, in Rect scrollBounds,
+            in Point positionInWindow);
+
+    /**
+     * Nothing in the window can be scrolled, scroll capture not offered.
+     */
+    oneway void onClientUnavailable();
+
+    /**
+     * Notifies the system that the client has confirmed the request and is ready to begin providing
+     * image requests.
+     */
+    oneway void onCaptureStarted();
+
+    /**
+     * Received a response from a capture request.
+     */
+    oneway void onCaptureBufferSent(long frameNumber, in Rect capturedArea);
+
+    /**
+     * Signals that the capture session has completed and the target window may be returned to
+     * normal interactive use. This may be due to normal shutdown, or after a timeout or other
+     * unrecoverable state change such as activity lifecycle, window visibility or focus.
+     */
+    oneway void onConnectionClosed();
+}
diff --git a/core/java/android/view/IWindow.aidl b/core/java/android/view/IWindow.aidl
index 9f2d36d..e09bf9d 100644
--- a/core/java/android/view/IWindow.aidl
+++ b/core/java/android/view/IWindow.aidl
@@ -21,15 +21,16 @@
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.os.ParcelFileDescriptor;
+import android.util.MergedConfiguration;
+import android.view.DisplayCutout;
 import android.view.DragEvent;
+import android.view.InsetsSourceControl;
+import android.view.InsetsState;
+import android.view.IScrollCaptureController;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
-import android.view.DisplayCutout;
-import android.view.InsetsState;
-import android.view.InsetsSourceControl;
 
 import com.android.internal.os.IResultReceiver;
-import android.util.MergedConfiguration;
 
 /**
  * API back to a client window that the Window Manager uses to inform it of
@@ -139,4 +140,11 @@
      * Tell the window that it is either gaining or losing pointer capture.
      */
     void dispatchPointerCaptureChanged(boolean hasCapture);
+
+    /**
+     * Called when Scroll Capture support is requested for a window.
+     *
+     * @param controller the controller to receive responses
+     */
+    void requestScrollCapture(in IScrollCaptureController controller);
 }
diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
index b0bacb9..b3b53f0 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -42,6 +42,7 @@
 import android.view.IDisplayWindowRotationController;
 import android.view.IOnKeyguardExitResult;
 import android.view.IPinnedStackListener;
+import android.view.IScrollCaptureController;
 import android.view.RemoteAnimationAdapter;
 import android.view.IRotationWatcher;
 import android.view.ISystemGestureExclusionListener;
@@ -749,4 +750,18 @@
      * @param flags see definition in SurfaceTracing.cpp
      */
     void setLayerTracingFlags(int flags);
+
+    /**
+     * Forwards a scroll capture request to the appropriate window, if available.
+     *
+     * @param displayId the id of the display to target
+     * @param behindClient token for a window, used to filter the search to windows behind it, or
+     *                     {@code null} to accept a window at any zOrder
+     * @param taskId specifies the id of a task the result must belong to, or -1 to ignore task ids
+     * @param controller the controller to receive results, a call to either
+     *      {@link IScrollCaptureController#onClientConnected} or
+     *      {@link IScrollCaptureController#onClientUnavailable}.
+     */
+    void requestScrollCapture(int displayId, IBinder behindClient, int taskId,
+            IScrollCaptureController controller);
 }
diff --git a/core/java/android/view/ImeInsetsSourceConsumer.java b/core/java/android/view/ImeInsetsSourceConsumer.java
index bdc8ba8..df89130 100644
--- a/core/java/android/view/ImeInsetsSourceConsumer.java
+++ b/core/java/android/view/ImeInsetsSourceConsumer.java
@@ -153,7 +153,7 @@
 
     @Override
     protected boolean isRequestedVisibleAwaitingControl() {
-        return mIsRequestedVisibleAwaitingControl;
+        return mIsRequestedVisibleAwaitingControl || isRequestedVisible();
     }
 
     private boolean isDummyOrEmptyEditor(EditorInfo info) {
diff --git a/core/java/android/view/ScrollCaptureCallback.java b/core/java/android/view/ScrollCaptureCallback.java
new file mode 100644
index 0000000..e1a4e74
--- /dev/null
+++ b/core/java/android/view/ScrollCaptureCallback.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+import android.annotation.UiThread;
+import android.graphics.Rect;
+
+import java.util.function.Consumer;
+
+/**
+ * A ScrollCaptureCallback is responsible for providing rendered snapshots of scrolling content for
+ * the scroll capture system. A single callback is responsible for providing support to a single
+ * scrolling UI element. At request time, the system will select the best candidate from among all
+ * callbacks registered within the window.
+ * <p>
+ * A callback is assigned to a View using {@link View#setScrollCaptureCallback}, or to the window as
+ * {@link Window#addScrollCaptureCallback}. The point where the callback is registered defines the
+ * frame of reference for the bounds measurements used.
+ * <p>
+ * <b>Terminology</b>
+ * <dl>
+ * <dt>Containing View</dt>
+ * <dd>The view on which this callback is attached, or the root view of the window if the callback
+ * is assigned  directly to a window.</dd>
+ *
+ * <dt>Scroll Bounds</dt>
+ * <dd>A rectangle which describes an area within the containing view where scrolling content may
+ * be positioned. This may be the Containing View bounds itself, or any rectangle within.
+ * Requested by {@link #onScrollCaptureSearch}.</dd>
+ *
+ * <dt>Scroll Delta</dt>
+ * <dd>The distance the scroll position has moved since capture started. Implementations are
+ * responsible for tracking changes in vertical scroll position during capture. This is required to
+ * map the capture area to the correct location, given the current scroll position.
+ *
+ * <dt>Capture Area</dt>
+ * <dd>A rectangle which describes the area to capture, relative to scroll bounds. The vertical
+ * position remains relative to the starting scroll position and any movement since ("Scroll Delta")
+ * should be subtracted to locate the correct local position, and scrolled into view as necessary.
+ * </dd>
+ * </dl>
+ *
+ * @see View#setScrollCaptureHint(int)
+ * @see View#setScrollCaptureCallback(ScrollCaptureCallback)
+ * @see Window#addScrollCaptureCallback(ScrollCaptureCallback)
+ *
+ * @hide
+ */
+@UiThread
+public interface ScrollCaptureCallback {
+
+    /**
+     * The system is searching for the appropriate scrolling container to capture and would like to
+     * know the size and position of scrolling content handled by this callback.
+     * <p>
+     * Implementations should inset {@code containingViewBounds} to cover only the area within the
+     * containing view where scrolling content may be positioned. This should cover only the content
+     * which tracks with scrolling movement.
+     * <p>
+     * Return the updated rectangle to {@code resultConsumer}. If for any reason the scrolling
+     * content is not available to capture, a {@code null} rectangle may be returned, and this view
+     * will be excluded as the target for this request.
+     * <p>
+     * Responses received after XXXms will be discarded.
+     * <p>
+     * TODO: finalize timeout
+     *
+     * @param onReady              consumer for the updated rectangle
+     */
+    void onScrollCaptureSearch(@NonNull Consumer<Rect> onReady);
+
+    /**
+     * Scroll Capture has selected this callback to provide the scrolling image content.
+     * <p>
+     * The onReady signal should be called when ready to begin handling image requests.
+     */
+    void onScrollCaptureStart(@NonNull ScrollCaptureSession session, @NonNull Runnable onReady);
+
+    /**
+     * An image capture has been requested from the scrolling content.
+     * <p>
+     * <code>captureArea</code> contains the bounds of the image requested, relative to the
+     * rectangle provided by {@link ScrollCaptureCallback#onScrollCaptureSearch}, referred to as
+     * {@code scrollBounds}.
+     * here.
+     * <p>
+     * A series of requests will step by a constant vertical amount relative to {@code
+     * scrollBounds}, moving through the scrolling range of content, above and below the current
+     * visible area. The rectangle's vertical position will not account for any scrolling movement
+     * since capture started. Implementations therefore must track any scroll position changes and
+     * subtract this distance from requests.
+     * <p>
+     * To handle a request, the content should be scrolled to maximize the visible area of the
+     * requested rectangle. Offset {@code captureArea} again to account for any further scrolling.
+     * <p>
+     * Finally, clip this rectangle against scrollBounds to determine what portion, if any is
+     * visible content to capture. If the rectangle is completely clipped, set it to {@link
+     * Rect#setEmpty() empty} and skip the next step.
+     * <p>
+     * Make a copy of {@code captureArea}, transform to window coordinates and draw the window,
+     * clipped to this rectangle, into the {@link ScrollCaptureSession#getSurface() surface} at
+     * offset (0,0).
+     * <p>
+     * Finally, return the resulting {@code captureArea} using
+     * {@link ScrollCaptureSession#notifyBufferSent}.
+     * <p>
+     * If the response is not supplied within XXXms, the session will end with a call to {@link
+     * #onScrollCaptureEnd}, after which {@code session} is invalid and should be discarded.
+     * <p>
+     * TODO: finalize timeout
+     * <p>
+     *
+     * @param captureArea the area to capture, a rectangle within {@code scrollBounds}
+     */
+    void onScrollCaptureImageRequest(
+            @NonNull ScrollCaptureSession session, @NonNull Rect captureArea);
+
+    /**
+     * Signals that capture has ended. Implementations should release any temporary resources or
+     * references to objects in use during the capture. Any resources obtained from the session are
+     * now invalid and attempts to use them after this point may throw an exception.
+     * <p>
+     * The window should be returned as much as possible to its original state when capture started.
+     * At a minimum, the content should be scrolled to its original position.
+     * <p>
+     * <code>onReady</code> should be called when the window should be made visible and
+     * interactive. The system will wait up to XXXms for this call before proceeding.
+     * <p>
+     * TODO: finalize timeout
+     *
+     * @param onReady a callback to inform the system that the application has completed any
+     *                cleanup and is ready to become visible
+     */
+    void onScrollCaptureEnd(@NonNull Runnable onReady);
+}
+
diff --git a/core/java/android/view/ScrollCaptureClient.java b/core/java/android/view/ScrollCaptureClient.java
new file mode 100644
index 0000000..f163124
--- /dev/null
+++ b/core/java/android/view/ScrollCaptureClient.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UiThread;
+import android.annotation.WorkerThread;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.util.CloseGuard;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A client of the system providing Scroll Capture capability on behalf of a Window.
+ * <p>
+ * An instance is created to wrap the selected {@link ScrollCaptureCallback}.
+ *
+ * @hide
+ */
+public class ScrollCaptureClient extends IScrollCaptureClient.Stub {
+
+    private static final String TAG = "ScrollCaptureClient";
+    private static final int DEFAULT_TIMEOUT = 1000;
+
+    private final Handler mHandler;
+    private ScrollCaptureTarget mSelectedTarget;
+    private int mTimeoutMillis = DEFAULT_TIMEOUT;
+
+    protected Surface mSurface;
+    private IScrollCaptureController mController;
+
+    private final Rect mScrollBounds;
+    private final Point mPositionInWindow;
+    private final CloseGuard mCloseGuard;
+
+    // The current session instance in use by the callback.
+    private ScrollCaptureSession mSession;
+
+    // Helps manage timeout callbacks registered to handler and aids testing.
+    private DelayedAction mTimeoutAction;
+
+    /**
+     * Constructs a ScrollCaptureClient.
+     *
+     * @param selectedTarget  the target the client is controlling
+     * @param controller the callbacks to reply to system requests
+     *
+     * @hide
+     */
+    public ScrollCaptureClient(
+            @NonNull ScrollCaptureTarget selectedTarget,
+            @NonNull IScrollCaptureController controller) {
+        requireNonNull(selectedTarget, "<selectedTarget> must non-null");
+        requireNonNull(controller, "<controller> must non-null");
+        final Rect scrollBounds = requireNonNull(selectedTarget.getScrollBounds(),
+                "target.getScrollBounds() must be non-null to construct a client");
+
+        mSelectedTarget = selectedTarget;
+        mHandler = selectedTarget.getContainingView().getHandler();
+        mScrollBounds = new Rect(scrollBounds);
+        mPositionInWindow = new Point(selectedTarget.getPositionInWindow());
+
+        mController = controller;
+        mCloseGuard = new CloseGuard();
+        mCloseGuard.open("close");
+
+        selectedTarget.getContainingView().addOnAttachStateChangeListener(
+                new View.OnAttachStateChangeListener() {
+                    @Override
+                    public void onViewAttachedToWindow(View v) {
+
+                    }
+
+                    @Override
+                    public void onViewDetachedFromWindow(View v) {
+                        selectedTarget.getContainingView().removeOnAttachStateChangeListener(this);
+                        endCapture();
+                    }
+                });
+    }
+
+    @VisibleForTesting
+    public void setTimeoutMillis(int timeoutMillis) {
+        mTimeoutMillis = timeoutMillis;
+    }
+
+    @Nullable
+    @VisibleForTesting
+    public DelayedAction getTimeoutAction() {
+        return mTimeoutAction;
+    }
+
+    private void checkConnected() {
+        if (mSelectedTarget == null || mController == null) {
+            throw new IllegalStateException("This client has been disconnected.");
+        }
+    }
+
+    private void checkStarted() {
+        if (mSession == null) {
+            throw new IllegalStateException("Capture session has not been started!");
+        }
+    }
+
+    @WorkerThread // IScrollCaptureClient
+    @Override
+    public void startCapture(Surface surface) throws RemoteException {
+        checkConnected();
+        mSurface = surface;
+        scheduleTimeout(mTimeoutMillis, this::onStartCaptureTimeout);
+        mSession = new ScrollCaptureSession(mSurface, mScrollBounds, mPositionInWindow, this);
+        mHandler.post(() -> mSelectedTarget.getCallback().onScrollCaptureStart(mSession,
+                this::onStartCaptureCompleted));
+    }
+
+    @UiThread
+    private void onStartCaptureCompleted() {
+        if (cancelTimeout()) {
+            mHandler.post(() -> {
+                try {
+                    mController.onCaptureStarted();
+                } catch (RemoteException e) {
+                    doShutdown();
+                }
+            });
+        }
+    }
+
+    @UiThread
+    private void onStartCaptureTimeout() {
+        endCapture();
+    }
+
+    @WorkerThread // IScrollCaptureClient
+    @Override
+    public void requestImage(Rect requestRect) {
+        checkConnected();
+        checkStarted();
+        scheduleTimeout(mTimeoutMillis, this::onRequestImageTimeout);
+        // Response is dispatched via ScrollCaptureSession, to onRequestImageCompleted
+        mHandler.post(() -> mSelectedTarget.getCallback().onScrollCaptureImageRequest(
+                mSession, new Rect(requestRect)));
+    }
+
+    @UiThread
+    void onRequestImageCompleted(long frameNumber, Rect capturedArea) {
+        final Rect finalCapturedArea = new Rect(capturedArea);
+        if (cancelTimeout()) {
+            mHandler.post(() -> {
+                try {
+                    mController.onCaptureBufferSent(frameNumber, finalCapturedArea);
+                } catch (RemoteException e) {
+                    doShutdown();
+                }
+            });
+        }
+    }
+
+    @UiThread
+    private void onRequestImageTimeout() {
+        endCapture();
+    }
+
+    @WorkerThread // IScrollCaptureClient
+    @Override
+    public void endCapture() {
+        if (isStarted()) {
+            scheduleTimeout(mTimeoutMillis, this::onEndCaptureTimeout);
+            mHandler.post(() ->
+                    mSelectedTarget.getCallback().onScrollCaptureEnd(this::onEndCaptureCompleted));
+        } else {
+            disconnect();
+        }
+    }
+
+    private boolean isStarted() {
+        return mController != null && mSelectedTarget != null;
+    }
+
+    @UiThread
+    private void onEndCaptureCompleted() { // onEndCaptureCompleted
+        if (cancelTimeout()) {
+            doShutdown();
+        }
+    }
+
+    @UiThread
+    private void onEndCaptureTimeout() {
+        doShutdown();
+    }
+
+
+    private void doShutdown() {
+        try {
+            if (mController != null) {
+                mController.onConnectionClosed();
+            }
+        } catch (RemoteException e) {
+            // Ignore
+        } finally {
+            disconnect();
+        }
+    }
+
+    /**
+     * Shuts down this client and releases references to dependent objects. No attempt is made
+     * to notify the controller, use with caution!
+     */
+    public void disconnect() {
+        if (mSession != null) {
+            mSession.disconnect();
+            mSession = null;
+        }
+
+        mSelectedTarget = null;
+        mController = null;
+    }
+
+    /** @return a string representation of the state of this client */
+    public String toString() {
+        return "ScrollCaptureClient{"
+                + ", session=" + mSession
+                + ", selectedTarget=" + mSelectedTarget
+                + ", clientCallbacks=" + mController
+                + "}";
+    }
+
+    private boolean cancelTimeout() {
+        if (mTimeoutAction != null) {
+            return mTimeoutAction.cancel();
+        }
+        return false;
+    }
+
+    private void scheduleTimeout(long timeoutMillis, Runnable action) {
+        if (mTimeoutAction != null) {
+            mTimeoutAction.cancel();
+        }
+        mTimeoutAction = new DelayedAction(mHandler, timeoutMillis, action);
+    }
+
+    /** @hide */
+    @VisibleForTesting
+    public static class DelayedAction {
+        private final AtomicBoolean mCompleted = new AtomicBoolean();
+        private final Object mToken = new Object();
+        private final Handler mHandler;
+        private final Runnable mAction;
+
+        @VisibleForTesting
+        public DelayedAction(Handler handler, long timeoutMillis, Runnable action) {
+            mHandler = handler;
+            mAction = action;
+            mHandler.postDelayed(this::onTimeout, mToken, timeoutMillis);
+        }
+
+        private boolean onTimeout() {
+            if (mCompleted.compareAndSet(false, true)) {
+                mAction.run();
+                return true;
+            }
+            return false;
+        }
+
+        /**
+         * Cause the timeout action to run immediately and mark as timed out.
+         *
+         * @return true if the timeout was run, false if the timeout had already been canceled
+         */
+        @VisibleForTesting
+        public boolean timeoutNow() {
+            return onTimeout();
+        }
+
+        /**
+         * Attempt to cancel the timeout action (such as after a callback is made)
+         *
+         * @return true if the timeout was canceled and will not run, false if time has expired and
+         * the timeout action has or will run momentarily
+         */
+        public boolean cancel() {
+            if (!mCompleted.compareAndSet(false, true)) {
+                // Whoops, too late!
+                return false;
+            }
+            mHandler.removeCallbacksAndMessages(mToken);
+            return true;
+        }
+    }
+}
diff --git a/core/java/android/view/ScrollCaptureSession.java b/core/java/android/view/ScrollCaptureSession.java
new file mode 100644
index 0000000..628e23f
--- /dev/null
+++ b/core/java/android/view/ScrollCaptureSession.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Point;
+import android.graphics.Rect;
+
+/**
+ * A session represents the scope of interaction between a {@link ScrollCaptureCallback} and the
+ * system during an active scroll capture operation. During the scope of a session, a callback
+ * will receive a series of requests for image data. Resources provided here are valid for use
+ * until {@link ScrollCaptureCallback#onScrollCaptureEnd(Runnable)}.
+ *
+ * @hide
+ */
+public class ScrollCaptureSession {
+
+    private final Surface mSurface;
+    private final Rect mScrollBounds;
+    private final Point mPositionInWindow;
+
+    @Nullable
+    private ScrollCaptureClient mClient;
+
+    /** @hide */
+    public ScrollCaptureSession(Surface surface, Rect scrollBounds, Point positionInWindow,
+            ScrollCaptureClient client) {
+        mSurface = surface;
+        mScrollBounds = scrollBounds;
+        mPositionInWindow = positionInWindow;
+        mClient = client;
+    }
+
+    /**
+     * Returns a
+     * <a href="https://source.android.com/devices/graphics/arch-bq-gralloc">BufferQueue</a> in the
+     * form of a {@link Surface} for transfer of image buffers.
+     *
+     * @return the surface for transferring image buffers
+     * @throws IllegalStateException if the session has been closed
+     */
+    @NonNull
+    public Surface getSurface() {
+        return mSurface;
+    }
+
+    /**
+     * Returns the {@code scroll bounds}, as provided by
+     * {@link ScrollCaptureCallback#onScrollCaptureSearch}.
+     *
+     * @return the area of scrolling content within the containing view
+     */
+    @NonNull
+    public Rect getScrollBounds() {
+        return mScrollBounds;
+    }
+
+    /**
+     * Returns the offset of {@code scroll bounds} within the window.
+     *
+     * @return the area of scrolling content within the containing view
+     */
+    @NonNull
+    public Point getPositionInWindow() {
+        return mPositionInWindow;
+    }
+
+    /**
+     * Notify the system that an a buffer has been posted via the getSurface() channel.
+     *
+     * @param frameNumber  the frame number of the queued buffer
+     * @param capturedArea the area captured, relative to scroll bounds
+     */
+    public void notifyBufferSent(long frameNumber, @NonNull Rect capturedArea) {
+        if (mClient != null) {
+            mClient.onRequestImageCompleted(frameNumber, capturedArea);
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public void disconnect() {
+        mClient = null;
+        if (mSurface.isValid()) {
+            mSurface.release();
+        }
+    }
+}
diff --git a/core/java/android/view/ScrollCaptureTarget.java b/core/java/android/view/ScrollCaptureTarget.java
new file mode 100644
index 0000000..f3fcabb
--- /dev/null
+++ b/core/java/android/view/ScrollCaptureTarget.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UiThread;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.Rect;
+
+import com.android.internal.util.FastMath;
+
+/**
+ * A target collects the set of contextual information for a ScrollCaptureHandler discovered during
+ * a {@link View#dispatchScrollCaptureSearch scroll capture search}.
+ *
+ * @hide
+ */
+public final class ScrollCaptureTarget {
+    private final View mContainingView;
+    private final ScrollCaptureCallback mCallback;
+    private final Rect mLocalVisibleRect;
+    private final Point mPositionInWindow;
+    private final int mHint;
+    private Rect mScrollBounds;
+
+    private final float[] mTmpFloatArr = new float[2];
+    private final Matrix mMatrixViewLocalToWindow = new Matrix();
+    private final Rect mTmpRect = new Rect();
+
+    public ScrollCaptureTarget(@NonNull View scrollTarget, @NonNull Rect localVisibleRect,
+            @NonNull Point positionInWindow, @NonNull ScrollCaptureCallback callback) {
+        mContainingView = scrollTarget;
+        mHint = mContainingView.getScrollCaptureHint();
+        mCallback = callback;
+        mLocalVisibleRect = localVisibleRect;
+        mPositionInWindow = positionInWindow;
+    }
+
+    /** @return the hint that the {@code containing view} had during the scroll capture search */
+    @View.ScrollCaptureHint
+    public int getHint() {
+        return mHint;
+    }
+
+    /** @return the {@link ScrollCaptureCallback} for this target */
+    @NonNull
+    public ScrollCaptureCallback getCallback() {
+        return mCallback;
+    }
+
+    /** @return the {@code containing view} for this {@link ScrollCaptureCallback callback} */
+    @NonNull
+    public View getContainingView() {
+        return mContainingView;
+    }
+
+    /**
+     * Returns the un-clipped, visible bounds of the containing view during the scroll capture
+     * search. This is used to determine on-screen area to assist in selecting the primary target.
+     *
+     * @return the visible bounds of the {@code containing view} in view-local coordinates
+     */
+    @NonNull
+    public Rect getLocalVisibleRect() {
+        return mLocalVisibleRect;
+    }
+
+    /** @return the position of the {@code containing view} within the window */
+    @NonNull
+    public Point getPositionInWindow() {
+        return mPositionInWindow;
+    }
+
+    /** @return the {@code scroll bounds} for this {@link ScrollCaptureCallback callback} */
+    @Nullable
+    public Rect getScrollBounds() {
+        return mScrollBounds;
+    }
+
+    /**
+     * Sets the scroll bounds rect to the intersection of provided rect and the current bounds of
+     * the {@code containing view}.
+     */
+    public void setScrollBounds(@Nullable Rect scrollBounds) {
+        mScrollBounds = Rect.copyOrNull(scrollBounds);
+        if (mScrollBounds == null) {
+            return;
+        }
+        if (!mScrollBounds.intersect(0, 0,
+                mContainingView.getWidth(), mContainingView.getHeight())) {
+            mScrollBounds.setEmpty();
+        }
+    }
+
+    private static void zero(float[] pointArray) {
+        pointArray[0] = 0;
+        pointArray[1] = 0;
+    }
+
+    private static void roundIntoPoint(Point pointObj, float[] pointArray) {
+        pointObj.x = FastMath.round(pointArray[0]);
+        pointObj.y = FastMath.round(pointArray[1]);
+    }
+
+    /**
+     * Refresh the value of {@link #mLocalVisibleRect} and {@link #mPositionInWindow} based on the
+     * current state of the {@code containing view}.
+     */
+    @UiThread
+    public void updatePositionInWindow() {
+        mMatrixViewLocalToWindow.reset();
+        mContainingView.transformMatrixToGlobal(mMatrixViewLocalToWindow);
+
+        zero(mTmpFloatArr);
+        mMatrixViewLocalToWindow.mapPoints(mTmpFloatArr);
+        roundIntoPoint(mPositionInWindow, mTmpFloatArr);
+    }
+
+}
diff --git a/core/java/android/view/ScrollCaptureTargetResolver.java b/core/java/android/view/ScrollCaptureTargetResolver.java
new file mode 100644
index 0000000..71e82c5
--- /dev/null
+++ b/core/java/android/view/ScrollCaptureTargetResolver.java
@@ -0,0 +1,387 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.AnyThread;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UiThread;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+
+import java.util.Queue;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+
+/**
+ * Queries additional state from a list of {@link ScrollCaptureTarget targets} via asynchronous
+ * callbacks, then aggregates and reduces the target list to a single target, or null if no target
+ * is suitable.
+ * <p>
+ * The rules for selection are (in order):
+ * <ul>
+ * <li>prefer getScrollBounds(): non-empty
+ * <li>prefer View.getScrollCaptureHint == SCROLL_CAPTURE_HINT_INCLUDE
+ * <li>prefer descendants before parents
+ * <li>prefer larger area for getScrollBounds() (clipped to view bounds)
+ * </ul>
+ *
+ * <p>
+ * All calls to {@link ScrollCaptureCallback#onScrollCaptureSearch} are made on the main thread,
+ * with results are queued and consumed to the main thread as well.
+ *
+ * @see #start(Handler, long, Consumer)
+ *
+ * @hide
+ */
+@UiThread
+public class ScrollCaptureTargetResolver {
+    private static final String TAG = "ScrollCaptureTargetRes";
+    private static final boolean DEBUG = true;
+
+    private final Object mLock = new Object();
+
+    private final Queue<ScrollCaptureTarget> mTargets;
+    private Handler mHandler;
+    private long mTimeLimitMillis;
+
+    private Consumer<ScrollCaptureTarget> mWhenComplete;
+    private int mPendingBoundsRequests;
+    private long mDeadlineMillis;
+
+    private ScrollCaptureTarget mResult;
+    private boolean mFinished;
+
+    private boolean mStarted;
+
+    private static int area(Rect r) {
+        return r.width() * r.height();
+    }
+
+    private static boolean nullOrEmpty(Rect r) {
+        return r == null || r.isEmpty();
+    }
+
+    /**
+     * Binary operator which selects the best {@link ScrollCaptureTarget}.
+     */
+    private static ScrollCaptureTarget chooseTarget(ScrollCaptureTarget a, ScrollCaptureTarget b) {
+        Log.d(TAG, "chooseTarget: " + a + " or " + b);
+        // Nothing plus nothing is still nothing.
+        if (a == null && b == null) {
+            Log.d(TAG, "chooseTarget: (both null) return " + null);
+            return null;
+        }
+        // Prefer non-null.
+        if (a == null || b == null) {
+            ScrollCaptureTarget c = (a == null) ? b : a;
+            Log.d(TAG, "chooseTarget: (other is null) return " + c);
+            return c;
+
+        }
+
+        boolean emptyScrollBoundsA = nullOrEmpty(a.getScrollBounds());
+        boolean emptyScrollBoundsB = nullOrEmpty(b.getScrollBounds());
+        if (emptyScrollBoundsA || emptyScrollBoundsB) {
+            if (emptyScrollBoundsA && emptyScrollBoundsB) {
+                // Both have an empty or null scrollBounds
+                Log.d(TAG, "chooseTarget: (both have empty or null bounds) return " + null);
+                return null;
+            }
+            // Prefer the one with a non-empty scroll bounds
+            if (emptyScrollBoundsA) {
+                Log.d(TAG, "chooseTarget: (a has empty or null bounds) return " + b);
+                return b;
+            }
+            Log.d(TAG, "chooseTarget: (b has empty or null bounds) return " + a);
+            return a;
+        }
+
+        final View viewA = a.getContainingView();
+        final View viewB = b.getContainingView();
+
+        // Prefer any view with scrollCaptureHint="INCLUDE", over one without
+        // This is an escape hatch for the next rule (descendants first)
+        boolean hintIncludeA = hasIncludeHint(viewA);
+        boolean hintIncludeB = hasIncludeHint(viewB);
+        if (hintIncludeA != hintIncludeB) {
+            ScrollCaptureTarget c = (hintIncludeA) ? a : b;
+            Log.d(TAG, "chooseTarget: (has hint=INCLUDE) return " + c);
+            return c;
+        }
+
+        // If the views are relatives, prefer the descendant. This allows implementations to
+        // leverage nested scrolling APIs by interacting with the innermost scrollable view (as
+        // would happen with touch input).
+        if (isDescendant(viewA, viewB)) {
+            Log.d(TAG, "chooseTarget: (b is descendant of a) return " + b);
+            return b;
+        }
+        if (isDescendant(viewB, viewA)) {
+            Log.d(TAG, "chooseTarget: (a is descendant of b) return " + a);
+            return a;
+        }
+
+        // finally, prefer one with larger scroll bounds
+        int scrollAreaA = area(a.getScrollBounds());
+        int scrollAreaB = area(b.getScrollBounds());
+        ScrollCaptureTarget c = (scrollAreaA >= scrollAreaB) ? a : b;
+        Log.d(TAG, "chooseTarget: return " + c);
+        return c;
+    }
+
+    /**
+     * Creates an instance to query and filter {@code target}.
+     *
+     * @param targets   a list of {@link ScrollCaptureTarget} as collected by {@link
+     *                  View#dispatchScrollCaptureSearch}.
+     * @param uiHandler the UI thread handler for the view tree
+     * @see #start(long, Consumer)
+     */
+    public ScrollCaptureTargetResolver(Queue<ScrollCaptureTarget> targets) {
+        mTargets = targets;
+    }
+
+    void checkThread() {
+        if (mHandler.getLooper() != Looper.myLooper()) {
+            throw new IllegalStateException("Called from wrong thread! ("
+                    + Thread.currentThread().getName() + ")");
+        }
+    }
+
+    /**
+     * Blocks until a result is returned (after completion or timeout).
+     * <p>
+     * For testing only. Normal usage should receive a callback after calling {@link #start}.
+     */
+    @VisibleForTesting
+    public ScrollCaptureTarget waitForResult() throws InterruptedException {
+        synchronized (mLock) {
+            while (!mFinished) {
+                mLock.wait();
+            }
+        }
+        return mResult;
+    }
+
+
+    private void supplyResult(ScrollCaptureTarget target) {
+        checkThread();
+        if (mFinished) {
+            return;
+        }
+        mResult = chooseTarget(mResult, target);
+        boolean finish = mPendingBoundsRequests == 0
+                || SystemClock.elapsedRealtime() >= mDeadlineMillis;
+        if (finish) {
+            System.err.println("We think we're done, or timed out");
+            mPendingBoundsRequests = 0;
+            mWhenComplete.accept(mResult);
+            synchronized (mLock) {
+                mFinished = true;
+                mLock.notify();
+            }
+            mWhenComplete = null;
+        }
+    }
+
+    /**
+     * Asks all targets for {@link ScrollCaptureCallback#onScrollCaptureSearch(Consumer)
+     * scrollBounds}, and selects the primary target according to the {@link
+     * #chooseTarget} function.
+     *
+     * @param timeLimitMillis the amount of time to wait for all responses before delivering the top
+     *                        result
+     * @param resultConsumer  the consumer to receive the primary target
+     */
+    @AnyThread
+    public void start(Handler uiHandler, long timeLimitMillis,
+            Consumer<ScrollCaptureTarget> resultConsumer) {
+        synchronized (mLock) {
+            if (mStarted) {
+                throw new IllegalStateException("already started!");
+            }
+            if (timeLimitMillis < 0) {
+                throw new IllegalArgumentException("Time limit must be positive");
+            }
+            mHandler = uiHandler;
+            mTimeLimitMillis = timeLimitMillis;
+            mWhenComplete = resultConsumer;
+            if (mTargets.isEmpty()) {
+                mHandler.post(() -> supplyResult(null));
+                return;
+            }
+            mStarted = true;
+            uiHandler.post(() -> run(timeLimitMillis, resultConsumer));
+        }
+    }
+
+
+    private void run(long timeLimitMillis, Consumer<ScrollCaptureTarget> resultConsumer) {
+        checkThread();
+
+        mPendingBoundsRequests = mTargets.size();
+        for (ScrollCaptureTarget target : mTargets) {
+            queryTarget(target);
+        }
+        mDeadlineMillis = SystemClock.elapsedRealtime() + mTimeLimitMillis;
+        mHandler.postAtTime(mTimeoutRunnable, mDeadlineMillis);
+    }
+
+    private final Runnable mTimeoutRunnable = new Runnable() {
+        @Override
+        public void run() {
+            checkThread();
+            supplyResult(null);
+        }
+    };
+
+
+    /**
+     * Adds a target to the list and requests {@link ScrollCaptureCallback#onScrollCaptureSearch}
+     * scrollBounds} from it. Results are returned by a call to {@link #onScrollBoundsProvided}.
+     *
+     * @param target the target to add
+     */
+    @UiThread
+    private void queryTarget(@NonNull ScrollCaptureTarget target) {
+        checkThread();
+        final ScrollCaptureCallback callback = target.getCallback();
+        // from the UI thread, request scroll bounds
+        callback.onScrollCaptureSearch(
+                // allow only one callback to onReady.accept():
+                new SingletonConsumer<Rect>(
+                        // Queue and consume on the UI thread
+                        ((scrollBounds) -> mHandler.post(
+                                () -> onScrollBoundsProvided(target, scrollBounds)))));
+
+    }
+
+    @UiThread
+    private void onScrollBoundsProvided(ScrollCaptureTarget target, @Nullable Rect scrollBounds) {
+        checkThread();
+        if (mFinished) {
+            return;
+        }
+
+        // Record progress.
+        mPendingBoundsRequests--;
+
+        // Remove the timeout.
+        mHandler.removeCallbacks(mTimeoutRunnable);
+
+        boolean doneOrTimedOut = mPendingBoundsRequests == 0
+                || SystemClock.elapsedRealtime() >= mDeadlineMillis;
+
+        final View containingView = target.getContainingView();
+        if (!nullOrEmpty(scrollBounds) && containingView.isAggregatedVisible()) {
+            target.updatePositionInWindow();
+            target.setScrollBounds(scrollBounds);
+            supplyResult(target);
+        }
+
+        System.err.println("mPendingBoundsRequests: " + mPendingBoundsRequests);
+        System.err.println("mDeadlineMillis: " + mDeadlineMillis);
+        System.err.println("SystemClock.elapsedRealtime(): " + SystemClock.elapsedRealtime());
+
+        if (!mFinished) {
+            // Reschedule the timeout.
+            System.err.println(
+                    "We think we're NOT done yet and will check back at " + mDeadlineMillis);
+            mHandler.postAtTime(mTimeoutRunnable, mDeadlineMillis);
+        }
+    }
+
+    private static boolean hasIncludeHint(View view) {
+        return (view.getScrollCaptureHint() & View.SCROLL_CAPTURE_HINT_INCLUDE) != 0;
+    }
+
+    /**
+     * Determines if {@code otherView} is a descendant of {@code view}.
+     *
+     * @param view      a view
+     * @param otherView another view
+     * @return true if {@code view} is an ancestor of {@code otherView}
+     */
+    private static boolean isDescendant(@NonNull View view, @NonNull View otherView) {
+        if (view == otherView) {
+            return false;
+        }
+        ViewParent otherParent = otherView.getParent();
+        while (otherParent != view && otherParent != null) {
+            otherParent = otherParent.getParent();
+        }
+        return otherParent == view;
+    }
+
+    private static int findRelation(@NonNull View a, @NonNull View b) {
+        if (a == b) {
+            return 0;
+        }
+
+        ViewParent parentA = a.getParent();
+        ViewParent parentB = b.getParent();
+
+        while (parentA != null || parentB != null) {
+            if (parentA == parentB) {
+                return 0;
+            }
+            if (parentA == b) {
+                return 1; // A is descendant of B
+            }
+            if (parentB == a) {
+                return -1; // B is descendant of A
+            }
+            if (parentA != null) {
+                parentA = parentA.getParent();
+            }
+            if (parentB != null) {
+                parentB = parentB.getParent();
+            }
+        }
+        return 0;
+    }
+
+    /**
+     * A safe wrapper for a consumer callbacks intended to accept a single value. It ensures
+     * that the receiver of the consumer does not retain a reference to {@code target} after use nor
+     * cause race conditions by invoking {@link Consumer#accept accept} more than once.
+     *
+     * @param target the target consumer
+     */
+    static class SingletonConsumer<T> implements Consumer<T> {
+        final AtomicReference<Consumer<T>> mAtomicRef;
+
+        SingletonConsumer(Consumer<T> target) {
+            mAtomicRef = new AtomicReference<>(target);
+        }
+
+        @Override
+        public void accept(T t) {
+            final Consumer<T> consumer = mAtomicRef.getAndSet(null);
+            if (consumer != null) {
+                consumer.accept(t);
+            }
+        }
+    }
+}
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 8abe72f..f98c1f6 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -144,6 +144,7 @@
 
 import com.android.internal.R;
 import com.android.internal.util.FrameworkStatsLog;
+import com.android.internal.view.ScrollCaptureInternal;
 import com.android.internal.view.TooltipPopup;
 import com.android.internal.view.menu.MenuBuilder;
 import com.android.internal.widget.ScrollBarUtils;
@@ -167,6 +168,7 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Queue;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Predicate;
@@ -1311,7 +1313,6 @@
      */
     public static final int AUTOFILL_TYPE_LIST = 3;
 
-
     /**
      * Autofill type for a field that contains a date, which is represented by a long representing
      * the number of milliseconds since the standard base time known as "the epoch", namely
@@ -1441,6 +1442,58 @@
      */
     public static final int IMPORTANT_FOR_CONTENT_CAPTURE_NO_EXCLUDE_DESCENDANTS = 0x8;
 
+    /** {@hide} */
+    @IntDef(flag = true, prefix = {"SCROLL_CAPTURE_HINT_"},
+            value = {
+                    SCROLL_CAPTURE_HINT_AUTO,
+                    SCROLL_CAPTURE_HINT_EXCLUDE,
+                    SCROLL_CAPTURE_HINT_INCLUDE,
+                    SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ScrollCaptureHint {}
+
+    /**
+     * The content of this view will be considered for scroll capture if scrolling is possible.
+     *
+     * @see #getScrollCaptureHint()
+     * @see #setScrollCaptureHint(int)
+     * @hide
+     */
+    public static final int SCROLL_CAPTURE_HINT_AUTO = 0;
+
+    /**
+     * Explicitly exclcude this view as a potential scroll capture target. The system will not
+     * consider it. Mutually exclusive with {@link #SCROLL_CAPTURE_HINT_INCLUDE}, which this flag
+     * takes precedence over.
+     *
+     * @see #getScrollCaptureHint()
+     * @see #setScrollCaptureHint(int)
+     * @hide
+     */
+    public static final int SCROLL_CAPTURE_HINT_EXCLUDE = 0x1;
+
+    /**
+     * Explicitly include this view as a potential scroll capture target. When locating a scroll
+     * capture target, this view will be prioritized before others without this flag. Mutually
+     * exclusive with {@link #SCROLL_CAPTURE_HINT_EXCLUDE}, which takes precedence.
+     *
+     * @see #getScrollCaptureHint()
+     * @see #setScrollCaptureHint(int)
+     * @hide
+     */
+    public static final int SCROLL_CAPTURE_HINT_INCLUDE = 0x2;
+
+    /**
+     * Explicitly exclude all children of this view as potential scroll capture targets. This view
+     * is unaffected. Note: Excluded children are not considered, regardless of {@link
+     * #SCROLL_CAPTURE_HINT_INCLUDE}.
+     *
+     * @see #getScrollCaptureHint()
+     * @see #setScrollCaptureHint(int)
+     * @hide
+     */
+    public static final int SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS = 0x4;
 
     /**
      * This view is enabled. Interpretation varies by subclass.
@@ -3430,6 +3483,7 @@
      *                         11       PFLAG4_CONTENT_CAPTURE_IMPORTANCE_MASK
      *                        1         PFLAG4_FRAMEWORK_OPTIONAL_FITS_SYSTEM_WINDOWS
      *                       1          PFLAG4_AUTOFILL_HIDE_HIGHLIGHT
+     *                     11           PFLAG4_SCROLL_CAPTURE_HINT_MASK
      * |-------|-------|-------|-------|
      */
 
@@ -3477,6 +3531,15 @@
      */
     private static final int PFLAG4_AUTOFILL_HIDE_HIGHLIGHT = 0x200;
 
+    /**
+     * Shift for the bits in {@link #mPrivateFlags4} related to scroll capture.
+     */
+    static final int PFLAG4_SCROLL_CAPTURE_HINT_SHIFT = 10;
+
+    static final int PFLAG4_SCROLL_CAPTURE_HINT_MASK = (SCROLL_CAPTURE_HINT_INCLUDE
+            | SCROLL_CAPTURE_HINT_EXCLUDE | SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS)
+            << PFLAG4_SCROLL_CAPTURE_HINT_SHIFT;
+
     /* End of masks for mPrivateFlags4 */
 
     /** @hide */
@@ -4690,6 +4753,11 @@
          * Used to track {@link #mSystemGestureExclusionRects}
          */
         public RenderNode.PositionUpdateListener mPositionUpdateListener;
+
+        /**
+         * Allows the application to implement custom scroll capture support.
+         */
+        ScrollCaptureCallback mScrollCaptureCallback;
     }
 
     @UnsupportedAppUsage
@@ -5941,6 +6009,9 @@
                 case R.styleable.View_forceDarkAllowed:
                     mRenderNode.setForceDarkAllowed(a.getBoolean(attr, true));
                     break;
+                case R.styleable.View_scrollCaptureHint:
+                    setScrollCaptureHint((a.getInt(attr, SCROLL_CAPTURE_HINT_AUTO)));
+                    break;
             }
         }
 
@@ -29091,6 +29162,11 @@
         int mLeashedParentAccessibilityViewId;
 
         /**
+         *
+         */
+        ScrollCaptureInternal mScrollCaptureInternal;
+
+        /**
          * Creates a new set of attachment information with the specified
          * events handler and thread.
          *
@@ -29150,6 +29226,14 @@
 
             return events;
         }
+
+        @Nullable
+        ScrollCaptureInternal getScrollCaptureInternal() {
+            if (mScrollCaptureInternal != null) {
+                mScrollCaptureInternal = new ScrollCaptureInternal();
+            }
+            return mScrollCaptureInternal;
+        }
     }
 
     /**
@@ -29683,6 +29767,104 @@
         }
     }
 
+
+    /**
+     * Returns the current scroll capture hint for this view.
+     *
+     * @return the current scroll capture hint
+     *
+     * @hide
+     */
+    @ScrollCaptureHint
+    public int getScrollCaptureHint() {
+        return (mPrivateFlags4 & PFLAG4_SCROLL_CAPTURE_HINT_MASK)
+                >> PFLAG4_SCROLL_CAPTURE_HINT_SHIFT;
+    }
+
+    /**
+     * Sets the scroll capture hint for this View. These flags affect the search for a potential
+     * scroll capture targets.
+     *
+     * @param hint the scrollCaptureHint flags value to set
+     *
+     * @hide
+     */
+    public void setScrollCaptureHint(@ScrollCaptureHint int hint) {
+        mPrivateFlags4 &= ~PFLAG4_SCROLL_CAPTURE_HINT_MASK;
+        mPrivateFlags4 |= ((hint << PFLAG4_SCROLL_CAPTURE_HINT_SHIFT)
+                & PFLAG4_SCROLL_CAPTURE_HINT_MASK);
+    }
+
+    /**
+     * Sets the callback to receive scroll capture requests. This component is the adapter between
+     * the scroll capture API and application UI code. If no callback is set, the system may provide
+     * an implementation. Any value provided here will take precedence over a system version.
+     * <p>
+     * This view will be ignored when {@link #SCROLL_CAPTURE_HINT_EXCLUDE} is set in its {@link
+     * #setScrollCaptureHint(int) scrollCaptureHint}, regardless whether a callback has been set.
+     * <p>
+     * It is recommended to set the scroll capture hint {@link #SCROLL_CAPTURE_HINT_INCLUDE} when
+     * setting a custom callback to help ensure it is selected as the target.
+     *
+     * @param callback the new callback to assign
+     *
+     * @hide
+     */
+    public void setScrollCaptureCallback(@Nullable ScrollCaptureCallback callback) {
+        getListenerInfo().mScrollCaptureCallback = callback;
+    }
+
+    /** {@hide} */
+    @Nullable
+    public ScrollCaptureCallback createScrollCaptureCallbackInternal(@NonNull Rect localVisibleRect,
+            @NonNull Point windowOffset) {
+        if (mAttachInfo == null) {
+            return null;
+        }
+        if (mAttachInfo.mScrollCaptureInternal == null) {
+            mAttachInfo.mScrollCaptureInternal = new ScrollCaptureInternal();
+        }
+        return mAttachInfo.mScrollCaptureInternal.requestCallback(this, localVisibleRect,
+                windowOffset);
+    }
+
+    /**
+     * Called when scroll capture is requested, to search for appropriate content to scroll. If
+     * applicable, this view adds itself to the provided list for consideration, subject to the
+     * flags set by {@link #setScrollCaptureHint}.
+     *
+     * @param localVisibleRect the local visible rect of this view
+     * @param windowOffset     the offset of localVisibleRect within the window
+     * @param targets          a queue which collects potential targets
+     *
+     * @throws IllegalStateException if this view is not attached to a window
+     * @hide
+     */
+    public void dispatchScrollCaptureSearch(@NonNull Rect localVisibleRect,
+            @NonNull Point windowOffset, @NonNull Queue<ScrollCaptureTarget> targets) {
+        int hint = getScrollCaptureHint();
+        if ((hint & SCROLL_CAPTURE_HINT_EXCLUDE) != 0) {
+            return;
+        }
+
+        // Get a callback provided by the framework, library or application.
+        ScrollCaptureCallback callback =
+                (mListenerInfo == null) ? null : mListenerInfo.mScrollCaptureCallback;
+
+        // Try internal support for standard scrolling containers.
+        if (callback == null) {
+            callback = createScrollCaptureCallbackInternal(localVisibleRect, windowOffset);
+        }
+
+        // If found, then add it to the list.
+        if (callback != null) {
+            // Add to the list for consideration
+            Point offset = new Point(windowOffset.x, windowOffset.y);
+            Rect rect = new Rect(localVisibleRect);
+            targets.add(new ScrollCaptureTarget(this, rect, offset, callback));
+        }
+    }
+
     /**
      * Dump all private flags in readable format, useful for documentation and
      * sanity checking.
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index e34e84c..7935eb1 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -40,6 +40,7 @@
 import android.graphics.Insets;
 import android.graphics.Matrix;
 import android.graphics.Paint;
+import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.RectF;
@@ -75,6 +76,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Queue;
 import java.util.function.Predicate;
 
 /**
@@ -188,7 +190,16 @@
     private PointF mLocalPoint;
 
     // Lazily-created holder for point computations.
-    private float[] mTempPoint;
+    private float[] mTempPosition;
+
+    // Lazily-created holder for point computations.
+    private Point mTempPoint;
+
+    // Lazily created Rect for dispatch to children
+    private Rect mTempRect;
+
+    // Lazily created int[2] for dispatch to children
+    private int[] mTempLocation;
 
     // Layout animation
     private LayoutAnimationController mLayoutAnimationController;
@@ -1860,7 +1871,7 @@
         final float tx = mCurrentDragStartEvent.mX;
         final float ty = mCurrentDragStartEvent.mY;
 
-        final float[] point = getTempPoint();
+        final float[] point = getTempLocationF();
         point[0] = tx;
         point[1] = ty;
         transformPointToViewLocal(point, child);
@@ -2932,9 +2943,23 @@
         }
     }
 
-    private float[] getTempPoint() {
+    private Rect getTempRect() {
+        if (mTempRect == null) {
+            mTempRect = new Rect();
+        }
+        return mTempRect;
+    }
+
+    private float[] getTempLocationF() {
+        if (mTempPosition == null) {
+            mTempPosition = new float[2];
+        }
+        return mTempPosition;
+    }
+
+    private Point getTempPoint() {
         if (mTempPoint == null) {
-            mTempPoint = new float[2];
+            mTempPoint = new Point();
         }
         return mTempPoint;
     }
@@ -2948,7 +2973,7 @@
     @UnsupportedAppUsage
     protected boolean isTransformedTouchPointInView(float x, float y, View child,
             PointF outLocalPoint) {
-        final float[] point = getTempPoint();
+        final float[] point = getTempLocationF();
         point[0] = x;
         point[1] = y;
         transformPointToViewLocal(point, child);
@@ -4568,7 +4593,7 @@
             final boolean nonActionable = !child.isClickable() && !child.isLongClickable();
             final boolean duplicatesState = (child.mViewFlags & DUPLICATE_PARENT_STATE) != 0;
             if (nonActionable || duplicatesState) {
-                final float[] point = getTempPoint();
+                final float[] point = getTempLocationF();
                 point[0] = x;
                 point[1] = y;
                 transformPointToViewLocal(point, child);
@@ -7354,6 +7379,97 @@
     }
 
     /**
+     * Offsets the given rectangle in parent's local coordinates into child's coordinate space
+     * and clips the result to the child View's bounds, padding and clipRect if appropriate. If the
+     * resulting rectangle is not empty, the request is forwarded to the child.
+     * <p>
+     * Note: This method does not account for any static View transformations which may be
+     * applied to the child view.
+     *
+     * @param child            the child to dispatch to
+     * @param localVisibleRect the visible (clipped) area of this ViewGroup, in local coordinates
+     * @param windowOffset     the offset of localVisibleRect within the window
+     * @param targets          a queue to collect located targets
+     */
+    private void dispatchTransformedScrollCaptureSearch(View child, Rect localVisibleRect,
+            Point windowOffset, Queue<ScrollCaptureTarget> targets) {
+
+        // copy local visible rect for modification and dispatch
+        final Rect childVisibleRect = getTempRect();
+        childVisibleRect.set(localVisibleRect);
+
+        // transform to child coords
+        final Point childWindowOffset = getTempPoint();
+        childWindowOffset.set(windowOffset.x, windowOffset.y);
+
+        final int dx = child.mLeft - mScrollX;
+        final int dy = child.mTop - mScrollY;
+
+        childVisibleRect.offset(-dx, -dy);
+        childWindowOffset.offset(dx, dy);
+
+        boolean rectIsVisible = true;
+        final int width = mRight - mLeft;
+        final int height = mBottom - mTop;
+
+        // Clip to child bounds
+        if (getClipChildren()) {
+            rectIsVisible = childVisibleRect.intersect(0, 0, child.getWidth(), child.getHeight());
+        }
+
+        // Clip to child padding.
+        if (rectIsVisible && (child instanceof ViewGroup)
+                && ((ViewGroup) child).getClipToPadding()) {
+            rectIsVisible = childVisibleRect.intersect(
+                    child.mPaddingLeft, child.mPaddingTop,
+                    child.getWidth() - child.mPaddingRight,
+                    child.getHeight() - child.mPaddingBottom);
+        }
+        // Clip to child clipBounds.
+        if (rectIsVisible && child.mClipBounds != null) {
+            rectIsVisible = childVisibleRect.intersect(child.mClipBounds);
+        }
+        if (rectIsVisible) {
+            child.dispatchScrollCaptureSearch(childVisibleRect, childWindowOffset, targets);
+        }
+    }
+
+    /**
+     * Handle the scroll capture search request by checking this view if applicable, then to each
+     * child view.
+     *
+     * @param localVisibleRect the visible area of this ViewGroup in local coordinates, according to
+     *                         the parent
+     * @param windowOffset     the offset of this view within the window
+     * @param targets          the collected list of scroll capture targets
+     *
+     * @hide
+     */
+    @Override
+    public void dispatchScrollCaptureSearch(
+            @NonNull Rect localVisibleRect, @NonNull Point windowOffset,
+            @NonNull Queue<ScrollCaptureTarget> targets) {
+
+        // Dispatch to self first.
+        super.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targets);
+
+        // Then dispatch to children, if not excluding descendants.
+        if ((getScrollCaptureHint() & SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS) == 0) {
+            final int childCount = getChildCount();
+            for (int i = 0; i < childCount; i++) {
+                View child = getChildAt(i);
+                // Only visible views can be captured.
+                if (child.getVisibility() != View.VISIBLE) {
+                    continue;
+                }
+                // Transform to child coords and dispatch
+                dispatchTransformedScrollCaptureSearch(child, localVisibleRect, windowOffset,
+                        targets);
+            }
+        }
+    }
+
+    /**
      * Returns the animation listener to which layout animation events are
      * sent.
      *
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index ed1edc3..9d275cd 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -206,6 +206,7 @@
     private static final boolean DEBUG_INPUT_STAGES = false || LOCAL_LOGV;
     private static final boolean DEBUG_KEEP_SCREEN_ON = false || LOCAL_LOGV;
     private static final boolean DEBUG_CONTENT_CAPTURE = false || LOCAL_LOGV;
+    private static final boolean DEBUG_SCROLL_CAPTURE = false || LOCAL_LOGV;
 
     /**
      * Set to false if we do not want to use the multi threaded renderer even though
@@ -653,6 +654,8 @@
     private final InsetsController mInsetsController;
     private final ImeFocusController mImeFocusController;
 
+    private ScrollCaptureClient mScrollCaptureClient;
+
     /**
      * @return {@link ImeFocusController} for this instance.
      */
@@ -661,6 +664,11 @@
         return mImeFocusController;
     }
 
+    /** @return The current {@link ScrollCaptureClient} for this instance, if any is active. */
+    @Nullable
+    public ScrollCaptureClient getScrollCaptureClient() {
+        return mScrollCaptureClient;
+    }
 
     private final GestureExclusionTracker mGestureExclusionTracker = new GestureExclusionTracker();
 
@@ -694,6 +702,8 @@
     // draw returns.
     private SurfaceControl.Transaction mRtBLASTSyncTransaction = new SurfaceControl.Transaction();
 
+    private HashSet<ScrollCaptureCallback> mRootScrollCaptureCallbacks;
+
     private String mTag = TAG;
 
     public ViewRootImpl(Context context, Display display) {
@@ -3769,7 +3779,9 @@
                 mNextReportConsumeBLAST = true;
                 mNextDrawUseBLASTSyncTransaction = false;
 
-                mBlastBufferQueue.setNextTransaction(mRtBLASTSyncTransaction);
+                if (mBlastBufferQueue != null) {
+                    mBlastBufferQueue.setNextTransaction(mRtBLASTSyncTransaction);
+                }
             }
             boolean canUseAsync = draw(fullRedrawNeeded);
             if (usingAsyncReport && !canUseAsync) {
@@ -4778,6 +4790,7 @@
     private static final int MSG_LOCATION_IN_PARENT_DISPLAY_CHANGED = 33;
     private static final int MSG_SHOW_INSETS = 34;
     private static final int MSG_HIDE_INSETS = 35;
+    private static final int MSG_REQUEST_SCROLL_CAPTURE = 36;
 
 
     final class ViewRootHandler extends Handler {
@@ -5080,6 +5093,9 @@
                 case MSG_LOCATION_IN_PARENT_DISPLAY_CHANGED: {
                     updateLocationInParentDisplay(msg.arg1, msg.arg2);
                 } break;
+                case MSG_REQUEST_SCROLL_CAPTURE:
+                    handleScrollCaptureRequest((IScrollCaptureController) msg.obj);
+                    break;
             }
         }
     }
@@ -8789,6 +8805,131 @@
         return false;
     }
 
+    /**
+     * Adds a scroll capture callback to this window.
+     *
+     * @param callback the callback to add
+     */
+    public void addScrollCaptureCallback(ScrollCaptureCallback callback) {
+        if (mRootScrollCaptureCallbacks == null) {
+            mRootScrollCaptureCallbacks = new HashSet<>();
+        }
+        mRootScrollCaptureCallbacks.add(callback);
+    }
+
+    /**
+     * Removes a scroll capture callback from this window.
+     *
+     * @param callback the callback to remove
+     */
+    public void removeScrollCaptureCallback(ScrollCaptureCallback callback) {
+        if (mRootScrollCaptureCallbacks != null) {
+            mRootScrollCaptureCallbacks.remove(callback);
+            if (mRootScrollCaptureCallbacks.isEmpty()) {
+                mRootScrollCaptureCallbacks = null;
+            }
+        }
+    }
+
+    /**
+     * Dispatches a scroll capture request to the view hierarchy on the ui thread.
+     *
+     * @param controller the controller to receive replies
+     */
+    public void dispatchScrollCaptureRequest(@NonNull IScrollCaptureController controller) {
+        mHandler.obtainMessage(MSG_REQUEST_SCROLL_CAPTURE, controller).sendToTarget();
+    }
+
+    /**
+     * Collect and include any ScrollCaptureCallback instances registered with the window.
+     *
+     * @see #addScrollCaptureCallback(ScrollCaptureCallback)
+     * @param targets the search queue for targets
+     */
+    private void collectRootScrollCaptureTargets(Queue<ScrollCaptureTarget> targets) {
+        for (ScrollCaptureCallback cb : mRootScrollCaptureCallbacks) {
+            // Add to the list for consideration
+            Point offset = new Point(mView.getLeft(), mView.getTop());
+            Rect rect = new Rect(0, 0, mView.getWidth(), mView.getHeight());
+            targets.add(new ScrollCaptureTarget(mView, rect, offset, cb));
+        }
+    }
+
+    /**
+     * Handles an inbound request for scroll capture from the system. If a client is not already
+     * active, a search will be dispatched through the view tree to locate scrolling content.
+     * <p>
+     * Either {@link IScrollCaptureController#onClientConnected(IScrollCaptureClient, Rect,
+     * Point)} or {@link IScrollCaptureController#onClientUnavailable()} will be returned
+     * depending on the results of the search.
+     *
+     * @param controller the interface to the system controller
+     * @see ScrollCaptureTargetResolver
+     */
+    private void handleScrollCaptureRequest(@NonNull IScrollCaptureController controller) {
+        LinkedList<ScrollCaptureTarget> targetList = new LinkedList<>();
+
+        // Window (root) level callbacks
+        collectRootScrollCaptureTargets(targetList);
+
+        // Search through View-tree
+        View rootView = getView();
+        Point point = new Point();
+        Rect rect = new Rect(0, 0, rootView.getWidth(), rootView.getHeight());
+        getChildVisibleRect(rootView, rect, point);
+        rootView.dispatchScrollCaptureSearch(rect, point, targetList);
+
+        // No-op path. Scroll capture not offered for this window.
+        if (targetList.isEmpty()) {
+            dispatchScrollCaptureSearchResult(controller, null);
+            return;
+        }
+
+        // Request scrollBounds from each of the targets.
+        // Continues with the consumer once all responses are consumed, or the timeout expires.
+        ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetList);
+        resolver.start(mHandler, 1000,
+                (selected) -> dispatchScrollCaptureSearchResult(controller, selected));
+    }
+
+    /** Called by {@link #handleScrollCaptureRequest} when a result is returned */
+    private void dispatchScrollCaptureSearchResult(
+            @NonNull IScrollCaptureController controller,
+            @Nullable ScrollCaptureTarget selectedTarget) {
+
+        // If timeout or no eligible targets found.
+        if (selectedTarget == null) {
+            try {
+                if (DEBUG_SCROLL_CAPTURE) {
+                    Log.d(TAG, "scrollCaptureSearch returned no targets available.");
+                }
+                controller.onClientUnavailable();
+            } catch (RemoteException e) {
+                if (DEBUG_SCROLL_CAPTURE) {
+                    Log.w(TAG, "Failed to notify controller of scroll capture search result.", e);
+                }
+            }
+            return;
+        }
+
+        // Create a client instance and return it to the caller
+        mScrollCaptureClient = new ScrollCaptureClient(selectedTarget, controller);
+        try {
+            if (DEBUG_SCROLL_CAPTURE) {
+                Log.d(TAG, "scrollCaptureSearch returning client: " + getScrollCaptureClient());
+            }
+            controller.onClientConnected(
+                    mScrollCaptureClient,
+                    selectedTarget.getScrollBounds(),
+                    selectedTarget.getPositionInWindow());
+        } catch (RemoteException e) {
+            if (DEBUG_SCROLL_CAPTURE) {
+                Log.w(TAG, "Failed to notify controller of scroll capture search result.", e);
+            }
+            mScrollCaptureClient.disconnect();
+            mScrollCaptureClient = null;
+        }
+    }
 
     private void reportNextDraw() {
         if (mReportNextDraw == false) {
@@ -9091,6 +9232,13 @@
             }
         }
 
+        @Override
+        public void requestScrollCapture(IScrollCaptureController controller) {
+            final ViewRootImpl viewAncestor = mViewAncestor.get();
+            if (viewAncestor != null) {
+                viewAncestor.dispatchScrollCaptureRequest(controller);
+            }
+        }
     }
 
     public static final class CalledFromWrongThreadException extends AndroidRuntimeException {
diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java
index ae9afaba..b153648 100644
--- a/core/java/android/view/Window.java
+++ b/core/java/android/view/Window.java
@@ -2535,6 +2535,33 @@
         return Collections.emptyList();
     }
 
+    /**
+     * System request to begin scroll capture.
+     *
+     * @param controller the controller to receive responses
+     * @hide
+     */
+    public void requestScrollCapture(IScrollCaptureController controller) {
+    }
+
+    /**
+     * Registers a {@link ScrollCaptureCallback} with the root of this window.
+     *
+     * @param callback the callback to add
+     * @hide
+     */
+    public void addScrollCaptureCallback(@NonNull ScrollCaptureCallback callback) {
+    }
+
+    /**
+     * Unregisters a {@link ScrollCaptureCallback} previously registered with this window.
+     *
+     * @param callback the callback to remove
+     * @hide
+     */
+    public void removeScrollCaptureCallback(@NonNull ScrollCaptureCallback callback) {
+    }
+
     /** @hide */
     public void setTheme(int resId) {
     }
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index cc380f3..e4dbd637 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -514,33 +514,24 @@
     int TAKE_SCREENSHOT_PROVIDED_IMAGE = 3;
 
     /**
-     * Parcel key for the screen shot bitmap sent with messages of type
-     * {@link #TAKE_SCREENSHOT_PROVIDED_IMAGE}, type {@link android.graphics.Bitmap}
+     * Enum listing the possible sources from which a screenshot was originated. Used for logging.
+     *
      * @hide
      */
-    String PARCEL_KEY_SCREENSHOT_BITMAP = "screenshot_screen_bitmap";
-
-    /**
-     * Parcel key for the screen bounds of the image sent with messages of type
-     * [@link {@link #TAKE_SCREENSHOT_PROVIDED_IMAGE}], type {@link Rect} in screen coordinates.
-     * @hide
-     */
-    String PARCEL_KEY_SCREENSHOT_BOUNDS = "screenshot_screen_bounds";
-
-    /**
-     * Parcel key for the task id of the task that the screen shot was taken of, sent with messages
-     * of type [@link {@link #TAKE_SCREENSHOT_PROVIDED_IMAGE}], type int.
-     * @hide
-     */
-    String PARCEL_KEY_SCREENSHOT_TASK_ID = "screenshot_task_id";
-
-    /**
-     * Parcel key for the visible insets of the image sent with messages of type
-     * [@link {@link #TAKE_SCREENSHOT_PROVIDED_IMAGE}], type {@link android.graphics.Insets} in
-     * screen coordinates.
-     * @hide
-     */
-    String PARCEL_KEY_SCREENSHOT_INSETS = "screenshot_insets";
+    @IntDef({ScreenshotSource.SCREENSHOT_GLOBAL_ACTIONS,
+            ScreenshotSource.SCREENSHOT_KEY_CHORD,
+            ScreenshotSource.SCREENSHOT_KEY_OTHER,
+            ScreenshotSource.SCREENSHOT_OVERVIEW,
+            ScreenshotSource.SCREENSHOT_ACCESSIBILITY_ACTIONS,
+            ScreenshotSource.SCREENSHOT_OTHER})
+    @interface ScreenshotSource {
+        int SCREENSHOT_GLOBAL_ACTIONS = 0;
+        int SCREENSHOT_KEY_CHORD = 1;
+        int SCREENSHOT_KEY_OTHER = 2;
+        int SCREENSHOT_OVERVIEW = 3;
+        int SCREENSHOT_ACCESSIBILITY_ACTIONS = 4;
+        int SCREENSHOT_OTHER = 5;
+    }
 
     /**
      * @hide
diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java
index ec51301..397bce4 100644
--- a/core/java/android/view/WindowlessWindowManager.java
+++ b/core/java/android/view/WindowlessWindowManager.java
@@ -24,7 +24,6 @@
 import android.os.RemoteException;
 import android.util.Log;
 import android.util.MergedConfiguration;
-import android.view.IWindowSession;
 
 import java.util.HashMap;
 
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 7f6c0d2..7016c5c 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -1213,7 +1213,7 @@
 
         BitmapReflectionAction(Parcel in) {
             viewId = in.readInt();
-            methodName = in.readString();
+            methodName = in.readString8();
             bitmapId = in.readInt();
             bitmap = mBitmapCache.getBitmapForId(bitmapId);
         }
@@ -1221,7 +1221,7 @@
         @Override
         public void writeToParcel(Parcel dest, int flags) {
             dest.writeInt(viewId);
-            dest.writeString(methodName);
+            dest.writeString8(methodName);
             dest.writeInt(bitmapId);
         }
 
@@ -1282,7 +1282,7 @@
 
         ReflectionAction(Parcel in) {
             this.viewId = in.readInt();
-            this.methodName = in.readString();
+            this.methodName = in.readString8();
             this.type = in.readInt();
             //noinspection ConstantIfStatement
             if (false) {
@@ -1318,7 +1318,7 @@
                     this.value = (char)in.readInt();
                     break;
                 case STRING:
-                    this.value = in.readString();
+                    this.value = in.readString8();
                     break;
                 case CHAR_SEQUENCE:
                     this.value = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
@@ -1347,7 +1347,7 @@
 
         public void writeToParcel(Parcel out, int flags) {
             out.writeInt(this.viewId);
-            out.writeString(this.methodName);
+            out.writeString8(this.methodName);
             out.writeInt(this.type);
             //noinspection ConstantIfStatement
             if (false) {
@@ -1383,7 +1383,7 @@
                     out.writeInt((int)((Character)this.value).charValue());
                     break;
                 case STRING:
-                    out.writeString((String)this.value);
+                    out.writeString8((String)this.value);
                     break;
                 case CHAR_SEQUENCE:
                     TextUtils.writeToParcel((CharSequence)this.value, out, flags);
diff --git a/core/java/com/android/internal/app/AbstractMultiProfilePagerAdapter.java b/core/java/com/android/internal/app/AbstractMultiProfilePagerAdapter.java
index b1e356d..bcb32fb 100644
--- a/core/java/com/android/internal/app/AbstractMultiProfilePagerAdapter.java
+++ b/core/java/com/android/internal/app/AbstractMultiProfilePagerAdapter.java
@@ -437,6 +437,9 @@
         resetViewVisibilitiesForWorkProfileEmptyState(emptyStateView);
         emptyStateView.setVisibility(View.VISIBLE);
 
+        View container = emptyStateView.findViewById(R.id.resolver_empty_state_container);
+        setupContainerPadding(container);
+
         TextView title = emptyStateView.findViewById(R.id.resolver_empty_state_title);
         title.setText(titleRes);
 
@@ -463,6 +466,12 @@
         activeListAdapter.markTabLoaded();
     }
 
+    /**
+     * Sets up the padding of the view containing the empty state screens.
+     * <p>This method is meant to be overridden so that subclasses can customize the padding.
+     */
+    protected void setupContainerPadding(View container) {}
+
     private void showConsumerUserNoAppsAvailableEmptyState(ResolverListAdapter activeListAdapter) {
         ProfileDescriptor descriptor = getItem(
                 userHandleToPageIndex(activeListAdapter.getUserHandle()));
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index 970bab9..2fc75a7 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -2885,6 +2885,13 @@
         return METRICS_CATEGORY_CHOOSER;
     }
 
+    @Override
+    protected void onProfileTabSelected() {
+        ChooserGridAdapter currentRootAdapter =
+                mChooserMultiProfilePagerAdapter.getCurrentRootAdapter();
+        currentRootAdapter.updateDirectShareExpansion();
+    }
+
     /**
      * Adapter for all types of items and targets in ShareSheet.
      * Note that ranked sections like Direct Share - while appearing grid-like - are handled on the
@@ -3357,15 +3364,7 @@
         }
 
         public void handleScroll(View v, int y, int oldy) {
-            // Only expand direct share area if there is a minimum number of shortcuts,
-            // which will help reduce the amount of visible shuffling due to older-style
-            // direct share targets.
-            int orientation = getResources().getConfiguration().orientation;
-            boolean canExpandDirectShare =
-                    mChooserListAdapter.getNumShortcutResults() > getMaxTargetsPerRow()
-                    && orientation == Configuration.ORIENTATION_PORTRAIT
-                    && !isInMultiWindowMode();
-
+            boolean canExpandDirectShare = canExpandDirectShare();
             if (mDirectShareViewHolder != null && canExpandDirectShare) {
                 mDirectShareViewHolder.handleScroll(
                         mChooserMultiProfilePagerAdapter.getActiveAdapterView(), y, oldy,
@@ -3373,6 +3372,18 @@
             }
         }
 
+        /**
+         * Only expand direct share area if there is a minimum number of shortcuts,
+         * which will help reduce the amount of visible shuffling due to older-style
+         * direct share targets.
+         */
+        private boolean canExpandDirectShare() {
+            int orientation = getResources().getConfiguration().orientation;
+            return mChooserListAdapter.getNumShortcutResults() > getMaxTargetsPerRow()
+                    && orientation == Configuration.ORIENTATION_PORTRAIT
+                    && !isInMultiWindowMode();
+        }
+
         public ChooserListAdapter getListAdapter() {
             return mChooserListAdapter;
         }
@@ -3380,6 +3391,19 @@
         boolean shouldCellSpan(int position) {
             return getItemViewType(position) == VIEW_TYPE_NORMAL;
         }
+
+        void updateDirectShareExpansion() {
+            if (mDirectShareViewHolder == null || !canExpandDirectShare()) {
+                return;
+            }
+            RecyclerView activeAdapterView =
+                    mChooserMultiProfilePagerAdapter.getActiveAdapterView();
+            if (mResolverDrawerLayout.isCollapsed()) {
+                mDirectShareViewHolder.collapse(activeAdapterView);
+            } else {
+                mDirectShareViewHolder.expand(activeAdapterView);
+            }
+        }
     }
 
     /**
@@ -3577,6 +3601,20 @@
             newHeight = Math.max(newHeight, mDirectShareMinHeight);
             yDiff = newHeight - prevHeight;
 
+            updateDirectShareRowHeight(view, yDiff, newHeight);
+        }
+
+        void expand(RecyclerView view) {
+            updateDirectShareRowHeight(view, mDirectShareMaxHeight - mDirectShareCurrHeight,
+                    mDirectShareMaxHeight);
+        }
+
+        void collapse(RecyclerView view) {
+            updateDirectShareRowHeight(view, mDirectShareMinHeight - mDirectShareCurrHeight,
+                    mDirectShareMinHeight);
+        }
+
+        private void updateDirectShareRowHeight(RecyclerView view, int yDiff, int newHeight) {
             if (view == null || view.getChildCount() == 0 || yDiff == 0) {
                 return;
             }
diff --git a/core/java/com/android/internal/app/IntentForwarderActivity.java b/core/java/com/android/internal/app/IntentForwarderActivity.java
index 3eb0923..2c48925 100644
--- a/core/java/com/android/internal/app/IntentForwarderActivity.java
+++ b/core/java/com/android/internal/app/IntentForwarderActivity.java
@@ -50,6 +50,9 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 
 /**
  * This is used in conjunction with
@@ -74,11 +77,13 @@
     private Injector mInjector;
 
     private MetricsLogger mMetricsLogger;
+    protected ExecutorService mExecutorService;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         mInjector = createInjector();
+        mExecutorService = Executors.newSingleThreadExecutor();
 
         Intent intentReceived = getIntent();
         String className = intentReceived.getComponent().getClassName();
@@ -118,30 +123,9 @@
                 mInjector.getIPackageManager(), getContentResolver());
         if (newIntent != null) {
             newIntent.prepareToLeaveUser(callingUserId);
-
-            final ResolveInfo ri = mInjector.resolveActivityAsUser(newIntent, MATCH_DEFAULT_ONLY,
-                    targetUserId);
-            try {
-                startActivityAsCaller(newIntent, null, null, false, targetUserId);
-            } catch (RuntimeException e) {
-                int launchedFromUid = -1;
-                String launchedFromPackage = "?";
-                try {
-                    launchedFromUid = ActivityTaskManager.getService().getLaunchedFromUid(
-                            getActivityToken());
-                    launchedFromPackage = ActivityTaskManager.getService().getLaunchedFromPackage(
-                            getActivityToken());
-                } catch (RemoteException ignored) {
-                }
-
-                Slog.wtf(TAG, "Unable to launch as UID " + launchedFromUid + " package "
-                        + launchedFromPackage + ", while running in "
-                        + ActivityThread.currentProcessName(), e);
-            }
-
-            if (shouldShowDisclosure(ri, intentReceived)) {
-                mInjector.showToast(userMessageId, Toast.LENGTH_LONG);
-            }
+            maybeShowDisclosureAsync(intentReceived, newIntent, targetUserId, userMessageId);
+            CompletableFuture.runAsync(() -> startActivityAsCaller(
+                    newIntent, targetUserId), mExecutorService);
         } else {
             Slog.wtf(TAG, "the intent: " + intentReceived + " cannot be forwarded from user "
                     + callingUserId + " to user " + targetUserId);
@@ -149,6 +133,44 @@
         finish();
     }
 
+    private void maybeShowDisclosureAsync(
+            Intent intentReceived, Intent newIntent, int userId, int messageId) {
+        final CompletableFuture<ResolveInfo> resolveInfoFuture =
+                mInjector.resolveActivityAsUser(newIntent, MATCH_DEFAULT_ONLY, userId);
+        resolveInfoFuture.thenAcceptAsync(ri -> {
+            if (shouldShowDisclosure(ri, intentReceived)) {
+                mInjector.showToast(messageId, Toast.LENGTH_LONG);
+            }
+        }, getApplicationContext().getMainExecutor());
+    }
+
+    private void startActivityAsCaller(Intent newIntent, int userId) {
+        try {
+            startActivityAsCaller(
+                    newIntent,
+                    /* options= */ null,
+                    /* permissionToken= */ null,
+                    /* ignoreTargetSecurity= */ false,
+                    userId);
+        } catch (RuntimeException e) {
+            int launchedFromUid = -1;
+            String launchedFromPackage = "?";
+            try {
+                launchedFromUid = ActivityTaskManager.getService().getLaunchedFromUid(
+                        getActivityToken());
+                launchedFromPackage = ActivityTaskManager.getService()
+                        .getLaunchedFromPackage(getActivityToken());
+            } catch (RemoteException ignored) {
+            }
+
+            Slog.wtf(TAG, "Unable to launch as UID " + launchedFromUid + " package "
+                    + launchedFromPackage + ", while running in "
+                    + ActivityThread.currentProcessName(), e);
+        } finally {
+            mExecutorService.shutdown();
+        }
+    }
+
     private void launchChooserActivityWithCorrectTab(Intent intentReceived, String className) {
         // When showing the sharesheet, instead of forwarding to the other profile,
         // we launch the sharesheet in the current user and select the other tab.
@@ -322,8 +344,11 @@
         }
 
         @Override
-        public ResolveInfo resolveActivityAsUser(Intent intent, int flags, int userId) {
-            return getPackageManager().resolveActivityAsUser(intent, flags, userId);
+        @Nullable
+        public CompletableFuture<ResolveInfo> resolveActivityAsUser(
+                Intent intent, int flags, int userId) {
+            return CompletableFuture.supplyAsync(
+                    () -> getPackageManager().resolveActivityAsUser(intent, flags, userId));
         }
 
         @Override
@@ -339,7 +364,7 @@
 
         PackageManager getPackageManager();
 
-        ResolveInfo resolveActivityAsUser(Intent intent, int flags, int userId);
+        CompletableFuture<ResolveInfo> resolveActivityAsUser(Intent intent, int flags, int userId);
 
         void showToast(@StringRes int messageId, int duration);
     }
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index 1bc982c..00faa3b 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -146,6 +146,7 @@
 
     private static final String TAG = "ResolverActivity";
     private static final boolean DEBUG = false;
+    private static final String LAST_SHOWN_TAB_KEY = "last_shown_tab_key";
 
     private boolean mRegistered;
 
@@ -844,9 +845,19 @@
     }
 
     @Override
+    protected void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        ViewPager viewPager = findViewById(R.id.profile_pager);
+        outState.putInt(LAST_SHOWN_TAB_KEY, viewPager.getCurrentItem());
+    }
+
+    @Override
     protected void onRestoreInstanceState(Bundle savedInstanceState) {
         super.onRestoreInstanceState(savedInstanceState);
         resetButtonBar();
+        ViewPager viewPager = findViewById(R.id.profile_pager);
+        viewPager.setCurrentItem(savedInstanceState.getInt(LAST_SHOWN_TAB_KEY));
+        mMultiProfilePagerAdapter.clearInactiveProfileCache();
     }
 
     private boolean isHttpSchemeAndViewAction(Intent intent) {
@@ -1585,6 +1596,7 @@
         TabHost tabHost = findViewById(R.id.profile_tabhost);
         tabHost.setup();
         ViewPager viewPager = findViewById(R.id.profile_pager);
+        viewPager.setSaveEnabled(false);
         TabHost.TabSpec tabSpec = tabHost.newTabSpec(TAB_TAG_PERSONAL)
                 .setContent(R.id.profile_pager)
                 .setIndicator(getString(R.string.resolver_personal_tab));
@@ -1610,6 +1622,7 @@
             }
             setupViewVisibilities();
             maybeLogProfileChange();
+            onProfileTabSelected();
             DevicePolicyEventLogger
                     .createEvent(DevicePolicyEnums.RESOLVER_SWITCH_TABS)
                     .setInt(viewPager.getCurrentItem())
@@ -1628,6 +1641,12 @@
         findViewById(R.id.resolver_tab_divider).setVisibility(View.VISIBLE);
     }
 
+    /**
+     * Callback called when user changes the profile tab.
+     * <p>This method is intended to be overridden by subclasses.
+     */
+    protected void onProfileTabSelected() { }
+
     private void resetCheckedItem() {
         if (!isIntentPicker()) {
             return;
@@ -1745,22 +1764,33 @@
             return;
         }
         final ViewGroup buttonLayout = findViewById(R.id.button_bar);
-        if (buttonLayout != null) {
-            buttonLayout.setVisibility(View.VISIBLE);
-
-            if (!useLayoutWithDefault()) {
-                int inset = mSystemWindowInsets != null ? mSystemWindowInsets.bottom : 0;
-                buttonLayout.setPadding(buttonLayout.getPaddingLeft(), buttonLayout.getPaddingTop(),
-                        buttonLayout.getPaddingRight(), getResources().getDimensionPixelSize(
-                                R.dimen.resolver_button_bar_spacing) + inset);
-            }
-            mOnceButton = (Button) buttonLayout.findViewById(R.id.button_once);
-            mAlwaysButton = (Button) buttonLayout.findViewById(R.id.button_always);
-
-            resetAlwaysOrOnceButtonBar();
-        } else {
+        if (buttonLayout == null) {
             Log.e(TAG, "Layout unexpectedly does not have a button bar");
+            return;
         }
+        ResolverListAdapter activeListAdapter =
+                mMultiProfilePagerAdapter.getActiveListAdapter();
+        View buttonBarDivider = findViewById(R.id.resolver_button_bar_divider);
+        if (activeListAdapter.isTabLoaded()
+                && mMultiProfilePagerAdapter.shouldShowEmptyStateScreen(activeListAdapter)) {
+            buttonLayout.setVisibility(View.INVISIBLE);
+            buttonBarDivider.setVisibility(View.INVISIBLE);
+            return;
+        }
+
+        buttonBarDivider.setVisibility(View.VISIBLE);
+        buttonLayout.setVisibility(View.VISIBLE);
+
+        if (!useLayoutWithDefault()) {
+            int inset = mSystemWindowInsets != null ? mSystemWindowInsets.bottom : 0;
+            buttonLayout.setPadding(buttonLayout.getPaddingLeft(), buttonLayout.getPaddingTop(),
+                    buttonLayout.getPaddingRight(), getResources().getDimensionPixelSize(
+                            R.dimen.resolver_button_bar_spacing) + inset);
+        }
+        mOnceButton = (Button) buttonLayout.findViewById(R.id.button_once);
+        mAlwaysButton = (Button) buttonLayout.findViewById(R.id.button_always);
+
+        resetAlwaysOrOnceButtonBar();
     }
 
     private void resetAlwaysOrOnceButtonBar() {
diff --git a/core/java/com/android/internal/app/ResolverMultiProfilePagerAdapter.java b/core/java/com/android/internal/app/ResolverMultiProfilePagerAdapter.java
index ad31d8b..885d1bb 100644
--- a/core/java/com/android/internal/app/ResolverMultiProfilePagerAdapter.java
+++ b/core/java/com/android/internal/app/ResolverMultiProfilePagerAdapter.java
@@ -213,6 +213,12 @@
                 /* subtitleRes */ 0);
     }
 
+    @Override
+    protected void setupContainerPadding(View container) {
+        container.setPadding(container.getPaddingLeft(), container.getPaddingTop(),
+                container.getPaddingRight(), /* bottom */ 0);
+    }
+
     class ResolverProfileDescriptor extends ProfileDescriptor {
         private ResolverListAdapter resolverListAdapter;
         final ListView listView;
diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java
index 43bd4a6..6432475 100644
--- a/core/java/com/android/internal/os/BatteryStatsImpl.java
+++ b/core/java/com/android/internal/os/BatteryStatsImpl.java
@@ -6093,7 +6093,8 @@
         return array;
     }
 
-    public void noteNetworkInterfaceTypeLocked(String iface, int networkType) {
+    /** @hide */
+    public void noteNetworkInterfaceType(String iface, int networkType) {
         if (TextUtils.isEmpty(iface)) return;
 
         synchronized (mModemNetworkLock) {
diff --git a/core/java/com/android/internal/policy/GestureNavigationSettingsObserver.java b/core/java/com/android/internal/policy/GestureNavigationSettingsObserver.java
index ebfea450..56a6db9 100644
--- a/core/java/com/android/internal/policy/GestureNavigationSettingsObserver.java
+++ b/core/java/com/android/internal/policy/GestureNavigationSettingsObserver.java
@@ -46,6 +46,9 @@
         r.registerContentObserver(
                 Settings.Secure.getUriFor(Settings.Secure.BACK_GESTURE_INSET_SCALE_RIGHT),
                 false, this, UserHandle.USER_ALL);
+        r.registerContentObserver(
+                Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE),
+                false, this, UserHandle.USER_ALL);
     }
 
     public void unregister() {
@@ -68,6 +71,11 @@
         return getSensitivity(userRes, Settings.Secure.BACK_GESTURE_INSET_SCALE_RIGHT);
     }
 
+    public boolean areNavigationButtonForcedVisible() {
+        return Settings.Secure.getIntForUser(mContext.getContentResolver(),
+                Settings.Secure.USER_SETUP_COMPLETE, 0, UserHandle.USER_CURRENT) == 0;
+    }
+
     private int getSensitivity(Resources userRes, String side) {
         final int inset = userRes.getDimensionPixelSize(
                 com.android.internal.R.dimen.config_backGestureInset);
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index 25c114f..23ba653 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -79,6 +79,7 @@
 import android.view.ContextThemeWrapper;
 import android.view.Gravity;
 import android.view.IRotationWatcher.Stub;
+import android.view.IScrollCaptureController;
 import android.view.IWindowManager;
 import android.view.InputDevice;
 import android.view.InputEvent;
@@ -89,6 +90,7 @@
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.MotionEvent;
+import android.view.ScrollCaptureCallback;
 import android.view.SearchEvent;
 import android.view.SurfaceHolder.Callback2;
 import android.view.View;
@@ -3916,4 +3918,35 @@
                     : null);
         }
     }
+
+    /**
+     * System request to begin scroll capture.
+     *
+     * @param controller the controller to receive responses
+     * @hide
+     */
+    @Override
+    public void requestScrollCapture(IScrollCaptureController controller) {
+        getViewRootImpl().dispatchScrollCaptureRequest(controller);
+    }
+
+    /**
+     * Registers a handler providing scrolling capture support for window content.
+     *
+     * @param callback the callback to add
+     */
+    @Override
+    public void addScrollCaptureCallback(@NonNull ScrollCaptureCallback callback) {
+        getViewRootImpl().addScrollCaptureCallback(callback);
+    }
+
+    /**
+     * Unregisters the given {@link ScrollCaptureCallback}.
+     *
+     * @param callback the callback to remove
+     */
+    @Override
+    public void removeScrollCaptureCallback(@NonNull ScrollCaptureCallback callback) {
+        getViewRootImpl().removeScrollCaptureCallback(callback);
+    }
 }
diff --git a/core/java/com/android/internal/util/ScreenshotHelper.java b/core/java/com/android/internal/util/ScreenshotHelper.java
index 7cff90b..adadc5e 100644
--- a/core/java/com/android/internal/util/ScreenshotHelper.java
+++ b/core/java/com/android/internal/util/ScreenshotHelper.java
@@ -1,5 +1,7 @@
 package com.android.internal.util;
 
+import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OTHER;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.ComponentName;
@@ -10,11 +12,12 @@
 import android.graphics.Insets;
 import android.graphics.Rect;
 import android.net.Uri;
-import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Message;
 import android.os.Messenger;
+import android.os.Parcel;
+import android.os.Parcelable;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.util.Log;
@@ -23,6 +26,109 @@
 import java.util.function.Consumer;
 
 public class ScreenshotHelper {
+
+    /**
+     * Describes a screenshot request (to make it easier to pass data through to the handler).
+     */
+    public static class ScreenshotRequest implements Parcelable {
+        private int mSource;
+        private boolean mHasStatusBar;
+        private boolean mHasNavBar;
+        private Bitmap mBitmap;
+        private Rect mBoundsInScreen;
+        private Insets mInsets;
+        private int mTaskId;
+
+        ScreenshotRequest(int source, boolean hasStatus, boolean hasNav) {
+            mSource = source;
+            mHasStatusBar = hasStatus;
+            mHasNavBar = hasNav;
+        }
+
+        ScreenshotRequest(
+                int source, Bitmap bitmap, Rect boundsInScreen, Insets insets, int taskId) {
+            mSource = source;
+            mBitmap = bitmap;
+            mBoundsInScreen = boundsInScreen;
+            mInsets = insets;
+            mTaskId = taskId;
+        }
+
+        ScreenshotRequest(Parcel in) {
+            mSource = in.readInt();
+            mHasStatusBar = in.readBoolean();
+            mHasNavBar = in.readBoolean();
+            if (in.readInt() == 1) {
+                mBitmap = in.readParcelable(Bitmap.class.getClassLoader());
+                mBoundsInScreen = in.readParcelable(Rect.class.getClassLoader());
+                mInsets = in.readParcelable(Insets.class.getClassLoader());
+                mTaskId = in.readInt();
+            }
+        }
+
+        public int getSource() {
+            return mSource;
+        }
+
+        public boolean getHasStatusBar() {
+            return mHasStatusBar;
+        }
+
+        public boolean getHasNavBar() {
+            return mHasNavBar;
+        }
+
+        public Bitmap getBitmap() {
+            return mBitmap;
+        }
+
+        public Rect getBoundsInScreen() {
+            return mBoundsInScreen;
+        }
+
+        public Insets getInsets() {
+            return mInsets;
+        }
+
+        public int getTaskId() {
+            return mTaskId;
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(mSource);
+            dest.writeBoolean(mHasStatusBar);
+            dest.writeBoolean(mHasNavBar);
+            if (mBitmap == null) {
+                dest.writeInt(0);
+            } else {
+                dest.writeInt(1);
+                dest.writeParcelable(mBitmap, 0);
+                dest.writeParcelable(mBoundsInScreen, 0);
+                dest.writeParcelable(mInsets, 0);
+                dest.writeInt(mTaskId);
+            }
+        }
+
+        public static final @NonNull Parcelable.Creator<ScreenshotRequest> CREATOR =
+                new Parcelable.Creator<ScreenshotRequest>() {
+
+                    @Override
+                    public ScreenshotRequest createFromParcel(Parcel source) {
+                        return new ScreenshotRequest(source);
+                    }
+
+                    @Override
+                    public ScreenshotRequest[] newArray(int size) {
+                        return new ScreenshotRequest[size];
+                    }
+                };
+    }
     private static final String TAG = "ScreenshotHelper";
 
     // Time until we give up on the screenshot & show an error instead.
@@ -36,8 +142,10 @@
         mContext = context;
     }
 
+
+
     /**
-     * Request a screenshot be taken with a specific timeout.
+     * Request a screenshot be taken.
      *
      * Added to support reducing unit test duration; the method variant without a timeout argument
      * is recommended for general use.
@@ -47,6 +155,32 @@
      *                           or
      *                           {@link android.view.WindowManager#TAKE_SCREENSHOT_SELECTED_REGION}
      * @param hasStatus          {@code true} if the status bar is currently showing. {@code false}
+     *                           if not.
+     * @param hasNav             {@code true} if the navigation bar is currently showing. {@code
+     *                           false} if not.
+     * @param source             The source of the screenshot request. One of
+     *                           {SCREENSHOT_GLOBAL_ACTIONS, SCREENSHOT_KEY_CHORD,
+     *                           SCREENSHOT_OVERVIEW, SCREENSHOT_OTHER}
+     * @param handler            A handler used in case the screenshot times out
+     * @param completionConsumer Consumes `false` if a screenshot was not taken, and `true` if the
+     *                           screenshot was taken.
+     */
+    public void takeScreenshot(final int screenshotType, final boolean hasStatus,
+            final boolean hasNav, int source, @NonNull Handler handler,
+            @Nullable Consumer<Uri> completionConsumer) {
+        ScreenshotRequest screenshotRequest = new ScreenshotRequest(source, hasStatus, hasNav);
+        takeScreenshot(screenshotType, SCREENSHOT_TIMEOUT_MS, handler, screenshotRequest,
+                completionConsumer);
+    }
+
+    /**
+     * Request a screenshot be taken, with provided reason.
+     *
+     * @param screenshotType     The type of screenshot, for example either
+     *                           {@link android.view.WindowManager#TAKE_SCREENSHOT_FULLSCREEN}
+     *                           or
+     *                           {@link android.view.WindowManager#TAKE_SCREENSHOT_SELECTED_REGION}
+     * @param hasStatus          {@code true} if the status bar is currently showing. {@code false}
      *                           if
      *                           not.
      * @param hasNav             {@code true} if the navigation bar is currently showing. {@code
@@ -64,7 +198,7 @@
     }
 
     /**
-     * Request a screenshot be taken.
+     * Request a screenshot be taken with a specific timeout.
      *
      * Added to support reducing unit test duration; the method variant without a timeout argument
      * is recommended for general use.
@@ -89,9 +223,9 @@
     public void takeScreenshot(final int screenshotType, final boolean hasStatus,
             final boolean hasNav, long timeoutMs, @NonNull Handler handler,
             @Nullable Consumer<Uri> completionConsumer) {
-        takeScreenshot(screenshotType, hasStatus, hasNav, timeoutMs, handler, null,
-                completionConsumer
-        );
+        ScreenshotRequest screenshotRequest = new ScreenshotRequest(SCREENSHOT_OTHER, hasStatus,
+                hasNav);
+        takeScreenshot(screenshotType, timeoutMs, handler, screenshotRequest, completionConsumer);
     }
 
     /**
@@ -106,23 +240,16 @@
      *                           screenshot was taken.
      */
     public void provideScreenshot(@NonNull Bitmap screenshot, @NonNull Rect boundsInScreen,
-            @NonNull Insets insets, int taskId, @NonNull Handler handler,
-            @Nullable Consumer<Uri> completionConsumer) {
-        Bundle imageBundle = new Bundle();
-        imageBundle.putParcelable(WindowManager.PARCEL_KEY_SCREENSHOT_BITMAP, screenshot);
-        imageBundle.putParcelable(WindowManager.PARCEL_KEY_SCREENSHOT_BOUNDS, boundsInScreen);
-        imageBundle.putParcelable(WindowManager.PARCEL_KEY_SCREENSHOT_INSETS, insets);
-        imageBundle.putInt(WindowManager.PARCEL_KEY_SCREENSHOT_TASK_ID, taskId);
-
-        takeScreenshot(
-                WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE,
-                false, false, // ignored when image bundle is set
-                SCREENSHOT_TIMEOUT_MS, handler, imageBundle, completionConsumer);
+            @NonNull Insets insets, int taskId, int source,
+            @NonNull Handler handler, @Nullable Consumer<Uri> completionConsumer) {
+        ScreenshotRequest screenshotRequest =
+                new ScreenshotRequest(source, screenshot, boundsInScreen, insets, taskId);
+        takeScreenshot(WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_TIMEOUT_MS,
+                handler, screenshotRequest, completionConsumer);
     }
 
-    private void takeScreenshot(final int screenshotType, final boolean hasStatus,
-            final boolean hasNav, long timeoutMs, @NonNull Handler handler,
-            @Nullable Bundle providedImage, @Nullable Consumer<Uri> completionConsumer) {
+    private void takeScreenshot(final int screenshotType, long timeoutMs, @NonNull Handler handler,
+            ScreenshotRequest screenshotRequest, @Nullable Consumer<Uri> completionConsumer) {
         synchronized (mScreenshotLock) {
             if (mScreenshotConnection != null) {
                 return;
@@ -157,7 +284,7 @@
                             return;
                         }
                         Messenger messenger = new Messenger(service);
-                        Message msg = Message.obtain(null, screenshotType);
+                        Message msg = Message.obtain(null, screenshotType, screenshotRequest);
                         final ServiceConnection myConn = this;
                         Handler h = new Handler(handler.getLooper()) {
                             @Override
@@ -175,12 +302,6 @@
                             }
                         };
                         msg.replyTo = new Messenger(h);
-                        msg.arg1 = hasStatus ? 1 : 0;
-                        msg.arg2 = hasNav ? 1 : 0;
-
-                        if (screenshotType == WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE) {
-                            msg.setData(providedImage);
-                        }
 
                         try {
                             messenger.send(msg);
diff --git a/core/java/com/android/internal/view/BaseIWindow.java b/core/java/com/android/internal/view/BaseIWindow.java
index 47f094f..7f3eb45 100644
--- a/core/java/com/android/internal/view/BaseIWindow.java
+++ b/core/java/com/android/internal/view/BaseIWindow.java
@@ -26,6 +26,7 @@
 import android.util.MergedConfiguration;
 import android.view.DisplayCutout;
 import android.view.DragEvent;
+import android.view.IScrollCaptureController;
 import android.view.IWindow;
 import android.view.IWindowSession;
 import android.view.InsetsSourceControl;
@@ -169,4 +170,13 @@
     @Override
     public void dispatchPointerCaptureChanged(boolean hasCapture) {
     }
+
+    @Override
+    public void requestScrollCapture(IScrollCaptureController controller) {
+        try {
+            controller.onClientUnavailable();
+        } catch (RemoteException ex) {
+            // ignore
+        }
+    }
 }
diff --git a/core/java/com/android/internal/view/ScrollCaptureInternal.java b/core/java/com/android/internal/view/ScrollCaptureInternal.java
new file mode 100644
index 0000000..c589afde
--- /dev/null
+++ b/core/java/com/android/internal/view/ScrollCaptureInternal.java
@@ -0,0 +1,117 @@
+/*
+ * 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.internal.view;
+
+import android.annotation.Nullable;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.ScrollCaptureCallback;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Provides built-in framework level Scroll Capture support for standard scrolling Views.
+ */
+public class ScrollCaptureInternal {
+    private static final String TAG = "ScrollCaptureInternal";
+
+    private static final int UP = -1;
+    private static final int DOWN = 1;
+
+    /**
+     * Not a ViewGroup, or cannot scroll according to View APIs.
+     */
+    public static final int TYPE_FIXED = 0;
+
+    /**
+     * Slides a single child view using mScrollX/mScrollY.
+     */
+    public static final int TYPE_SCROLLING = 1;
+
+    /**
+     * Slides child views through the viewport by translating their layout positions with {@link
+     * View#offsetTopAndBottom(int)}. Manages Child view lifecycle, creating as needed and
+     * binding views to data from an adapter. Views are reused whenever possible.
+     */
+    public static final int TYPE_RECYCLING = 2;
+
+    /**
+     * Performs tests on the given View and determines:
+     * 1. If scrolling is possible
+     * 2. What mechanisms are used for scrolling.
+     * <p>
+     * This needs to be fast and not alloc memory. It's called on everything in the tree not marked
+     * as excluded during scroll capture search.
+     */
+    public static int detectScrollingType(View view) {
+        // Must be a ViewGroup
+        if (!(view instanceof ViewGroup)) {
+            return TYPE_FIXED;
+        }
+        // Confirm that it can scroll.
+        if (!(view.canScrollVertically(DOWN) || view.canScrollVertically(UP))) {
+            // Nothing to scroll here, move along.
+            return TYPE_FIXED;
+        }
+        // ScrollViews accept only a single child.
+        if (((ViewGroup) view).getChildCount() > 1) {
+            return TYPE_RECYCLING;
+        }
+        //Because recycling containers don't use scrollY, a non-zero value means Scroll view.
+        if (view.getScrollY() != 0) {
+            return TYPE_SCROLLING;
+        }
+        // Since scrollY cannot be negative, this means a Recycling view.
+        if (view.canScrollVertically(UP)) {
+            return TYPE_RECYCLING;
+        }
+        // canScrollVertically(UP) == false, getScrollY() == 0, getChildCount() == 1.
+
+        // For Recycling containers, this should be a no-op (RecyclerView logs a warning)
+        view.scrollTo(view.getScrollX(), 1);
+
+        // A scrolling container would have moved by 1px.
+        if (view.getScrollY() == 1) {
+            view.scrollTo(view.getScrollX(), 0);
+            return TYPE_SCROLLING;
+        }
+        return TYPE_RECYCLING;
+    }
+
+    /**
+     * Creates a scroll capture callback for the given view if possible.
+     *
+     * @param view             the view to capture
+     * @param localVisibleRect the visible area of the given view in local coordinates, as supplied
+     *                         by the view parent
+     * @param positionInWindow the offset of localVisibleRect within the window
+     *
+     * @return a new callback or null if the View isn't supported
+     */
+    @Nullable
+    public ScrollCaptureCallback requestCallback(View view, Rect localVisibleRect,
+            Point positionInWindow) {
+        // Nothing to see here yet.
+        int i = detectScrollingType(view);
+        switch (i) {
+            case TYPE_SCROLLING:
+                return new ScrollCaptureViewSupport<>((ViewGroup) view,
+                        new ScrollViewCaptureHelper());
+        }
+        return null;
+    }
+}
diff --git a/core/java/com/android/internal/view/ScrollCaptureViewHelper.java b/core/java/com/android/internal/view/ScrollCaptureViewHelper.java
new file mode 100644
index 0000000..9f100bd
--- /dev/null
+++ b/core/java/com/android/internal/view/ScrollCaptureViewHelper.java
@@ -0,0 +1,87 @@
+/*
+ * 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.internal.view;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Rect;
+import android.view.View;
+
+interface ScrollCaptureViewHelper<V extends View> {
+    int UP = -1;
+    int DOWN = 1;
+
+    /**
+     * Verifies that the view is still visible and scrollable. If true is returned here, expect a
+     * call to {@link #onComputeScrollBounds(View)} to follow.
+     *
+     * @param view the view being captured
+     * @return true if the callback should respond to a request with scroll bounds
+     */
+    default boolean onAcceptSession(@Nullable V view) {
+        return view != null && view.isVisibleToUser()
+                && (view.canScrollVertically(UP) || view.canScrollVertically(DOWN));
+    }
+
+    /**
+     * Given a scroll capture request for a view, adjust the provided rect to cover the scrollable
+     * content area. The default implementation returns the padded content area of {@code view}.
+     *
+     * @param view the view being captured
+     */
+    default Rect onComputeScrollBounds(@Nullable V view) {
+        return new Rect(view.getPaddingLeft(), view.getPaddingTop(),
+                view.getWidth() - view.getPaddingRight(),
+                view.getHeight() - view.getPaddingBottom());
+    }
+    /**
+     * Adjust the target for capture.
+     * <p>
+     * Do not touch anything that may change layout positions or sizes on screen. Anything else may
+     * be adjusted as long as it can be reversed in {@link #onPrepareForEnd(View)}.
+     *
+     * @param view         the view being captured
+     * @param scrollBounds the bounds within {@code view} where content scrolls
+     */
+    void onPrepareForStart(@NonNull V view, Rect scrollBounds);
+
+    /**
+     * Map the request onto the screen.
+     * <p>
+     * Given a  rect describing the area to capture, relative to scrollBounds, take actions
+     * necessary to bring the content within the rectangle into the visible area of the view if
+     * needed and return the resulting rectangle describing the position and bounds of the area
+     * which is visible.
+     *
+     * @param scrollBounds the area in which scrolling content moves, local to the {@code containing
+     *                     view}
+     * @param requestRect  the area relative to {@code scrollBounds} which describes the location of
+     *                     content to capture for the request
+     * @return the visible area within scrollBounds of the requested rectangle, return {@code null}
+     * in the case of an unrecoverable error condition, to abort the capture process
+     */
+    Rect onScrollRequested(@NonNull V view, Rect scrollBounds, Rect requestRect);
+
+    /**
+     * Restore the target after capture.
+     * <p>
+     * Put back anything that was changed in {@link #onPrepareForStart(View, Rect)}.
+     *
+     * @param view the view being captured
+     */
+    void onPrepareForEnd(@NonNull V view);
+}
diff --git a/core/java/com/android/internal/view/ScrollCaptureViewSupport.java b/core/java/com/android/internal/view/ScrollCaptureViewSupport.java
new file mode 100644
index 0000000..4087eda
--- /dev/null
+++ b/core/java/com/android/internal/view/ScrollCaptureViewSupport.java
@@ -0,0 +1,239 @@
+/*
+ * 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.internal.view;
+
+import android.graphics.HardwareRenderer;
+import android.graphics.Matrix;
+import android.graphics.RecordingCanvas;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.RenderNode;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.util.DisplayMetrics;
+import android.view.ScrollCaptureCallback;
+import android.view.ScrollCaptureSession;
+import android.view.Surface;
+import android.view.View;
+
+import java.lang.ref.WeakReference;
+import java.util.function.Consumer;
+
+/**
+ * Provides a ScrollCaptureCallback implementation for to handle arbitrary View-based scrolling
+ * containers.
+ * <p>
+ * To use this class, supply the target view and an implementation of {@ScrollCaptureViewHelper}
+ * to the callback.
+ *
+ * @param <V> the specific View subclass handled
+ * @hide
+ */
+public class ScrollCaptureViewSupport<V extends View> implements ScrollCaptureCallback {
+
+    private final WeakReference<V> mWeakView;
+    private final ScrollCaptureViewHelper<V> mViewHelper;
+    private ViewRenderer mRenderer;
+    private Handler mUiHandler;
+    private boolean mStarted;
+    private boolean mEnded;
+
+    static <V extends View> ScrollCaptureCallback createCallback(V view,
+            ScrollCaptureViewHelper<V> impl) {
+        return new ScrollCaptureViewSupport<>(view, impl);
+    }
+
+    ScrollCaptureViewSupport(V containingView, ScrollCaptureViewHelper<V> viewHelper) {
+        mWeakView = new WeakReference<>(containingView);
+        mRenderer = new ViewRenderer();
+        mUiHandler = containingView.getHandler();
+        mViewHelper = viewHelper;
+    }
+
+    // Base implementation of ScrollCaptureCallback
+
+    @Override
+    public final void onScrollCaptureSearch(Consumer<Rect> onReady) {
+        V view = mWeakView.get();
+        mStarted = false;
+        mEnded = false;
+
+        if (view != null && view.isVisibleToUser() && mViewHelper.onAcceptSession(view)) {
+            onReady.accept(mViewHelper.onComputeScrollBounds(view));
+            return;
+        }
+        onReady.accept(null);
+    }
+
+    @Override
+    public final void onScrollCaptureStart(ScrollCaptureSession session, Runnable onReady) {
+        V view = mWeakView.get();
+        mEnded = false;
+        mStarted = true;
+
+        // Note: If somehow the view is already gone or detached, the first call to
+        // {@code onScrollCaptureImageRequest} will return an error and request the session to
+        // end.
+        if (view != null && view.isVisibleToUser()) {
+            mRenderer.setSurface(session.getSurface());
+            mViewHelper.onPrepareForStart(view, session.getScrollBounds());
+        }
+        onReady.run();
+    }
+
+    @Override
+    public final void onScrollCaptureImageRequest(ScrollCaptureSession session, Rect requestRect) {
+        V view = mWeakView.get();
+        if (view == null || !view.isVisibleToUser()) {
+            // Signal to the controller that we have a problem and can't continue.
+            session.notifyBufferSent(0, null);
+            return;
+        }
+        Rect captureArea = mViewHelper.onScrollRequested(view, session.getScrollBounds(),
+                requestRect);
+        mRenderer.renderFrame(view, captureArea, mUiHandler,
+                () -> session.notifyBufferSent(0, captureArea));
+    }
+
+    @Override
+    public final void onScrollCaptureEnd(Runnable onReady) {
+        V view = mWeakView.get();
+        if (mStarted && !mEnded) {
+            mViewHelper.onPrepareForEnd(view);
+            /* empty */
+            mEnded = true;
+            mRenderer.trimMemory();
+            mRenderer.setSurface(null);
+        }
+        onReady.run();
+    }
+
+    /**
+     * Internal helper class which assists in rendering sections of the view hierarchy relative to a
+     * given view. Used by framework implementations of ScrollCaptureHandler to render and dispatch
+     * image requests.
+     */
+    static final class ViewRenderer {
+        // alpha, "reasonable default" from Javadoc
+        private static final float AMBIENT_SHADOW_ALPHA = 0.039f;
+        private static final float SPOT_SHADOW_ALPHA = 0.039f;
+
+        // Default values:
+        //    lightX = (screen.width() / 2) - windowLeft
+        //    lightY = 0 - windowTop
+        //    lightZ = 600dp
+        //    lightRadius = 800dp
+        private static final float LIGHT_Z_DP = 400;
+        private static final float LIGHT_RADIUS_DP = 800;
+        private static final String TAG = "ViewRenderer";
+
+        private HardwareRenderer mRenderer;
+        private RenderNode mRootRenderNode;
+        private final RectF mTempRectF = new RectF();
+        private final Rect mSourceRect = new Rect();
+        private final Rect mTempRect = new Rect();
+        private final Matrix mTempMatrix = new Matrix();
+        private final int[] mTempLocation = new int[2];
+        private long mLastRenderedSourceDrawingId = -1;
+
+
+        ViewRenderer() {
+            mRenderer = new HardwareRenderer();
+            mRootRenderNode = new RenderNode("ScrollCaptureRoot");
+            mRenderer.setContentRoot(mRootRenderNode);
+
+            // TODO: Figure out a way to flip this on when we are sure the source window is opaque
+            mRenderer.setOpaque(false);
+        }
+
+        public void setSurface(Surface surface) {
+            mRenderer.setSurface(surface);
+        }
+
+        /**
+         * Cache invalidation check. If the source view is the same as the previous call (which is
+         * mostly always the case, then we can skip setting up lighting on each call (for now)
+         *
+         * @return true if the view changed, false if the view was previously rendered by this class
+         */
+        private boolean updateForView(View source) {
+            if (mLastRenderedSourceDrawingId == source.getUniqueDrawingId()) {
+                return false;
+            }
+            mLastRenderedSourceDrawingId = source.getUniqueDrawingId();
+            return true;
+        }
+
+        // TODO: may need to adjust lightY based on the virtual canvas position to get
+        //       consistent shadow positions across the whole capture. Or possibly just
+        //       pull lightZ way back to make shadows more uniform.
+        private void setupLighting(View mSource) {
+            mLastRenderedSourceDrawingId = mSource.getUniqueDrawingId();
+            DisplayMetrics metrics = mSource.getResources().getDisplayMetrics();
+            mSource.getLocationOnScreen(mTempLocation);
+            final float lightX = metrics.widthPixels / 2f - mTempLocation[0];
+            final float lightY = metrics.heightPixels - mTempLocation[1];
+            final int lightZ = (int) (LIGHT_Z_DP * metrics.density);
+            final int lightRadius = (int) (LIGHT_RADIUS_DP * metrics.density);
+
+            // Enable shadows for elevation/Z
+            mRenderer.setLightSourceGeometry(lightX, lightY, lightZ, lightRadius);
+            mRenderer.setLightSourceAlpha(AMBIENT_SHADOW_ALPHA, SPOT_SHADOW_ALPHA);
+
+        }
+
+        public void renderFrame(View localReference, Rect sourceRect, Handler handler,
+                Runnable onFrameCommitted) {
+            if (updateForView(localReference)) {
+                setupLighting(localReference);
+            }
+            buildRootDisplayList(localReference, sourceRect);
+            HardwareRenderer.FrameRenderRequest request = mRenderer.createRenderRequest();
+            request.setVsyncTime(SystemClock.elapsedRealtimeNanos());
+            request.setFrameCommitCallback(handler::post, onFrameCommitted);
+            request.setWaitForPresent(true);
+            request.syncAndDraw();
+        }
+
+        public void trimMemory() {
+            mRenderer.clearContent();
+        }
+
+        public void destroy() {
+            mRenderer.destroy();
+        }
+
+        private void transformToRoot(View local, Rect localRect, Rect outRect) {
+            mTempMatrix.reset();
+            local.transformMatrixToGlobal(mTempMatrix);
+            mTempRectF.set(localRect);
+            mTempMatrix.mapRect(mTempRectF);
+            mTempRectF.round(outRect);
+        }
+
+        private void buildRootDisplayList(View source, Rect localSourceRect) {
+            final View captureSource = source.getRootView();
+            transformToRoot(source, localSourceRect, mTempRect);
+            mRootRenderNode.setPosition(0, 0, mTempRect.width(), mTempRect.height());
+            RecordingCanvas canvas = mRootRenderNode.beginRecording(mTempRect.width(),
+                    mTempRect.height());
+            canvas.translate(-mTempRect.left, -mTempRect.top);
+            canvas.drawRenderNode(captureSource.updateDisplayListIfDirty());
+            mRootRenderNode.endRecording();
+        }
+    }
+}
diff --git a/core/java/com/android/internal/view/ScrollViewCaptureHelper.java b/core/java/com/android/internal/view/ScrollViewCaptureHelper.java
new file mode 100644
index 0000000..12bd461
--- /dev/null
+++ b/core/java/com/android/internal/view/ScrollViewCaptureHelper.java
@@ -0,0 +1,167 @@
+/*
+ * 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.internal.view;
+
+import android.annotation.NonNull;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+
+/**
+ * ScrollCapture for ScrollView and <i>ScrollView-like</i> ViewGroups.
+ * <p>
+ * Requirements for proper operation:
+ * <ul>
+ * <li>contains at most 1 child.
+ * <li>scrolls to absolute positions with {@link View#scrollTo(int, int)}.
+ * <li>has a finite, known content height and scrolling range
+ * <li>correctly implements {@link View#canScrollVertically(int)}
+ * <li>correctly implements {@link ViewParent#requestChildRectangleOnScreen(View,
+ * Rect, boolean)}
+ * </ul>
+ */
+public class ScrollViewCaptureHelper implements ScrollCaptureViewHelper<ViewGroup> {
+    private int mStartScrollY;
+    private boolean mScrollBarEnabled;
+    private int mOverScrollMode;
+
+    /** @see ScrollCaptureViewHelper#onPrepareForStart(View, Rect) */
+    public void onPrepareForStart(@NonNull ViewGroup view, Rect scrollBounds) {
+        mStartScrollY = view.getScrollY();
+        mOverScrollMode = view.getOverScrollMode();
+        if (mOverScrollMode != View.OVER_SCROLL_NEVER) {
+            view.setOverScrollMode(View.OVER_SCROLL_NEVER);
+        }
+        mScrollBarEnabled = view.isVerticalScrollBarEnabled();
+        if (mScrollBarEnabled) {
+            view.setVerticalScrollBarEnabled(false);
+        }
+    }
+
+    /** @see ScrollCaptureViewHelper#onScrollRequested(View, Rect, Rect) */
+    public Rect onScrollRequested(@NonNull ViewGroup view, Rect scrollBounds, Rect requestRect) {
+        final View contentView = view.getChildAt(0); // returns null, does not throw IOOBE
+        if (contentView == null) {
+            return null;
+        }
+        /*
+               +---------+ <----+ Content [25,25 - 275,1025] (w=250,h=1000)
+               |         |
+            ...|.........|...  startScrollY=100
+               |         |
+            +--+---------+---+ <--+ Container View [0,0 - 300,300] (scrollY=200)
+            |  .         .   |
+        --- |  . +-----+   <------+ Scroll Bounds [50,50 - 250,250] (200x200)
+         ^  |  . |     | .   |      (Local to Container View, fixed/un-scrolled)
+         |  |  . |     | .   |
+         |  |  . |     | .   |
+         |  |  . +-----+ .   |
+         |  |  .         .   |
+         |  +--+---------+---+
+         |     |         |
+        -+-    | +-----+ |
+               | |#####| |   <--+ Requested Bounds [0,300 - 200,400] (200x100)
+               | +-----+ |       (Local to Scroll Bounds, fixed/un-scrolled)
+               |         |
+               +---------+
+
+        Container View (ScrollView) [0,0 - 300,300] (scrollY = 200)
+        \__ Content [25,25 - 275,1025]  (250x1000) (contentView)
+        \__ Scroll Bounds[50,50 - 250,250]  (w=200,h=200)
+            \__ Requested Bounds[0,300 - 200,400] (200x100)
+       */
+
+        // 0) adjust the requestRect to account for scroll change since start
+        //
+        //  Scroll Bounds[50,50 - 250,250]  (w=200,h=200)
+        //  \__ Requested Bounds[0,200 - 200,300] (200x100)
+
+        // (y-100) (scrollY - mStartScrollY)
+        int scrollDelta = view.getScrollY() - mStartScrollY;
+
+        //  1) Translate request rect to make it relative to container view
+        //
+        //  Container View [0,0 - 300,300] (scrollY=200)
+        //  \__ Requested Bounds[50,250 - 250,350] (w=250, h=100)
+
+        // (x+50,y+50)
+        Rect requestedContainerBounds = new Rect(requestRect);
+        requestedContainerBounds.offset(0, -scrollDelta);
+        requestedContainerBounds.offset(scrollBounds.left, scrollBounds.top);
+
+        //  2) Translate from container to contentView relative (applying container scrollY)
+        //
+        //  Container View [0,0 - 300,300] (scrollY=200)
+        //  \__ Content [25,25 - 275,1025]  (250x1000) (contentView)
+        //      \__ Requested Bounds[25,425 - 200,525] (w=250, h=100)
+
+        // (x-25,y+175) (scrollY - content.top)
+        Rect requestedContentBounds = new Rect(requestedContainerBounds);
+        requestedContentBounds.offset(
+                view.getScrollX() - contentView.getLeft(),
+                view.getScrollY() - contentView.getTop());
+
+
+
+        // requestRect is now local to contentView as requestedContentBounds
+        // contentView (and each parent in turn if possible) will be scrolled
+        // (if necessary) to make all of requestedContent visible, (if possible!)
+        contentView.requestRectangleOnScreen(new Rect(requestedContentBounds), true);
+
+        // update new offset between starting and current scroll position
+        scrollDelta = view.getScrollY() - mStartScrollY;
+
+
+        // TODO: adjust to avoid occlusions/minimize scroll changes
+
+        Point offset = new Point();
+        final Rect capturedRect = new Rect(requestedContentBounds); // empty
+        if (!view.getChildVisibleRect(contentView, capturedRect, offset)) {
+            capturedRect.setEmpty();
+            return capturedRect;
+        }
+        // Transform back from global to content-view local
+        capturedRect.offset(-offset.x, -offset.y);
+
+        // Then back to container view
+        capturedRect.offset(
+                contentView.getLeft() - view.getScrollX(),
+                contentView.getTop() - view.getScrollY());
+
+
+        // And back to relative to scrollBounds
+        capturedRect.offset(-scrollBounds.left, -scrollBounds.top);
+
+        // Apply scrollDelta again to return to make capturedRect relative to scrollBounds at
+        // the scroll position at start of capture.
+        capturedRect.offset(0, scrollDelta);
+        return capturedRect;
+    }
+
+    /** @see ScrollCaptureViewHelper#onPrepareForEnd(View)  */
+    public void onPrepareForEnd(@NonNull ViewGroup view) {
+        view.scrollTo(0, mStartScrollY);
+        if (mOverScrollMode != View.OVER_SCROLL_NEVER) {
+            view.setOverScrollMode(mOverScrollMode);
+        }
+        if (mScrollBarEnabled) {
+            view.setVerticalScrollBarEnabled(true);
+        }
+    }
+}
diff --git a/core/java/com/android/internal/widget/ResolverDrawerLayout.java b/core/java/com/android/internal/widget/ResolverDrawerLayout.java
index 31527e8..fb2ecf3 100644
--- a/core/java/com/android/internal/widget/ResolverDrawerLayout.java
+++ b/core/java/com/android/internal/widget/ResolverDrawerLayout.java
@@ -1084,6 +1084,7 @@
     protected Parcelable onSaveInstanceState() {
         final SavedState ss = new SavedState(super.onSaveInstanceState());
         ss.open = mCollapsibleHeight > 0 && mCollapseOffset == 0;
+        ss.mCollapsibleHeightReserved = mCollapsibleHeightReserved;
         return ss;
     }
 
@@ -1092,6 +1093,7 @@
         final SavedState ss = (SavedState) state;
         super.onRestoreInstanceState(ss.getSuperState());
         mOpenOnLayout = ss.open;
+        mCollapsibleHeightReserved = ss.mCollapsibleHeightReserved;
     }
 
     public static class LayoutParams extends MarginLayoutParams {
@@ -1142,6 +1144,7 @@
 
     static class SavedState extends BaseSavedState {
         boolean open;
+        private int mCollapsibleHeightReserved;
 
         SavedState(Parcelable superState) {
             super(superState);
@@ -1150,12 +1153,14 @@
         private SavedState(Parcel in) {
             super(in);
             open = in.readInt() != 0;
+            mCollapsibleHeightReserved = in.readInt();
         }
 
         @Override
         public void writeToParcel(Parcel out, int flags) {
             super.writeToParcel(out, flags);
             out.writeInt(open ? 1 : 0);
+            out.writeInt(mCollapsibleHeightReserved);
         }
 
         public static final Parcelable.Creator<SavedState> CREATOR =
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index ba7aef7..b51d4f5 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -1244,12 +1244,11 @@
 {
     if (mExitWithoutCleanup) {
         ALOGI("VM exiting with result code %d, cleanup skipped.", code);
-        ::_exit(code);
     } else {
         ALOGI("VM exiting with result code %d.", code);
         onExit(code);
-        ::exit(code);
     }
+    ::_exit(code);
 }
 
 void AndroidRuntime::onVmCreated(JNIEnv* env)
diff --git a/core/jni/android_os_Parcel.cpp b/core/jni/android_os_Parcel.cpp
index 483b455..e7a2fb4 100644
--- a/core/jni/android_os_Parcel.cpp
+++ b/core/jni/android_os_Parcel.cpp
@@ -273,7 +273,28 @@
     }
 }
 
-static void android_os_Parcel_writeString(JNIEnv* env, jclass clazz, jlong nativePtr, jstring val)
+static void android_os_Parcel_writeString8(JNIEnv* env, jclass clazz, jlong nativePtr, jstring val)
+{
+    Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
+    if (parcel != NULL) {
+        status_t err = NO_MEMORY;
+        if (val) {
+            const size_t len = env->GetStringUTFLength(val);
+            const char* str = env->GetStringUTFChars(val, 0);
+            if (str) {
+                err = parcel->writeString8(str, len);
+                env->ReleaseStringUTFChars(val, str);
+            }
+        } else {
+            err = parcel->writeString8(NULL, 0);
+        }
+        if (err != NO_ERROR) {
+            signalExceptionForError(env, clazz, err);
+        }
+    }
+}
+
+static void android_os_Parcel_writeString16(JNIEnv* env, jclass clazz, jlong nativePtr, jstring val)
 {
     Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
     if (parcel != NULL) {
@@ -444,7 +465,21 @@
     return 0;
 }
 
-static jstring android_os_Parcel_readString(JNIEnv* env, jclass clazz, jlong nativePtr)
+static jstring android_os_Parcel_readString8(JNIEnv* env, jclass clazz, jlong nativePtr)
+{
+    Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
+    if (parcel != NULL) {
+        size_t len;
+        const char* str = parcel->readString8Inplace(&len);
+        if (str) {
+            return env->NewStringUTF(str);
+        }
+        return NULL;
+    }
+    return NULL;
+}
+
+static jstring android_os_Parcel_readString16(JNIEnv* env, jclass clazz, jlong nativePtr)
 {
     Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
     if (parcel != NULL) {
@@ -722,7 +757,9 @@
     // @FastNative
     {"nativeWriteDouble",         "(JD)V", (void*)android_os_Parcel_writeDouble},
     // @FastNative
-    {"nativeWriteString",         "(JLjava/lang/String;)V", (void*)android_os_Parcel_writeString},
+    {"nativeWriteString8",        "(JLjava/lang/String;)V", (void*)android_os_Parcel_writeString8},
+    // @FastNative
+    {"nativeWriteString16",       "(JLjava/lang/String;)V", (void*)android_os_Parcel_writeString16},
     // @FastNative
     {"nativeWriteStrongBinder",   "(JLandroid/os/IBinder;)V", (void*)android_os_Parcel_writeStrongBinder},
     // @FastNative
@@ -740,7 +777,9 @@
     // @CriticalNative
     {"nativeReadDouble",          "(J)D", (void*)android_os_Parcel_readDouble},
     // @FastNative
-    {"nativeReadString",          "(J)Ljava/lang/String;", (void*)android_os_Parcel_readString},
+    {"nativeReadString8",         "(J)Ljava/lang/String;", (void*)android_os_Parcel_readString8},
+    // @FastNative
+    {"nativeReadString16",        "(J)Ljava/lang/String;", (void*)android_os_Parcel_readString16},
     // @FastNative
     {"nativeReadStrongBinder",    "(J)Landroid/os/IBinder;", (void*)android_os_Parcel_readStrongBinder},
     // @FastNative
diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp
index 924dc4b..21985f0 100644
--- a/core/jni/com_android_internal_os_Zygote.cpp
+++ b/core/jni/com_android_internal_os_Zygote.cpp
@@ -119,6 +119,7 @@
 
 static pid_t gSystemServerPid = 0;
 
+static constexpr const char* kVoldAppDataIsolation = "persist.sys.vold_app_data_isolation_enabled";
 static constexpr const char* kPropFuse = "persist.sys.fuse";
 static const char kZygoteClassName[] = "com/android/internal/os/Zygote";
 static jclass gZygoteClass;
@@ -831,6 +832,7 @@
              multiuser_get_uid(user_id, AID_EVERYBODY), fail_fn);
 
   bool isFuse = GetBoolProperty(kPropFuse, false);
+  bool isAppDataIsolationEnabled = GetBoolProperty(kVoldAppDataIsolation, false);
 
   if (isFuse) {
     if (mount_mode == MOUNT_EXTERNAL_PASS_THROUGH) {
@@ -840,6 +842,9 @@
     } else if (mount_mode == MOUNT_EXTERNAL_INSTALLER) {
       const std::string installer_source = StringPrintf("/mnt/installer/%d", user_id);
       BindMount(installer_source, "/storage", fail_fn);
+    } else if (isAppDataIsolationEnabled && mount_mode == MOUNT_EXTERNAL_ANDROID_WRITABLE) {
+      const std::string writable_source = StringPrintf("/mnt/androidwritable/%d", user_id);
+      BindMount(writable_source, "/storage", fail_fn);
     } else {
       BindMount(user_source, "/storage", fail_fn);
     }
diff --git a/core/proto/android/app/settings_enums.proto b/core/proto/android/app/settings_enums.proto
index 3e007e4..997829e 100644
--- a/core/proto/android/app/settings_enums.proto
+++ b/core/proto/android/app/settings_enums.proto
@@ -2523,16 +2523,10 @@
     // OS: R
     PANEL_ADD_WIFI_NETWORKS = 1809;
 
-    // OPEN: Settings > Accessibility > Enable accessibility service > Show tutorial dialog
+    // OPEN: Settings > Accessibility > Enable the feature or shortcut > Show tutorial dialog
     // CATEGORY: SETTINGS
     // OS: R
-    DIALOG_TOGGLE_SCREEN_ACCESSIBILITY_BUTTON = 1810;
-
-    // OPEN: Settings > Accessibility > Enable accessibility service > Show tutorial dialog in
-    // gesture mode
-    // CATEGORY: SETTINGS
-    // OS: R
-    DIALOG_TOGGLE_SCREEN_GESTURE_NAVIGATION = 1811;
+    DIALOG_ACCESSIBILITY_TUTORIAL = 1810;
 
     // OPEN: Settings > Accessibility > Edit shortcut dialog
     // CATEGORY: SETTINGS
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 6e8b9de..ee25ac2 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -1111,12 +1111,13 @@
          grants your app this permission. If you don't need this permission, be sure your <a
          href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#target">{@code
          targetSdkVersion}</a> is 4 or higher.
-         <p>Protection level: normal
+         <p>Protection level: dangerous
     -->
     <permission android:name="android.permission.READ_PHONE_STATE"
+        android:permissionGroup="android.permission-group.UNDEFINED"
         android:label="@string/permlab_readPhoneState"
         android:description="@string/permdesc_readPhoneState"
-        android:protectionLevel="normal" />
+        android:protectionLevel="dangerous" />
 
     <!-- Allows read access to the device's phone number(s). This is a subset of the capabilities
          granted by {@link #READ_PHONE_STATE} but is exposed to instant applications.
diff --git a/core/res/res/layout/resolver_empty_states.xml b/core/res/res/layout/resolver_empty_states.xml
index 5890bed..fe11769 100644
--- a/core/res/res/layout/resolver_empty_states.xml
+++ b/core/res/res/layout/resolver_empty_states.xml
@@ -24,6 +24,7 @@
     android:paddingStart="24dp"
     android:paddingEnd="24dp">
     <RelativeLayout
+        android:id="@+id/resolver_empty_state_container"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:paddingTop="48dp"
@@ -43,6 +44,7 @@
             android:fontFamily="@string/config_headlineFontFamilyMedium"
             android:textColor="@color/resolver_empty_state_text"
             android:textSize="14sp"
+            android:gravity="center_horizontal"
             android:layout_centerHorizontal="true" />
         <TextView
             android:id="@+id/resolver_empty_state_subtitle"
diff --git a/core/res/res/layout/resolver_list.xml b/core/res/res/layout/resolver_list.xml
index b754e0c..76ecefc 100644
--- a/core/res/res/layout/resolver_list.xml
+++ b/core/res/res/layout/resolver_list.xml
@@ -112,59 +112,61 @@
             </FrameLayout>
         </LinearLayout>
     </TabHost>
-
-    <View
-        android:layout_alwaysShow="true"
-        android:layout_width="match_parent"
-        android:layout_height="1dp"
-        android:background="?attr/colorBackgroundFloating"
-        android:foreground="?attr/dividerVertical" />
-
     <LinearLayout
-        android:id="@+id/button_bar"
-        android:visibility="gone"
-        style="?attr/buttonBarStyle"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_ignoreOffset="true"
         android:layout_alwaysShow="true"
-        android:layout_hasNestedScrollIndicator="true"
-        android:gravity="end|center_vertical"
-        android:orientation="horizontal"
-        android:layoutDirection="locale"
-        android:measureWithLargestChild="true"
-        android:background="?attr/colorBackgroundFloating"
-        android:paddingTop="@dimen/resolver_button_bar_spacing"
-        android:paddingBottom="@dimen/resolver_button_bar_spacing"
-        android:paddingStart="@dimen/resolver_edge_margin"
-        android:paddingEnd="@dimen/resolver_small_margin"
-        android:elevation="@dimen/resolver_elevation">
-
-        <Button
-            android:id="@+id/button_once"
-            android:layout_width="wrap_content"
-            android:layout_gravity="start"
-            android:maxLines="2"
-            style="?attr/buttonBarButtonStyle"
-            android:fontFamily="@android:string/config_headlineFontFamilyMedium"
+        android:orientation="vertical"
+        android:background="?attr/colorBackgroundFloating">
+        <View
+            android:id="@+id/resolver_button_bar_divider"
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:background="?attr/colorBackgroundFloating"
+            android:foreground="?attr/dividerVertical" />
+        <LinearLayout
+            android:id="@+id/button_bar"
+            android:visibility="gone"
+            style="?attr/buttonBarStyle"
+            android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:textAllCaps="false"
-            android:enabled="false"
-            android:text="@string/activity_resolver_use_once"
-            android:onClick="onButtonClick" />
+            android:layout_ignoreOffset="true"
+            android:layout_hasNestedScrollIndicator="true"
+            android:gravity="end|center_vertical"
+            android:orientation="horizontal"
+            android:layoutDirection="locale"
+            android:measureWithLargestChild="true"
+            android:paddingTop="@dimen/resolver_button_bar_spacing"
+            android:paddingBottom="@dimen/resolver_button_bar_spacing"
+            android:paddingStart="@dimen/resolver_edge_margin"
+            android:paddingEnd="@dimen/resolver_small_margin"
+            android:elevation="@dimen/resolver_elevation">
 
-        <Button
-            android:id="@+id/button_always"
-            android:layout_width="wrap_content"
-            android:layout_gravity="end"
-            android:maxLines="2"
-            style="?attr/buttonBarButtonStyle"
-            android:fontFamily="@android:string/config_headlineFontFamilyMedium"
-            android:textAllCaps="false"
-            android:layout_height="wrap_content"
-            android:enabled="false"
-            android:text="@string/activity_resolver_use_always"
-            android:onClick="onButtonClick" />
+            <Button
+                android:id="@+id/button_once"
+                android:layout_width="wrap_content"
+                android:layout_gravity="start"
+                android:maxLines="2"
+                style="?attr/buttonBarButtonStyle"
+                android:fontFamily="@android:string/config_headlineFontFamilyMedium"
+                android:layout_height="wrap_content"
+                android:textAllCaps="false"
+                android:enabled="false"
+                android:text="@string/activity_resolver_use_once"
+                android:onClick="onButtonClick" />
+
+            <Button
+                android:id="@+id/button_always"
+                android:layout_width="wrap_content"
+                android:layout_gravity="end"
+                android:maxLines="2"
+                style="?attr/buttonBarButtonStyle"
+                android:fontFamily="@android:string/config_headlineFontFamilyMedium"
+                android:textAllCaps="false"
+                android:layout_height="wrap_content"
+                android:enabled="false"
+                android:text="@string/activity_resolver_use_always"
+                android:onClick="onButtonClick" />
+        </LinearLayout>
     </LinearLayout>
-
 </com.android.internal.widget.ResolverDrawerLayout>
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 4065a6c..a8d1605 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -2527,6 +2527,21 @@
             <flag name="noExcludeDescendants" value="0x8" />
         </attr>
 
+        <!-- Hints the Android System whether the this View should be considered a scroll capture target. -->
+        <attr name="scrollCaptureHint">
+            <!-- Let the Android System  determine if the view can be a scroll capture target. -->
+            <flag name="auto" value="0" />
+            <!-- Hint the Android System that this view is a likely target. If capable, it will
+                 be ranked above other views without this flag. -->
+            <flag name="include" value="0x1" />
+            <!-- Hint the Android System that this view should never be considered a scroll capture
+                 target. -->
+            <flag name="exclude" value="0x2" />
+            <!-- Hint the Android System that this view's children should not be examined and should
+                 be excluded as a scroll capture target. -->
+            <flag name="excludeDescendants" value="0x4" />
+        </attr>
+
         <!-- Boolean that controls whether a view can take focus while in touch mode.
              If this is true for a view, that view can gain focus when clicked on, and can keep
              focus if another view is clicked on that doesn't have this attribute set to true. -->
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 1a98553..6fac46e 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1908,7 +1908,7 @@
     <!-- The name of the package that will hold the call screening role by default. -->
     <string name="config_defaultCallScreening" translatable="false"></string>
     <!-- The name of the package that will hold the system gallery role. -->
-    <string name="config_systemGallery" translatable="false">com.android.gallery</string>
+    <string name="config_systemGallery" translatable="false">com.android.gallery3d</string>
 
     <!-- The name of the package that will be allowed to change its components' label/icon. -->
     <string name="config_overrideComponentUiPackage" translatable="false"></string>
@@ -3496,10 +3496,9 @@
 
     <!-- Do not translate. Mcc codes whose existence trigger the presence of emergency
          affordances-->
-    <integer-array name="config_emergency_mcc_codes" translatable="false">
-        <item>404</item>
-        <item>405</item>
-    </integer-array>
+    <string-array name="config_emergency_iso_country_codes" translatable="false">
+        <item>in</item>
+    </string-array>
 
     <!-- Package name for the device provisioning package. -->
     <string name="config_deviceProvisioningPackage"></string>
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index 67d20da..fb887c3 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -3020,6 +3020,8 @@
       <public name="preserveLegacyExternalStorage" />
       <public name="mimeGroup" />
       <public name="gwpAsanMode" />
+      <!-- @hide -->
+      <public name="scrollCaptureHint" />
     </public-group>
 
     <public-group type="drawable" first-id="0x010800b5">
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 05c00ce..4efd7ff 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3085,7 +3085,7 @@
   <java-symbol type="string" name="global_action_emergency" />
   <java-symbol type="string" name="config_emergency_call_number" />
   <java-symbol type="string" name="config_emergency_dialer_package" />
-  <java-symbol type="array" name="config_emergency_mcc_codes" />
+  <java-symbol type="array" name="config_emergency_iso_country_codes" />
 
   <java-symbol type="string" name="config_dozeDoubleTapSensorType" />
   <java-symbol type="string" name="config_dozeTapSensorType" />
@@ -3921,6 +3921,8 @@
   <java-symbol type="id" name="resolver_empty_state_button" />
   <java-symbol type="id" name="resolver_empty_state_progress" />
   <java-symbol type="id" name="resolver_tab_divider" />
+  <java-symbol type="id" name="resolver_button_bar_divider" />
+  <java-symbol type="id" name="resolver_empty_state_container" />
   <java-symbol type="string" name="resolver_cant_share_with_work_apps" />
   <java-symbol type="string" name="resolver_cant_share_with_work_apps_explanation" />
   <java-symbol type="string" name="resolver_cant_share_with_personal_apps" />
diff --git a/core/res/res/xml/default_zen_mode_config.xml b/core/res/res/xml/default_zen_mode_config.xml
index 9110661..873b9eb 100644
--- a/core/res/res/xml/default_zen_mode_config.xml
+++ b/core/res/res/xml/default_zen_mode_config.xml
@@ -20,8 +20,8 @@
 <!-- Default configuration for zen mode.  See android.service.notification.ZenModeConfig. -->
 <zen version="9">
     <allow alarms="true" media="true" system="false" calls="true" callsFrom="2" messages="false"
-            reminders="false" events="false" repeatCallers="true" conversations="true"
-            conversationsFrom="2"/>
+            reminders="false" events="false" repeatCallers="true" convos="false"
+            convosFrom="3"/>
     <automatic ruleId="EVENTS_DEFAULT_RULE" enabled="false" snoozing="false" name="Event" zen="1"
                component="android/com.android.server.notification.EventConditionProvider"
                conditionId="condition://android/event?userId=-10000&amp;calendar=&amp;reply=1"/>
diff --git a/core/tests/benchmarks/src/android/os/ParcelStringBenchmark.java b/core/tests/benchmarks/src/android/os/ParcelStringBenchmark.java
new file mode 100644
index 0000000..daa90c2
--- /dev/null
+++ b/core/tests/benchmarks/src/android/os/ParcelStringBenchmark.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+import com.google.caliper.AfterExperiment;
+import com.google.caliper.BeforeExperiment;
+import com.google.caliper.Param;
+
+public class ParcelStringBenchmark {
+
+    @Param({"com.example.typical_package_name", "從不喜歡孤單一個 - 蘇永康/吳雨霏"})
+    String mValue;
+
+    private Parcel mParcel;
+
+    @BeforeExperiment
+    protected void setUp() {
+        mParcel = Parcel.obtain();
+    }
+
+    @AfterExperiment
+    protected void tearDown() {
+        mParcel.recycle();
+        mParcel = null;
+    }
+
+    public void timeWriteString8(int reps) {
+        for (int i = 0; i < reps; i++) {
+            mParcel.setDataPosition(0);
+            mParcel.writeString8(mValue);
+        }
+    }
+
+    public void timeReadString8(int reps) {
+        mParcel.writeString8(mValue);
+
+        for (int i = 0; i < reps; i++) {
+            mParcel.setDataPosition(0);
+            mParcel.readString8();
+        }
+    }
+
+    public void timeWriteString16(int reps) {
+        for (int i = 0; i < reps; i++) {
+            mParcel.setDataPosition(0);
+            mParcel.writeString16(mValue);
+        }
+    }
+
+    public void timeReadString16(int reps) {
+        mParcel.writeString16(mValue);
+
+        for (int i = 0; i < reps; i++) {
+            mParcel.setDataPosition(0);
+            mParcel.readString16();
+        }
+    }
+}
diff --git a/core/tests/coretests/src/android/os/ParcelTest.java b/core/tests/coretests/src/android/os/ParcelTest.java
index 46873b9..dcb3e2f 100644
--- a/core/tests/coretests/src/android/os/ParcelTest.java
+++ b/core/tests/coretests/src/android/os/ParcelTest.java
@@ -87,4 +87,27 @@
 
         p.recycle();
     }
+
+    /**
+     * Verify that writing/reading UTF-8 and UTF-16 strings works well.
+     */
+    @Test
+    public void testStrings() {
+        final String[] strings = {
+                null, "", "abc\0def", "com.example.typical_package_name",
+                "從不喜歡孤單一個 - 蘇永康/吳雨霏", "example"
+        };
+
+        final Parcel p = Parcel.obtain();
+        for (String string : strings) {
+            p.writeString8(string);
+            p.writeString16(string);
+        }
+
+        p.setDataPosition(0);
+        for (String string : strings) {
+            assertEquals(string, p.readString8());
+            assertEquals(string, p.readString16());
+        }
+    }
 }
diff --git a/core/tests/coretests/src/android/view/ScrollCaptureClientTest.java b/core/tests/coretests/src/android/view/ScrollCaptureClientTest.java
new file mode 100644
index 0000000..e6ac2d6
--- /dev/null
+++ b/core/tests/coretests/src/android/view/ScrollCaptureClientTest.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
+import static androidx.test.InstrumentationRegistry.getTargetContext;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Handler;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.mockito.stubbing.Answer;
+
+/**
+ * Tests of {@link ScrollCaptureClient}.
+ */
+@SuppressWarnings("UnnecessaryLocalVariable")
+@RunWith(AndroidJUnit4.class)
+public class ScrollCaptureClientTest {
+
+    private final Point mPositionInWindow = new Point(1, 2);
+    private final Rect mLocalVisibleRect = new Rect(2, 3, 4, 5);
+    private final Rect mScrollBounds = new Rect(3, 4, 5, 6);
+
+    private Handler mHandler;
+    private ScrollCaptureTarget mTarget1;
+
+    @Mock
+    private Surface mSurface;
+    @Mock
+    private IScrollCaptureController mClientCallbacks;
+    @Mock
+    private View mMockView1;
+    @Mock
+    private ScrollCaptureCallback mCallback1;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mHandler = new Handler(getTargetContext().getMainLooper());
+
+        when(mMockView1.getHandler()).thenReturn(mHandler);
+        when(mMockView1.getScrollCaptureHint()).thenReturn(View.SCROLL_CAPTURE_HINT_INCLUDE);
+
+        mTarget1 = new ScrollCaptureTarget(
+                mMockView1, mLocalVisibleRect, mPositionInWindow, mCallback1);
+        mTarget1.setScrollBounds(mScrollBounds);
+    }
+
+    /** Test the DelayedAction timeout helper class works as expected. */
+    @Test
+    public void testDelayedAction() {
+        Runnable action = Mockito.mock(Runnable.class);
+        ScrollCaptureClient.DelayedAction delayed =
+                new ScrollCaptureClient.DelayedAction(mHandler, 100, action);
+        try {
+            Thread.sleep(200);
+        } catch (InterruptedException ex) {
+            /* ignore */
+        }
+        getInstrumentation().waitForIdleSync();
+        assertFalse(delayed.cancel());
+        assertFalse(delayed.timeoutNow());
+        verify(action, times(1)).run();
+    }
+
+    /** Test the DelayedAction cancel() */
+    @Test
+    public void testDelayedAction_cancel() {
+        Runnable action = Mockito.mock(Runnable.class);
+        ScrollCaptureClient.DelayedAction delayed =
+                new ScrollCaptureClient.DelayedAction(mHandler, 100, action);
+        try {
+            Thread.sleep(50);
+        } catch (InterruptedException ex) {
+            /* ignore */
+        }
+        assertTrue(delayed.cancel());
+        assertFalse(delayed.timeoutNow());
+        try {
+            Thread.sleep(200);
+        } catch (InterruptedException ex) {
+            /* ignore */
+        }
+        getInstrumentation().waitForIdleSync();
+        verify(action, never()).run();
+    }
+
+    /** Test the DelayedAction timeoutNow() - for testing only */
+    @Test
+    public void testDelayedAction_timeoutNow() {
+        Runnable action = Mockito.mock(Runnable.class);
+        ScrollCaptureClient.DelayedAction delayed =
+                new ScrollCaptureClient.DelayedAction(mHandler, 100, action);
+        try {
+            Thread.sleep(50);
+        } catch (InterruptedException ex) {
+            /* ignore */
+        }
+        assertTrue(delayed.timeoutNow());
+        assertFalse(delayed.cancel());
+        getInstrumentation().waitForIdleSync();
+        verify(action, times(1)).run();
+    }
+
+    /** Test creating a client with valid info */
+    @Test
+    public void testConstruction() {
+        new ScrollCaptureClient(mTarget1, mClientCallbacks);
+    }
+
+    /** Test creating a client fails if arguments are not valid. */
+    @Test
+    public void testConstruction_requiresScrollBounds() {
+        try {
+            mTarget1.setScrollBounds(null);
+            new ScrollCaptureClient(mTarget1, mClientCallbacks);
+            fail("An exception was expected.");
+        } catch (RuntimeException ex) {
+            // Ignore, expected.
+        }
+    }
+
+    @SuppressWarnings("SameParameterValue")
+    private static Answer<Void> runRunnable(int arg) {
+        return invocation -> {
+            Runnable r = invocation.getArgument(arg);
+            r.run();
+            return null;
+        };
+    }
+
+    @SuppressWarnings("SameParameterValue")
+    private static Answer<Void> reportBufferSent(int sessionArg, long frameNum, Rect capturedArea) {
+        return invocation -> {
+            ScrollCaptureSession session = invocation.getArgument(sessionArg);
+            session.notifyBufferSent(frameNum, capturedArea);
+            return null;
+        };
+    }
+
+    /** @see ScrollCaptureClient#startCapture(Surface) */
+    @Test
+    public void testStartCapture() throws Exception {
+        final ScrollCaptureClient client = new ScrollCaptureClient(mTarget1, mClientCallbacks);
+
+        // Have the session start accepted immediately
+        doAnswer(runRunnable(1)).when(mCallback1)
+                .onScrollCaptureStart(any(ScrollCaptureSession.class), any(Runnable.class));
+        client.startCapture(mSurface);
+        getInstrumentation().waitForIdleSync();
+
+        verify(mCallback1, times(1))
+                .onScrollCaptureStart(any(ScrollCaptureSession.class), any(Runnable.class));
+        verify(mClientCallbacks, times(1)).onCaptureStarted();
+        verifyNoMoreInteractions(mClientCallbacks);
+    }
+
+    @Test
+    public void testStartCaptureTimeout() throws Exception {
+        final ScrollCaptureClient client = new ScrollCaptureClient(mTarget1, mClientCallbacks);
+        client.startCapture(mSurface);
+
+        // Force timeout to fire
+        client.getTimeoutAction().timeoutNow();
+
+        getInstrumentation().waitForIdleSync();
+        verify(mCallback1, times(1)).onScrollCaptureEnd(any(Runnable.class));
+    }
+
+    private void startClient(ScrollCaptureClient client) throws Exception {
+        doAnswer(runRunnable(1)).when(mCallback1)
+                .onScrollCaptureStart(any(ScrollCaptureSession.class), any(Runnable.class));
+        client.startCapture(mSurface);
+        getInstrumentation().waitForIdleSync();
+        reset(mCallback1, mClientCallbacks);
+    }
+
+    /** @see ScrollCaptureClient#requestImage(Rect) */
+    @Test
+    public void testRequestImage() throws Exception {
+        final ScrollCaptureClient client = new ScrollCaptureClient(mTarget1, mClientCallbacks);
+        startClient(client);
+
+        // Stub the callback to complete the request immediately
+        doAnswer(reportBufferSent(/* sessionArg */ 0, /* frameNum */ 1L, new Rect(1, 2, 3, 4)))
+                .when(mCallback1)
+                .onScrollCaptureImageRequest(any(ScrollCaptureSession.class), any(Rect.class));
+
+        // Make the inbound binder call
+        client.requestImage(new Rect(1, 2, 3, 4));
+
+        // Wait for handler thread dispatch
+        getInstrumentation().waitForIdleSync();
+        verify(mCallback1, times(1)).onScrollCaptureImageRequest(
+                any(ScrollCaptureSession.class), eq(new Rect(1, 2, 3, 4)));
+
+        // Wait for binder thread dispatch
+        getInstrumentation().waitForIdleSync();
+        verify(mClientCallbacks, times(1)).onCaptureBufferSent(eq(1L), eq(new Rect(1, 2, 3, 4)));
+
+        verifyNoMoreInteractions(mCallback1, mClientCallbacks);
+    }
+
+    @Test
+    public void testRequestImageTimeout() throws Exception {
+        final ScrollCaptureClient client = new ScrollCaptureClient(mTarget1, mClientCallbacks);
+        startClient(client);
+
+        // Make the inbound binder call
+        client.requestImage(new Rect(1, 2, 3, 4));
+
+        // Wait for handler thread dispatch
+        getInstrumentation().waitForIdleSync();
+        verify(mCallback1, times(1)).onScrollCaptureImageRequest(
+                any(ScrollCaptureSession.class), eq(new Rect(1, 2, 3, 4)));
+
+        // Force timeout to fire
+        client.getTimeoutAction().timeoutNow();
+        getInstrumentation().waitForIdleSync();
+
+        // (callback not stubbed, does nothing)
+        // Timeout triggers request to end capture
+        verify(mCallback1, times(1)).onScrollCaptureEnd(any(Runnable.class));
+        verifyNoMoreInteractions(mCallback1, mClientCallbacks);
+    }
+
+    /** @see ScrollCaptureClient#endCapture() */
+    @Test
+    public void testEndCapture() throws Exception {
+        final ScrollCaptureClient client = new ScrollCaptureClient(mTarget1, mClientCallbacks);
+        startClient(client);
+
+        // Stub the callback to complete the request immediately
+        doAnswer(runRunnable(0))
+                .when(mCallback1)
+                .onScrollCaptureEnd(any(Runnable.class));
+
+        // Make the inbound binder call
+        client.endCapture();
+
+        // Wait for handler thread dispatch
+        getInstrumentation().waitForIdleSync();
+        verify(mCallback1, times(1)).onScrollCaptureEnd(any(Runnable.class));
+
+        // Wait for binder thread dispatch
+        getInstrumentation().waitForIdleSync();
+        verify(mClientCallbacks, times(1)).onConnectionClosed();
+
+        verifyNoMoreInteractions(mCallback1, mClientCallbacks);
+    }
+
+    @Test
+    public void testEndCaptureTimeout() throws Exception {
+        final ScrollCaptureClient client = new ScrollCaptureClient(mTarget1, mClientCallbacks);
+        startClient(client);
+
+        // Make the inbound binder call
+        client.endCapture();
+
+        // Wait for handler thread dispatch
+        getInstrumentation().waitForIdleSync();
+        verify(mCallback1, times(1)).onScrollCaptureEnd(any(Runnable.class));
+
+        // Force timeout to fire
+        client.getTimeoutAction().timeoutNow();
+
+        // Wait for binder thread dispatch
+        getInstrumentation().waitForIdleSync();
+        verify(mClientCallbacks, times(1)).onConnectionClosed();
+
+        verifyNoMoreInteractions(mCallback1, mClientCallbacks);
+    }
+}
diff --git a/core/tests/coretests/src/android/view/ScrollCaptureTargetResolverTest.java b/core/tests/coretests/src/android/view/ScrollCaptureTargetResolverTest.java
new file mode 100644
index 0000000..8b21b8e
--- /dev/null
+++ b/core/tests/coretests/src/android/view/ScrollCaptureTargetResolverTest.java
@@ -0,0 +1,498 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static androidx.test.InstrumentationRegistry.getTargetContext;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Handler;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.LinkedList;
+import java.util.function.Consumer;
+
+/**
+ * Tests of {@link ScrollCaptureTargetResolver}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class ScrollCaptureTargetResolverTest {
+
+    private static final long TEST_TIMEOUT_MS = 2000;
+    private static final long RESOLVER_TIMEOUT_MS = 1000;
+
+    private Handler mHandler;
+    private TargetConsumer mTargetConsumer;
+
+    @Before
+    public void setUp() {
+        mTargetConsumer = new TargetConsumer();
+        mHandler = new Handler(getTargetContext().getMainLooper());
+    }
+
+    @Test(timeout = TEST_TIMEOUT_MS)
+    public void testEmptyQueue() throws InterruptedException {
+        ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(new LinkedList<>());
+        resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer);
+
+        // Test only
+        resolver.waitForResult();
+
+        ScrollCaptureTarget result = mTargetConsumer.getLastValue();
+        assertNull("Expected null due to empty queue", result);
+    }
+
+    @Test(timeout = TEST_TIMEOUT_MS)
+    public void testNoValidTargets() throws InterruptedException {
+        LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>();
+
+        // Supplies scrollBounds = null
+        FakeScrollCaptureCallback callback1 = new FakeScrollCaptureCallback();
+        callback1.setScrollBounds(null);
+        ScrollCaptureTarget target1 = createTarget(callback1, new Rect(20, 30, 40, 50),
+                new Point(0, 0), View.SCROLL_CAPTURE_HINT_AUTO);
+
+        // Supplies scrollBounds = empty rect
+        FakeScrollCaptureCallback callback2 = new FakeScrollCaptureCallback();
+        callback2.setScrollBounds(new Rect());
+        ScrollCaptureTarget target2 = createTarget(callback2, new Rect(20, 30, 40, 50),
+                new Point(0, 20), View.SCROLL_CAPTURE_HINT_INCLUDE);
+
+        targetQueue.add(target1);
+        targetQueue.add(target2);
+
+        ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue);
+        resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer);
+
+        // Test only
+        resolver.waitForResult();
+
+        ScrollCaptureTarget result = mTargetConsumer.getLastValue();
+        assertNull("Expected null due to no valid targets", result);
+    }
+
+    @Test(timeout = TEST_TIMEOUT_MS)
+    public void testSingleTarget() throws InterruptedException {
+        FakeScrollCaptureCallback callback = new FakeScrollCaptureCallback();
+        ScrollCaptureTarget target = createTarget(callback,
+                new Rect(20, 30, 40, 50), new Point(10, 10),
+                View.SCROLL_CAPTURE_HINT_AUTO);
+        callback.setScrollBounds(new Rect(2, 2, 18, 18));
+
+        LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>();
+        targetQueue.add(target);
+        ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue);
+        resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer);
+
+        // Test only
+        resolver.waitForResult();
+
+        ScrollCaptureTarget result = mTargetConsumer.getLastValue();
+        assertSame("Excepted the same target as a result", target, result);
+        assertEquals("result has wrong scroll bounds",
+                new Rect(2, 2, 18, 18), result.getScrollBounds());
+    }
+
+    @Test(timeout = TEST_TIMEOUT_MS)
+    public void testSingleTarget_backgroundThread() throws InterruptedException {
+        BackgroundTestCallback callback1 = new BackgroundTestCallback();
+        ScrollCaptureTarget target1 = createTarget(callback1,
+                new Rect(20, 30, 40, 50), new Point(10, 10),
+                View.SCROLL_CAPTURE_HINT_AUTO);
+        callback1.setDelay(100);
+        callback1.setScrollBounds(new Rect(2, 2, 18, 18));
+
+        LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>();
+        targetQueue.add(target1);
+
+        ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue);
+        resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer);
+
+        // Test only
+        resolver.waitForResult();
+
+        ScrollCaptureTarget result = mTargetConsumer.getLastValue();
+        assertSame("Excepted the single target1 as a result", target1, result);
+        assertEquals("Result has wrong scroll bounds",
+                new Rect(2, 2, 18, 18), result.getScrollBounds());
+    }
+
+    @Test(timeout = TEST_TIMEOUT_MS)
+    public void testPreferNonEmptyBounds() throws InterruptedException {
+        LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>();
+
+        FakeScrollCaptureCallback callback1 = new FakeScrollCaptureCallback();
+        callback1.setScrollBounds(new Rect());
+        ScrollCaptureTarget target1 = createTarget(callback1, new Rect(20, 30, 40, 50),
+                new Point(0, 0), View.SCROLL_CAPTURE_HINT_AUTO);
+
+        FakeScrollCaptureCallback callback2 = new FakeScrollCaptureCallback();
+        callback2.setScrollBounds(new Rect(0, 0, 20, 20));
+        ScrollCaptureTarget target2 = createTarget(callback2, new Rect(20, 30, 40, 50),
+                new Point(0, 20), View.SCROLL_CAPTURE_HINT_INCLUDE);
+
+        FakeScrollCaptureCallback callback3 = new FakeScrollCaptureCallback();
+        callback3.setScrollBounds(null);
+        ScrollCaptureTarget target3 = createTarget(callback3, new Rect(20, 30, 40, 50),
+                new Point(0, 40), View.SCROLL_CAPTURE_HINT_AUTO);
+
+        targetQueue.add(target1);
+        targetQueue.add(target2); // scrollBounds not null or empty()
+        targetQueue.add(target3);
+
+        ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue);
+        resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer);
+        resolver.waitForResult();
+
+        ScrollCaptureTarget result = mTargetConsumer.getLastValue();
+        assertEquals("Expected " + target2 + " as a result", target2, result);
+        assertEquals("result has wrong scroll bounds",
+                new Rect(0, 0, 20, 20), result.getScrollBounds());
+    }
+
+    @Test(timeout = TEST_TIMEOUT_MS)
+    public void testPreferHintInclude() throws InterruptedException {
+        LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>();
+
+        FakeScrollCaptureCallback callback1 = new FakeScrollCaptureCallback();
+        callback1.setScrollBounds(new Rect(0, 0, 20, 20));
+        ScrollCaptureTarget target1 = createTarget(callback1, new Rect(20, 30, 40, 50),
+                new Point(0, 0), View.SCROLL_CAPTURE_HINT_AUTO);
+
+        FakeScrollCaptureCallback callback2 = new FakeScrollCaptureCallback();
+        callback2.setScrollBounds(new Rect(1, 1, 19, 19));
+        ScrollCaptureTarget target2 = createTarget(callback2, new Rect(20, 30, 40, 50),
+                new Point(0, 20), View.SCROLL_CAPTURE_HINT_INCLUDE);
+
+        FakeScrollCaptureCallback callback3 = new FakeScrollCaptureCallback();
+        callback3.setScrollBounds(new Rect(2, 2, 18, 18));
+        ScrollCaptureTarget target3 = createTarget(callback3, new Rect(20, 30, 40, 50),
+                new Point(0, 40), View.SCROLL_CAPTURE_HINT_AUTO);
+
+        targetQueue.add(target1);
+        targetQueue.add(target2); // * INCLUDE > AUTO
+        targetQueue.add(target3);
+
+        ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue);
+        resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer);
+
+        resolver.waitForResult();
+
+        ScrollCaptureTarget result = mTargetConsumer.getLastValue();
+        assertEquals("input = " + targetQueue + " Expected " + target2
+                + " as the result, due to hint=INCLUDE", target2, result);
+        assertEquals("result has wrong scroll bounds",
+                new Rect(1, 1, 19, 19), result.getScrollBounds());
+    }
+
+    @Test(timeout = TEST_TIMEOUT_MS)
+    public void testDescendantPreferred() throws InterruptedException {
+        LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>();
+
+        ViewGroup targetView1 = new FakeRootView(getTargetContext(), 0, 0, 60, 60); // 60x60
+        ViewGroup targetView2 = new FakeRootView(getTargetContext(), 20, 30, 40, 50); // 20x20
+        ViewGroup targetView3 = new FakeRootView(getTargetContext(), 5, 5, 15, 15); // 10x10
+
+        targetView1.addView(targetView2);
+        targetView2.addView(targetView3);
+
+        // Create first target with an unrelated parent
+        FakeScrollCaptureCallback callback1 = new FakeScrollCaptureCallback();
+        callback1.setScrollBounds(new Rect(0, 0, 60, 60));
+        ScrollCaptureTarget target1 = createTargetWithView(targetView1, callback1,
+                new Rect(0, 0, 60, 60),
+                new Point(0, 0), View.SCROLL_CAPTURE_HINT_AUTO);
+
+        // Create second target associated with a view within parent2
+        FakeScrollCaptureCallback callback2 = new FakeScrollCaptureCallback();
+        callback2.setScrollBounds(new Rect(0, 0, 20, 20));
+        ScrollCaptureTarget target2 = createTargetWithView(targetView2, callback2,
+                new Rect(0, 0, 20, 20),
+                new Point(20, 30), View.SCROLL_CAPTURE_HINT_AUTO);
+
+        // Create third target associated with a view within parent3
+        FakeScrollCaptureCallback callback3 = new FakeScrollCaptureCallback();
+        callback3.setScrollBounds(new Rect(0, 0, 15, 15));
+        ScrollCaptureTarget target3 = createTargetWithView(targetView3, callback3,
+                new Rect(0, 0, 15, 15),
+                new Point(25, 35), View.SCROLL_CAPTURE_HINT_AUTO);
+
+        targetQueue.add(target1); // auto, 60x60
+        targetQueue.add(target2); // auto, 20x20
+        targetQueue.add(target3); // auto, 15x15 <- innermost scrollable
+
+        ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue);
+        resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer);
+
+        // Test only
+        resolver.waitForResult();
+
+        ScrollCaptureTarget result = mTargetConsumer.getLastValue();
+        assertSame("Expected target3 as the result, due to relation", target3, result);
+        assertEquals("result has wrong scroll bounds",
+                new Rect(0, 0, 15, 15), result.getScrollBounds());
+    }
+
+    /**
+     * If a timeout expires, late results are ignored.
+     */
+    @Test(timeout = TEST_TIMEOUT_MS)
+    public void testTimeout() throws InterruptedException {
+        LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>();
+
+        // callback 1, 10x10, hint=AUTO, responds immediately from bg thread
+        BackgroundTestCallback callback1 = new BackgroundTestCallback();
+        callback1.setScrollBounds(new Rect(5, 5, 15, 15));
+        ScrollCaptureTarget target1 = createTarget(
+                callback1, new Rect(20, 30, 40, 50), new Point(10, 10),
+                View.SCROLL_CAPTURE_HINT_AUTO);
+        targetQueue.add(target1);
+
+        // callback 2, 20x20, hint=AUTO, responds after 5s from bg thread
+        BackgroundTestCallback callback2 = new BackgroundTestCallback();
+        callback2.setScrollBounds(new Rect(0, 0, 20, 20));
+        callback2.setDelay(5000);
+        ScrollCaptureTarget target2 = createTarget(
+                callback2, new Rect(20, 30, 40, 50), new Point(10, 10),
+                View.SCROLL_CAPTURE_HINT_AUTO);
+        targetQueue.add(target2);
+
+        // callback 3, 20x20, hint=INCLUDE, responds after 10s from bg thread
+        BackgroundTestCallback callback3 = new BackgroundTestCallback();
+        callback3.setScrollBounds(new Rect(0, 0, 20, 20));
+        callback3.setDelay(10000);
+        ScrollCaptureTarget target3 = createTarget(
+                callback3, new Rect(20, 30, 40, 50), new Point(10, 10),
+                View.SCROLL_CAPTURE_HINT_INCLUDE);
+        targetQueue.add(target3);
+
+        // callback 1 will be received
+        // callback 2 & 3 will be ignored due to timeout
+
+        ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue);
+        resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer);
+
+        resolver.waitForResult();
+
+        ScrollCaptureTarget result = mTargetConsumer.getLastValue();
+        assertSame("Expected target1 as the result, due to timeouts of others", target1, result);
+        assertEquals("result has wrong scroll bounds",
+                new Rect(5, 5, 15, 15), result.getScrollBounds());
+        assertEquals("callback1 should have been called",
+                1, callback1.getOnScrollCaptureSearchCount());
+        assertEquals("callback2 should have been called",
+                1, callback2.getOnScrollCaptureSearchCount());
+        assertEquals("callback3 should have been called",
+                1, callback3.getOnScrollCaptureSearchCount());
+    }
+
+    @Test(timeout = TEST_TIMEOUT_MS)
+    public void testWithCallbackMultipleReplies() throws InterruptedException {
+        // Calls response methods 3 times each
+        RepeatingCaptureCallback callback1 = new RepeatingCaptureCallback(3);
+        callback1.setScrollBounds(new Rect(2, 2, 18, 18));
+        ScrollCaptureTarget target1 = createTarget(callback1, new Rect(20, 30, 40, 50),
+                new Point(10, 10), View.SCROLL_CAPTURE_HINT_AUTO);
+
+        FakeScrollCaptureCallback callback2 = new FakeScrollCaptureCallback();
+        callback2.setScrollBounds(new Rect(0, 0, 20, 20));
+        ScrollCaptureTarget target2 = createTarget(callback2, new Rect(20, 30, 40, 50),
+                new Point(10, 10), View.SCROLL_CAPTURE_HINT_AUTO);
+
+        LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>();
+        targetQueue.add(target1);
+        targetQueue.add(target2);
+
+        ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue);
+        resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer);
+
+        resolver.waitForResult();
+
+        ScrollCaptureTarget result = mTargetConsumer.getLastValue();
+        assertSame("Expected target2 as the result, due to hint=INCLUDE", target2, result);
+        assertEquals("result has wrong scroll bounds",
+                new Rect(0, 0, 20, 20), result.getScrollBounds());
+        assertEquals("callback1 should have been called once",
+                1, callback1.getOnScrollCaptureSearchCount());
+        assertEquals("callback2 should have been called once",
+                1, callback2.getOnScrollCaptureSearchCount());
+    }
+
+    private static class TargetConsumer implements Consumer<ScrollCaptureTarget> {
+        volatile ScrollCaptureTarget mResult;
+        int mAcceptCount;
+
+        ScrollCaptureTarget getLastValue() {
+            return mResult;
+        }
+
+        int acceptCount() {
+            return mAcceptCount;
+        }
+
+        @Override
+        public void accept(@Nullable ScrollCaptureTarget t) {
+            mAcceptCount++;
+            mResult = t;
+        }
+    }
+
+    private void setupTargetView(View view, Rect localVisibleRect, int scrollCaptureHint) {
+        view.setScrollCaptureHint(scrollCaptureHint);
+        view.onVisibilityAggregated(true);
+        // Treat any offset as padding, outset localVisibleRect on all sides and use this as
+        // child bounds
+        Rect bounds = new Rect(localVisibleRect);
+        bounds.inset(-bounds.left, -bounds.top, bounds.left, bounds.top);
+        view.layout(bounds.left, bounds.top, bounds.right, bounds.bottom);
+        view.onVisibilityAggregated(true);
+    }
+
+    private ScrollCaptureTarget createTarget(ScrollCaptureCallback callback, Rect localVisibleRect,
+            Point positionInWindow, int scrollCaptureHint) {
+        View mockView = new View(getTargetContext());
+        return createTargetWithView(mockView, callback, localVisibleRect, positionInWindow,
+                scrollCaptureHint);
+    }
+
+    private ScrollCaptureTarget createTargetWithView(View view, ScrollCaptureCallback callback,
+            Rect localVisibleRect, Point positionInWindow, int scrollCaptureHint) {
+        setupTargetView(view, localVisibleRect, scrollCaptureHint);
+        return new ScrollCaptureTarget(view, localVisibleRect, positionInWindow, callback);
+    }
+
+
+    static class FakeRootView extends ViewGroup implements ViewParent {
+        FakeRootView(Context context, int l, int t, int r, int b) {
+            super(context);
+            layout(l, t, r, b);
+        }
+
+        @Override
+        protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        }
+    }
+
+    static class FakeScrollCaptureCallback implements ScrollCaptureCallback {
+        private Rect mScrollBounds;
+        private long mDelayMillis;
+        private int mOnScrollCaptureSearchCount;
+
+        public int getOnScrollCaptureSearchCount() {
+            return mOnScrollCaptureSearchCount;
+        }
+
+        @Override
+        public void onScrollCaptureSearch(Consumer<Rect> onReady) {
+            mOnScrollCaptureSearchCount++;
+            run(() -> {
+                Rect b = getScrollBounds();
+                onReady.accept(b);
+            });
+        }
+
+        @Override
+        public void onScrollCaptureStart(ScrollCaptureSession session, Runnable onReady) {
+            run(onReady);
+        }
+
+        @Override
+        public void onScrollCaptureImageRequest(ScrollCaptureSession session, Rect captureArea) {
+            run(() -> session.notifyBufferSent(0, captureArea));
+        }
+
+        @Override
+        public void onScrollCaptureEnd(Runnable onReady) {
+            run(onReady);
+        }
+
+        public void setScrollBounds(@Nullable Rect scrollBounds) {
+            mScrollBounds = scrollBounds;
+        }
+
+        public void setDelay(long delayMillis) {
+            mDelayMillis = delayMillis;
+        }
+
+        protected Rect getScrollBounds() {
+            return mScrollBounds;
+        }
+
+        protected void run(Runnable r) {
+            delay();
+            r.run();
+        }
+
+        protected void delay() {
+            if (mDelayMillis > 0) {
+                try {
+                    Thread.sleep(mDelayMillis);
+                } catch (InterruptedException e) {
+                    // Ignore
+                }
+            }
+        }
+    }
+
+    static class RepeatingCaptureCallback extends FakeScrollCaptureCallback {
+        private int mRepeatCount;
+
+        RepeatingCaptureCallback(int repeatCount) {
+            mRepeatCount = repeatCount;
+        }
+
+        protected void run(Runnable r) {
+            delay();
+            for (int i = 0; i < mRepeatCount; i++) {
+                r.run();
+            }
+        }
+    }
+
+    /** Response to async calls on an arbitrary background thread */
+    static class BackgroundTestCallback extends FakeScrollCaptureCallback {
+        static int sCount = 0;
+        private void runOnBackgroundThread(Runnable r) {
+            final Runnable target = () -> {
+                delay();
+                r.run();
+            };
+            Thread t = new Thread(target);
+            synchronized (BackgroundTestCallback.this) {
+                sCount++;
+            }
+            t.setName("Background-Thread-" + sCount);
+            t.start();
+        }
+
+        @Override
+        protected void run(Runnable r) {
+            runOnBackgroundThread(r);
+        }
+    }
+}
diff --git a/core/tests/coretests/src/android/view/ViewGroupScrollCaptureTest.java b/core/tests/coretests/src/android/view/ViewGroupScrollCaptureTest.java
new file mode 100644
index 0000000..3af0533
--- /dev/null
+++ b/core/tests/coretests/src/android/view/ViewGroupScrollCaptureTest.java
@@ -0,0 +1,480 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.testng.AssertJUnit.assertSame;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.FlakyTest;
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.LinkedList;
+import java.util.Queue;
+
+/**
+ * Exercises Scroll Capture search in {@link ViewGroup}.
+ */
+@Presubmit
+@SmallTest
+@FlakyTest(detail = "promote once confirmed flake-free")
+@RunWith(MockitoJUnitRunner.class)
+public class ViewGroupScrollCaptureTest {
+
+    @Mock
+    ScrollCaptureCallback mMockCallback;
+    @Mock
+    ScrollCaptureCallback mMockCallback2;
+
+    /** Make sure the hint flags are saved and loaded correctly. */
+    @Test
+    public void testSetScrollCaptureHint() throws Exception {
+        final Context context = getInstrumentation().getContext();
+        final MockViewGroup viewGroup = new MockViewGroup(context);
+
+        assertNotNull(viewGroup);
+        assertEquals("Default scroll capture hint flags should be [SCROLL_CAPTURE_HINT_AUTO]",
+                ViewGroup.SCROLL_CAPTURE_HINT_AUTO, viewGroup.getScrollCaptureHint());
+
+        viewGroup.setScrollCaptureHint(View.SCROLL_CAPTURE_HINT_INCLUDE);
+        assertEquals("The scroll capture hint was not stored correctly.",
+                ViewGroup.SCROLL_CAPTURE_HINT_INCLUDE, viewGroup.getScrollCaptureHint());
+
+        viewGroup.setScrollCaptureHint(ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE);
+        assertEquals("The scroll capture hint was not stored correctly.",
+                ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE, viewGroup.getScrollCaptureHint());
+
+        viewGroup.setScrollCaptureHint(ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS);
+        assertEquals("The scroll capture hint was not stored correctly.",
+                ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS,
+                viewGroup.getScrollCaptureHint());
+
+        viewGroup.setScrollCaptureHint(ViewGroup.SCROLL_CAPTURE_HINT_INCLUDE
+                | ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS);
+        assertEquals("The scroll capture hint was not stored correctly.",
+                ViewGroup.SCROLL_CAPTURE_HINT_INCLUDE
+                        | ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS,
+                viewGroup.getScrollCaptureHint());
+
+        viewGroup.setScrollCaptureHint(ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE
+                | ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS);
+        assertEquals("The scroll capture hint was not stored correctly.",
+                ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE
+                        | ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS,
+                viewGroup.getScrollCaptureHint());
+    }
+
+    /**
+     * Ensure a ViewGroup with 'scrollCaptureHint=auto', but no ScrollCaptureCallback set dispatches
+     * correctly. Verifies that the framework helper is called. Verifies a that non-null callback
+     * return results in an expected target in the results.
+     */
+    @MediumTest
+    @Test
+    public void testDispatchScrollCaptureSearch_noCallback_hintAuto() throws Exception {
+        final Context context = getInstrumentation().getContext();
+        final MockViewGroup viewGroup = new MockViewGroup(context, 0, 0, 200, 200);
+
+        // When system internal scroll capture is requested, this callback is returned.
+        viewGroup.setScrollCaptureCallbackInternalForTest(mMockCallback);
+
+        Rect localVisibleRect = new Rect(0, 0, 200, 200);
+        Point windowOffset = new Point();
+        LinkedList<ScrollCaptureTarget> targetList = new LinkedList<>();
+
+        // Dispatch
+        viewGroup.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targetList);
+
+        // Verify the system checked for fallback support
+        viewGroup.assertDispatchScrollCaptureCount(1);
+        viewGroup.assertLastDispatchScrollCaptureArgs(localVisibleRect, windowOffset);
+
+        // Verify the target is as expected.
+        assertEquals(1, targetList.size());
+        ScrollCaptureTarget target = targetList.get(0);
+        assertSame("Target has the wrong callback", mMockCallback, target.getCallback());
+        assertSame("Target has the wrong View", viewGroup, target.getContainingView());
+        assertEquals("Target hint is incorrect", View.SCROLL_CAPTURE_HINT_AUTO,
+                target.getContainingView().getScrollCaptureHint());
+    }
+
+    /**
+     * Ensure a ViewGroup with 'scrollCaptureHint=exclude' is ignored. The Framework helper is
+     * stubbed to return a callback. Verifies that the framework helper is not called (because of
+     * exclude), and no scroll capture target is added to the results.
+     */
+    @MediumTest
+    @Test
+    public void testDispatchScrollCaptureSearch_noCallback_hintExclude() throws Exception {
+        final Context context = getInstrumentation().getContext();
+        final MockViewGroup viewGroup =
+                new MockViewGroup(context, 0, 0, 200, 200, View.SCROLL_CAPTURE_HINT_EXCLUDE);
+
+        // When system internal scroll capture is requested, this callback is returned.
+        viewGroup.setScrollCaptureCallbackInternalForTest(mMockCallback);
+
+        Rect localVisibleRect = new Rect(0, 0, 200, 200);
+        Point windowOffset = new Point();
+        LinkedList<ScrollCaptureTarget> targetList = new LinkedList<>();
+
+        // Dispatch
+        viewGroup.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targetList);
+
+        // Verify the results.
+        assertEquals("Target list size should be zero.", 0, targetList.size());
+    }
+
+    /**
+     * Ensure that a ViewGroup with 'scrollCaptureHint=auto', and a scroll capture callback set
+     * dispatches as expected. Also verifies that the system fallback support is not called, and the
+     * the returned target is constructed correctly.
+     */
+    @MediumTest
+    @Test
+    public void testDispatchScrollCaptureSearch_withCallback_hintAuto() throws Exception {
+        final Context context = getInstrumentation().getContext();
+        MockViewGroup viewGroup = new MockViewGroup(context, 0, 0, 200, 200);
+
+        // With an already provided scroll capture callback
+        viewGroup.setScrollCaptureCallback(mMockCallback);
+
+        // When system internal scroll capture is requested, this callback is returned.
+        viewGroup.setScrollCaptureCallbackInternalForTest(mMockCallback);
+
+        Rect localVisibleRect = new Rect(0, 0, 200, 200);
+        Point windowOffset = new Point();
+        LinkedList<ScrollCaptureTarget> targetList = new LinkedList<>();
+
+        // Dispatch to the ViewGroup
+        viewGroup.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targetList);
+
+        // Confirm that framework support was not requested,
+        // because this view already had a callback set.
+        viewGroup.assertCreateScrollCaptureCallbackInternalCount(0);
+
+        // Verify the target is as expected.
+        assertEquals(1, targetList.size());
+        ScrollCaptureTarget target = targetList.get(0);
+        assertSame("Target has the wrong callback", mMockCallback, target.getCallback());
+        assertSame("Target has the wrong View", viewGroup, target.getContainingView());
+        assertEquals("Target hint is incorrect", View.SCROLL_CAPTURE_HINT_AUTO,
+                target.getContainingView().getScrollCaptureHint());
+    }
+
+    /**
+     * Ensure a ViewGroup with a callback set, but 'scrollCaptureHint=exclude' is ignored. The
+     * exclude flag takes precedence.  Verifies that the framework helper is not called (because of
+     * exclude, and a callback being set), and no scroll capture target is added to the results.
+     */
+    @MediumTest
+    @Test
+    public void testDispatchScrollCaptureSearch_withCallback_hintExclude() throws Exception {
+        final Context context = getInstrumentation().getContext();
+        MockViewGroup viewGroup =
+                new MockViewGroup(context, 0, 0, 200, 200, View.SCROLL_CAPTURE_HINT_EXCLUDE);
+        // With an already provided scroll capture callback
+        viewGroup.setScrollCaptureCallback(mMockCallback);
+
+        Rect localVisibleRect = new Rect(0, 0, 200, 200);
+        Point windowOffset = new Point();
+        LinkedList<ScrollCaptureTarget> targetList = new LinkedList<>();
+
+        // Dispatch to the ViewGroup itself
+        viewGroup.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targetList);
+
+        // Confirm that framework support was not requested, because this view is excluded.
+        // (And because this view has a callback set.)
+        viewGroup.assertCreateScrollCaptureCallbackInternalCount(0);
+
+        // Has callback, but hint=excluded, so excluded.
+        assertTrue(targetList.isEmpty());
+    }
+
+    /**
+     * Test scroll capture search dispatch to child views.
+     * <p>
+     * Verifies computation of child visible bounds.
+     * TODO: with scrollX / scrollY, split up into discrete tests
+     */
+    @MediumTest
+    @Test
+    public void testDispatchScrollCaptureSearch_toChildren() throws Exception {
+        final Context context = getInstrumentation().getContext();
+        final MockViewGroup viewGroup = new MockViewGroup(context, 0, 0, 200, 200);
+
+        Rect localVisibleRect = new Rect(25, 50, 175, 150);
+        Point windowOffset = new Point(0, 0);
+
+        //        visible area
+        //       |<- l=25,    |
+        //       |    r=175 ->|
+        // +--------------------------+
+        // | view1 (0, 0, 200, 25)    |
+        // +---------------+----------+
+        // |               |          |
+        // | view2         | view4    | --+
+        // | (0, 25,       |    (inv) |   | visible area
+        // |      150, 100)|          |   |
+        // +---------------+----------+   | t=50, b=150
+        // | view3         | view5    |   |
+        // | (0, 100       |(150, 100 | --+
+        // |     200, 200) | 200, 200)|
+        // |               |          |
+        // |               |          |
+        // +---------------+----------+ (200,200)
+
+        // View 1 is clipped and not visible.
+        final MockView view1 = new MockView(context, 0, 0, 200, 25);
+        viewGroup.addView(view1);
+
+        // View 2 is partially visible.
+        final MockView view2 = new MockView(context, 0, 25, 150, 100);
+        viewGroup.addView(view2);
+
+        // View 3 is partially visible.
+        // Pretend View3 can scroll by having framework provide fallback support
+        final MockView view3 = new MockView(context, 0, 100, 200, 200);
+        // When system internal scroll capture is requested for this view, return this callback.
+        view3.setScrollCaptureCallbackInternalForTest(mMockCallback);
+        viewGroup.addView(view3);
+
+        // View 4 is invisible and should be ignored.
+        final MockView view4 = new MockView(context, 150, 25, 200, 100, View.INVISIBLE);
+        viewGroup.addView(view4);
+
+        // View 4 is invisible and should be ignored.
+        final MockView view5 = new MockView(context, 150, 100, 200, 200);
+        // When system internal scroll capture is requested for this view, return this callback.
+        view5.setScrollCaptureCallback(mMockCallback2);
+        view5.setScrollCaptureHint(View.SCROLL_CAPTURE_HINT_INCLUDE);
+        viewGroup.addView(view5);
+
+        // Where targets are added
+        final LinkedList<ScrollCaptureTarget> targetList = new LinkedList<>();
+
+        // Dispatch to the ViewGroup
+        viewGroup.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targetList);
+
+        // View 1 is entirely clipped by the parent and not visible, dispatch
+        // skips this view entirely.
+        view1.assertDispatchScrollCaptureSearchCount(0);
+        view1.assertCreateScrollCaptureCallbackInternalCount(0);
+
+        // View 2, verify the computed localVisibleRect and windowOffset are correctly transformed
+        // to the child coordinate space
+        view2.assertDispatchScrollCaptureSearchCount(1);
+        view2.assertDispatchScrollCaptureSearchLastArgs(
+                new Rect(25, 25, 150, 75), new Point(0, 25));
+        // No callback set, so the framework is asked for support
+        view2.assertCreateScrollCaptureCallbackInternalCount(1);
+
+        // View 3, verify the computed localVisibleRect and windowOffset are correctly transformed
+        // to the child coordinate space
+        view3.assertDispatchScrollCaptureSearchCount(1);
+        view3.assertDispatchScrollCaptureSearchLastArgs(
+                new Rect(25, 0, 175, 50), new Point(0, 100));
+        // No callback set, so the framework is asked for support
+        view3.assertCreateScrollCaptureCallbackInternalCount(1);
+
+        // view4 is invisible, so it should be skipped entirely.
+        view4.assertDispatchScrollCaptureSearchCount(0);
+        view4.assertCreateScrollCaptureCallbackInternalCount(0);
+
+        // view5 is partially visible
+        view5.assertDispatchScrollCaptureSearchCount(1);
+        view5.assertDispatchScrollCaptureSearchLastArgs(
+                new Rect(0, 0, 25, 50), new Point(150, 100));
+        // view5 has a callback set on it, so internal framework support should not be consulted.
+        view5.assertCreateScrollCaptureCallbackInternalCount(0);
+
+        // 2 views should have been returned, view3 & view5
+        assertEquals(2, targetList.size());
+
+        ScrollCaptureTarget target = targetList.get(0);
+        assertSame("First target has the wrong View", view3, target.getContainingView());
+        assertSame("First target has the wrong callback", mMockCallback, target.getCallback());
+        assertEquals("First target hint is incorrect", View.SCROLL_CAPTURE_HINT_AUTO,
+                target.getContainingView().getScrollCaptureHint());
+
+        target = targetList.get(1);
+        assertSame("Second target has the wrong View", view5, target.getContainingView());
+        assertSame("Second target has the wrong callback", mMockCallback2, target.getCallback());
+        assertEquals("Second target hint is incorrect", View.SCROLL_CAPTURE_HINT_INCLUDE,
+                target.getContainingView().getScrollCaptureHint());
+    }
+
+    public static final class MockView extends View {
+        private ScrollCaptureCallback mInternalCallback;
+
+        private int mDispatchScrollCaptureSearchNumCalls;
+        private Rect mDispatchScrollCaptureSearchLastLocalVisibleRect;
+        private Point mDispatchScrollCaptureSearchLastWindowOffset;
+        private int mCreateScrollCaptureCallbackInternalCount;
+
+        MockView(Context context) {
+            this(context, /* left */ 0, /* top */0, /* right */ 0, /* bottom */0);
+        }
+
+        MockView(Context context, int left, int top, int right, int bottom) {
+            this(context, left, top, right, bottom, View.VISIBLE);
+        }
+
+        MockView(Context context, int left, int top, int right, int bottom, int visibility) {
+            super(context);
+            setVisibility(visibility);
+            setFrame(left, top, right, bottom);
+        }
+
+        public void setScrollCaptureCallbackInternalForTest(ScrollCaptureCallback internal) {
+            mInternalCallback = internal;
+        }
+
+        void assertDispatchScrollCaptureSearchCount(int count) {
+            assertEquals("Unexpected number of calls to dispatchScrollCaptureSearch",
+                    count, mDispatchScrollCaptureSearchNumCalls);
+        }
+
+        void assertDispatchScrollCaptureSearchLastArgs(Rect localVisibleRect, Point windowOffset) {
+            assertEquals("arg localVisibleRect was incorrect.",
+                    localVisibleRect, mDispatchScrollCaptureSearchLastLocalVisibleRect);
+            assertEquals("arg windowOffset was incorrect.",
+                    windowOffset, mDispatchScrollCaptureSearchLastWindowOffset);
+        }
+
+        void assertCreateScrollCaptureCallbackInternalCount(int count) {
+            assertEquals("Unexpected number of calls to createScrollCaptureCallackInternal",
+                    count, mCreateScrollCaptureCallbackInternalCount);
+        }
+
+        void reset() {
+            mDispatchScrollCaptureSearchNumCalls = 0;
+            mDispatchScrollCaptureSearchLastWindowOffset = null;
+            mDispatchScrollCaptureSearchLastLocalVisibleRect = null;
+            mCreateScrollCaptureCallbackInternalCount = 0;
+
+        }
+
+        @Override
+        public void dispatchScrollCaptureSearch(Rect localVisibleRect, Point windowOffset,
+                Queue<ScrollCaptureTarget> targets) {
+            mDispatchScrollCaptureSearchNumCalls++;
+            mDispatchScrollCaptureSearchLastLocalVisibleRect = new Rect(localVisibleRect);
+            mDispatchScrollCaptureSearchLastWindowOffset = new Point(windowOffset);
+            super.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targets);
+        }
+
+        @Override
+        @Nullable
+        public ScrollCaptureCallback createScrollCaptureCallbackInternal(Rect localVisibleRect,
+                Point offsetInWindow) {
+            mCreateScrollCaptureCallbackInternalCount++;
+            return mInternalCallback;
+        }
+    }
+
+    public static final class MockViewGroup extends ViewGroup {
+        private ScrollCaptureCallback mInternalCallback;
+        private int mDispatchScrollCaptureSearchNumCalls;
+        private Rect mDispatchScrollCaptureSearchLastLocalVisibleRect;
+        private Point mDispatchScrollCaptureSearchLastWindowOffset;
+        private int mCreateScrollCaptureCallbackInternalCount;
+
+
+        MockViewGroup(Context context) {
+            this(context, /* left */ 0, /* top */0, /* right */ 0, /* bottom */0);
+        }
+
+        MockViewGroup(Context context, int left, int top, int right, int bottom) {
+            this(context, left, top, right, bottom, View.SCROLL_CAPTURE_HINT_AUTO);
+        }
+
+        MockViewGroup(Context context, int left, int top, int right, int bottom,
+                int scrollCaptureHint) {
+            super(context);
+            setScrollCaptureHint(scrollCaptureHint);
+            setFrame(left, top, right, bottom);
+        }
+
+        public void setScrollCaptureCallbackInternalForTest(ScrollCaptureCallback internal) {
+            mInternalCallback = internal;
+        }
+
+        void assertDispatchScrollCaptureSearchCount(int count) {
+            assertEquals("Unexpected number of calls to dispatchScrollCaptureSearch",
+                    count, mDispatchScrollCaptureSearchNumCalls);
+        }
+
+        @Override
+        @Nullable
+        public ScrollCaptureCallback createScrollCaptureCallbackInternal(Rect localVisibleRect,
+                Point offsetInWindow) {
+            mCreateScrollCaptureCallbackInternalCount++;
+            return mInternalCallback;
+        }
+
+        @Override
+        protected void onLayout(boolean changed, int l, int t, int r, int b) {
+            // We don't layout this view.
+        }
+
+        void assertDispatchScrollCaptureCount(int count) {
+            assertEquals(count, mDispatchScrollCaptureSearchNumCalls);
+        }
+
+        void assertLastDispatchScrollCaptureArgs(Rect localVisibleRect, Point windowOffset) {
+            assertEquals("arg localVisibleRect to dispatchScrollCaptureCallback was incorrect.",
+                    localVisibleRect, mDispatchScrollCaptureSearchLastLocalVisibleRect);
+            assertEquals("arg windowOffset to dispatchScrollCaptureCallback was incorrect.",
+                    windowOffset, mDispatchScrollCaptureSearchLastWindowOffset);
+        }
+        void assertCreateScrollCaptureCallbackInternalCount(int count) {
+            assertEquals("Unexpected number of calls to createScrollCaptureCallackInternal",
+                    count, mCreateScrollCaptureCallbackInternalCount);
+        }
+
+        void reset() {
+            mDispatchScrollCaptureSearchNumCalls = 0;
+            mDispatchScrollCaptureSearchLastWindowOffset = null;
+            mDispatchScrollCaptureSearchLastLocalVisibleRect = null;
+            mCreateScrollCaptureCallbackInternalCount = 0;
+        }
+
+        @Override
+        public void dispatchScrollCaptureSearch(Rect localVisibleRect, Point windowOffset,
+                Queue<ScrollCaptureTarget> targets) {
+            mDispatchScrollCaptureSearchNumCalls++;
+            mDispatchScrollCaptureSearchLastLocalVisibleRect = new Rect(localVisibleRect);
+            mDispatchScrollCaptureSearchLastWindowOffset = new Point(windowOffset);
+            super.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targets);
+        }
+    }
+}
diff --git a/core/tests/coretests/src/com/android/internal/app/IntentForwarderActivityTest.java b/core/tests/coretests/src/com/android/internal/app/IntentForwarderActivityTest.java
index 5424b6f..43590ba 100644
--- a/core/tests/coretests/src/com/android/internal/app/IntentForwarderActivityTest.java
+++ b/core/tests/coretests/src/com/android/internal/app/IntentForwarderActivityTest.java
@@ -68,6 +68,8 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
 
 @RunWith(AndroidJUnit4.class)
 public class IntentForwarderActivityTest {
@@ -633,6 +635,11 @@
         public void onCreate(@Nullable Bundle savedInstanceState) {
             getIntent().setComponent(sComponentName);
             super.onCreate(savedInstanceState);
+            try {
+                mExecutorService.awaitTermination(/* timeout= */ 30, TimeUnit.SECONDS);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
         }
 
         @Override
@@ -671,7 +678,8 @@
         }
 
         @Override
-        public ResolveInfo resolveActivityAsUser(Intent intent, int flags, int userId) {
+        public CompletableFuture<ResolveInfo> resolveActivityAsUser(
+                Intent intent, int flags, int userId) {
             ActivityInfo activityInfo = new ActivityInfo();
             activityInfo.packageName = sPackageName;
             activityInfo.name = sActivityName;
@@ -680,7 +688,7 @@
             ResolveInfo resolveInfo = new ResolveInfo();
             resolveInfo.activityInfo = activityInfo;
 
-            return resolveInfo;
+            return CompletableFuture.completedFuture(resolveInfo);
         }
 
         @Override
diff --git a/core/tests/coretests/src/com/android/internal/view/ScrollViewCaptureHelperTest.java b/core/tests/coretests/src/com/android/internal/view/ScrollViewCaptureHelperTest.java
new file mode 100644
index 0000000..63a68e9
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/view/ScrollViewCaptureHelperTest.java
@@ -0,0 +1,352 @@
+/*
+ * 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.internal.view;
+
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+
+import static androidx.test.InstrumentationRegistry.getContext;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.graphics.Color;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.ScrollView;
+import android.widget.TextView;
+
+import androidx.test.annotation.UiThreadTest;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.util.Random;
+
+public class ScrollViewCaptureHelperTest {
+
+    private FrameLayout mParent;
+    private ScrollView mTarget;
+    private LinearLayout mContent;
+    private WindowManager mWm;
+
+    private WindowManager.LayoutParams mWindowLayoutParams;
+
+    private static final int CHILD_VIEWS = 12;
+    public static final int CHILD_VIEW_HEIGHT = 300;
+
+    private static final int WINDOW_WIDTH = 800;
+    private static final int WINDOW_HEIGHT = 1200;
+
+    private static final int CAPTURE_HEIGHT = 600;
+
+    private Random mRandom;
+
+    private static float sDensity;
+
+    @BeforeClass
+    public static void setUpClass() {
+        sDensity = getContext().getResources().getDisplayMetrics().density;
+    }
+
+    @Before
+    @UiThreadTest
+    public void setUp() {
+        mRandom = new Random();
+        mParent = new FrameLayout(getContext());
+
+        mTarget = new ScrollView(getContext());
+        mParent.addView(mTarget, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
+
+        mContent = new LinearLayout(getContext());
+        mContent.setOrientation(LinearLayout.VERTICAL);
+        mTarget.addView(mContent, new ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT));
+
+        for (int i = 0; i < CHILD_VIEWS; i++) {
+            TextView view = new TextView(getContext());
+            view.setText("Child #" + i);
+            view.setTextColor(Color.WHITE);
+            view.setTextSize(30f);
+            view.setBackgroundColor(Color.rgb(mRandom.nextFloat(), mRandom.nextFloat(),
+                    mRandom.nextFloat()));
+            mContent.addView(view, new ViewGroup.LayoutParams(MATCH_PARENT, CHILD_VIEW_HEIGHT));
+        }
+
+        // Window -> Parent -> Target -> Content
+
+        mWm = getContext().getSystemService(WindowManager.class);
+
+        // Setup the window that we are going to use
+        mWindowLayoutParams = new WindowManager.LayoutParams(WINDOW_WIDTH, WINDOW_HEIGHT,
+                TYPE_APPLICATION_OVERLAY, FLAG_NOT_TOUCHABLE, PixelFormat.OPAQUE);
+        mWindowLayoutParams.setTitle("ScrollViewCaptureHelper");
+        mWindowLayoutParams.gravity = Gravity.CENTER;
+        mWm.addView(mParent, mWindowLayoutParams);
+    }
+
+    @After
+    @UiThreadTest
+    public void tearDown() {
+        mWm.removeViewImmediate(mParent);
+    }
+
+    @Test
+    @UiThreadTest
+    public void onPrepareForStart() {
+        ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper();
+        Rect scrollBounds = svc.onComputeScrollBounds(mTarget);
+        svc.onPrepareForStart(mTarget, scrollBounds);
+    }
+
+    static void assertEmpty(Rect r) {
+        if (r != null && !r.isEmpty()) {
+            fail("Not true that " + r + " is empty");
+        }
+    }
+
+    static void assertContains(Rect parent, Rect child) {
+        if (!parent.contains(child)) {
+            fail("Not true that " + parent + " contains " + child);
+        }
+    }
+
+    static void assertRectEquals(Rect parent, Rect child) {
+        if (!parent.equals(child)) {
+            fail("Not true that " + parent + " is equal to " + child);
+        }
+    }
+
+    static Rect getVisibleRect(View v) {
+        Rect r = new Rect(0, 0, v.getWidth(), v.getHeight());
+        v.getLocalVisibleRect(r);
+        return r;
+    }
+
+
+    static int assertScrollToY(View v, int scrollY) {
+        v.scrollTo(0, scrollY);
+        int dest = v.getScrollY();
+        assertEquals(scrollY, dest);
+        return scrollY;
+    }
+
+
+    static void assertCapturedAreaCompletelyVisible(int startScrollY, Rect requestRect,
+            Rect localVisibleNow) {
+        Rect captured = new Rect(localVisibleNow);
+        captured.offset(0, -startScrollY); // make relative
+
+        if (!captured.contains(requestRect)) {
+            fail("Not true that all of " + requestRect + " is contained by " + captured);
+        }
+    }
+    static void assertCapturedAreaPartiallyVisible(int startScrollY, Rect requestRect,
+            Rect localVisibleNow) {
+        Rect captured = new Rect(localVisibleNow);
+        captured.offset(0, -startScrollY); // make relative
+
+        if (!Rect.intersects(captured, requestRect)) {
+            fail("Not true that any of " + requestRect + " intersects " + captured);
+        }
+    }
+
+    @Test
+    @UiThreadTest
+    public void onScrollRequested_up_fromTop() {
+        final int startScrollY = assertScrollToY(mTarget, 0);
+
+        ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper();
+        Rect scrollBounds = svc.onComputeScrollBounds(mTarget);
+        svc.onPrepareForStart(mTarget, scrollBounds);
+
+        assertTrue(scrollBounds.height() > CAPTURE_HEIGHT);
+
+        Rect request = new Rect(0, -CAPTURE_HEIGHT, scrollBounds.width(), 0);
+
+        Rect result = svc.onScrollRequested(mTarget, scrollBounds, request);
+
+        // The result is an empty rectangle and no scrolling, since it
+        // is not possible to physically scroll further up to make the
+        // requested area visible at all (it doesn't exist).
+        assertEmpty(result);
+    }
+
+    @Test
+    @UiThreadTest
+    public void onScrollRequested_down_fromTop() {
+        final int startScrollY = assertScrollToY(mTarget, 0);
+
+
+        ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper();
+        Rect scrollBounds = svc.onComputeScrollBounds(mTarget);
+        svc.onPrepareForStart(mTarget, scrollBounds);
+
+        assertTrue(scrollBounds.height() > CAPTURE_HEIGHT);
+
+        // Capture between y = +1200 to +1500 pixels BELOW current top
+        Rect request = new Rect(0, WINDOW_HEIGHT, scrollBounds.width(),
+                WINDOW_HEIGHT + CAPTURE_HEIGHT);
+
+        Rect result = svc.onScrollRequested(mTarget, scrollBounds, request);
+        assertRectEquals(request, result);
+
+        assertCapturedAreaCompletelyVisible(startScrollY, request, getVisibleRect(mContent));
+    }
+
+
+    @Test
+    @UiThreadTest
+    public void onScrollRequested_up_fromMiddle() {
+        final int startScrollY = assertScrollToY(mTarget, WINDOW_HEIGHT);
+
+        ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper();
+        Rect scrollBounds = svc.onComputeScrollBounds(mTarget);
+        svc.onPrepareForStart(mTarget, scrollBounds);
+
+        Rect request = new Rect(0, -CAPTURE_HEIGHT, scrollBounds.width(), 0);
+
+
+        Rect result = svc.onScrollRequested(mTarget, scrollBounds, request);
+
+        assertRectEquals(request, result);
+
+        assertCapturedAreaCompletelyVisible(startScrollY, request, getVisibleRect(mContent));
+    }
+
+    @Test
+    @UiThreadTest
+    public void onScrollRequested_down_fromMiddle() {
+        final int startScrollY = assertScrollToY(mTarget, WINDOW_HEIGHT);
+
+        ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper();
+        Rect scrollBounds = svc.onComputeScrollBounds(mTarget);
+        svc.onPrepareForStart(mTarget, scrollBounds);
+
+        Rect request = new Rect(0, WINDOW_HEIGHT, scrollBounds.width(),
+                WINDOW_HEIGHT + CAPTURE_HEIGHT);
+
+        Rect result = svc.onScrollRequested(mTarget, scrollBounds, request);
+        assertRectEquals(request, result);
+
+        assertCapturedAreaCompletelyVisible(startScrollY, request, getVisibleRect(mContent));
+    }
+
+    @Test
+    @UiThreadTest
+    public void onScrollRequested_up_fromBottom() {
+        final int startScrollY = assertScrollToY(mTarget, WINDOW_HEIGHT * 2);
+
+        ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper();
+        Rect scrollBounds = svc.onComputeScrollBounds(mTarget);
+        svc.onPrepareForStart(mTarget, scrollBounds);
+
+        Rect request = new Rect(0, -CAPTURE_HEIGHT, scrollBounds.width(), 0);
+
+        Rect result = svc.onScrollRequested(mTarget, scrollBounds, request);
+        assertRectEquals(request, result);
+
+        assertCapturedAreaCompletelyVisible(startScrollY, request, getVisibleRect(mContent));
+    }
+
+    @Test
+    @UiThreadTest
+    public void onScrollRequested_down_fromBottom() {
+        final int startScrollY = assertScrollToY(mTarget, WINDOW_HEIGHT * 2);
+
+        ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper();
+        Rect scrollBounds = svc.onComputeScrollBounds(mTarget);
+        svc.onPrepareForStart(mTarget, scrollBounds);
+
+        Rect request = new Rect(0, WINDOW_HEIGHT, scrollBounds.width(),
+                WINDOW_HEIGHT + CAPTURE_HEIGHT);
+
+        Rect result = svc.onScrollRequested(mTarget, scrollBounds, request);
+
+        // The result is an empty rectangle and no scrolling, since it
+        // is not possible to physically scroll further down to make the
+        // requested area visible at all (it doesn't exist).
+        assertEmpty(result);
+    }
+
+    @Test
+    @UiThreadTest
+    public void onScrollRequested_offTopEdge() {
+        final int startScrollY = assertScrollToY(mTarget, 0);
+
+        ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper();
+        Rect scrollBounds = svc.onComputeScrollBounds(mTarget);
+        svc.onPrepareForStart(mTarget, scrollBounds);
+
+        // Create a request which lands halfway off the top of the content
+        //from -1500 to -900, (starting at 1200 = -300 to +300 within the content)
+        int top = 0;
+        Rect request = new Rect(
+                0, top - (CAPTURE_HEIGHT / 2),
+                scrollBounds.width(), top + (CAPTURE_HEIGHT / 2));
+
+        Rect result = svc.onScrollRequested(mTarget, scrollBounds, request);
+        // The result is a partial result
+        Rect expectedResult = new Rect(request);
+        expectedResult.top += 300; // top half clipped
+        assertRectEquals(expectedResult, result);
+        assertCapturedAreaPartiallyVisible(startScrollY, request, getVisibleRect(mContent));
+    }
+
+    @Test
+    @UiThreadTest
+    public void onScrollRequested_offBottomEdge() {
+        final int startScrollY = assertScrollToY(mTarget, WINDOW_HEIGHT * 2); // 2400
+
+        ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper();
+        Rect scrollBounds = svc.onComputeScrollBounds(mTarget);
+        svc.onPrepareForStart(mTarget, scrollBounds);
+
+        // Create a request which lands halfway off the bottom of the content
+        //from 600 to to 1200, (starting at 2400 = 3000 to  3600 within the content)
+
+        int bottom = WINDOW_HEIGHT;
+        Rect request = new Rect(
+                0, bottom - (CAPTURE_HEIGHT / 2),
+                scrollBounds.width(), bottom + (CAPTURE_HEIGHT / 2));
+
+        Rect result = svc.onScrollRequested(mTarget, scrollBounds, request);
+
+        Rect expectedResult = new Rect(request);
+        expectedResult.bottom -= 300; // bottom half clipped
+        assertRectEquals(expectedResult, result);
+        assertCapturedAreaPartiallyVisible(startScrollY, request, getVisibleRect(mContent));
+
+    }
+
+    @Test
+    @UiThreadTest
+    public void onPrepareForEnd() {
+        ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper();
+        svc.onPrepareForEnd(mTarget);
+    }
+}
diff --git a/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java
index cd6b3af..fe33cd8 100644
--- a/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java
+++ b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java
@@ -36,6 +36,7 @@
 import android.graphics.Rect;
 import android.os.Handler;
 import android.os.Looper;
+import android.view.WindowManager;
 
 import androidx.test.runner.AndroidJUnit4;
 
@@ -91,7 +92,8 @@
     public void testProvidedImageScreenshot() {
         mScreenshotHelper.provideScreenshot(
                 Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888), new Rect(),
-                Insets.of(0, 0, 0, 0), 1, mHandler, null);
+                Insets.of(0, 0, 0, 0), 1,
+                WindowManager.ScreenshotSource.SCREENSHOT_OTHER, mHandler, null);
     }
 
     @Test
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index 18086ec..c5ac451 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -283,6 +283,12 @@
       "group": "WM_DEBUG_APP_TRANSITIONS",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
+    "-1517908912": {
+      "message": "requestScrollCapture: caught exception dispatching to window.token=%s",
+      "level": "WARN",
+      "group": "WM_ERROR",
+      "at": "com\/android\/server\/wm\/WindowManagerService.java"
+    },
     "-1515151503": {
       "message": ">>> OPEN TRANSACTION removeReplacedWindows",
       "level": "INFO",
@@ -1441,6 +1447,12 @@
       "group": "WM_DEBUG_RECENTS_ANIMATIONS",
       "at": "com\/android\/server\/wm\/RecentsAnimation.java"
     },
+    "646981048": {
+      "message": "Invalid displayId for requestScrollCapture: %d",
+      "level": "ERROR",
+      "group": "WM_ERROR",
+      "at": "com\/android\/server\/wm\/WindowManagerService.java"
+    },
     "662572728": {
       "message": "Attempted to add a toast window with bad token %s.  Aborting.",
       "level": "WARN",
@@ -1597,6 +1609,12 @@
       "group": "WM_ERROR",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
+    "1046922686": {
+      "message": "requestScrollCapture: caught exception dispatching callback: %s",
+      "level": "WARN",
+      "group": "WM_ERROR",
+      "at": "com\/android\/server\/wm\/WindowManagerService.java"
+    },
     "1051545910": {
       "message": "Exit animation finished in %s: remove=%b",
       "level": "VERBOSE",
diff --git a/graphics/java/android/graphics/SurfaceTexture.java b/graphics/java/android/graphics/SurfaceTexture.java
index cd878c5..228d03a 100644
--- a/graphics/java/android/graphics/SurfaceTexture.java
+++ b/graphics/java/android/graphics/SurfaceTexture.java
@@ -120,7 +120,7 @@
 
     /**
      * Construct a new SurfaceTexture to stream images to a given OpenGL texture.
-     *
+     * <p>
      * In single buffered mode the application is responsible for serializing access to the image
      * content buffer. Each time the image content is to be updated, the
      * {@link #releaseTexImage()} method must be called before the image content producer takes
@@ -143,7 +143,7 @@
 
     /**
      * Construct a new SurfaceTexture to stream images to a given OpenGL texture.
-     *
+     * <p>
      * In single buffered mode the application is responsible for serializing access to the image
      * content buffer. Each time the image content is to be updated, the
      * {@link #releaseTexImage()} method must be called before the image content producer takes
@@ -152,7 +152,7 @@
      * must be called before each ANativeWindow_lock, or that call will fail. When producing
      * image content with OpenGL ES, {@link #releaseTexImage()} must be called before the first
      * OpenGL ES function call each frame.
-     *
+     * <p>
      * Unlike {@link #SurfaceTexture(int, boolean)}, which takes an OpenGL texture object name,
      * this constructor creates the SurfaceTexture in detached mode. A texture name must be passed
      * in using {@link #attachToGLContext} before calling {@link #releaseTexImage()} and producing
@@ -222,15 +222,15 @@
      * method.  Both video and camera based image producers do override the size.  This method may
      * be used to set the image size when producing images with {@link android.graphics.Canvas} (via
      * {@link android.view.Surface#lockCanvas}), or OpenGL ES (via an EGLSurface).
-     *
+     * <p>
      * The new default buffer size will take effect the next time the image producer requests a
      * buffer to fill.  For {@link android.graphics.Canvas} this will be the next time {@link
      * android.view.Surface#lockCanvas} is called.  For OpenGL ES, the EGLSurface should be
      * destroyed (via eglDestroySurface), made not-current (via eglMakeCurrent), and then recreated
-     * (via eglCreateWindowSurface) to ensure that the new default size has taken effect.
-     *
+     * (via {@code eglCreateWindowSurface}) to ensure that the new default size has taken effect.
+     * <p>
      * The width and height parameters must be no greater than the minimum of
-     * GL_MAX_VIEWPORT_DIMS and GL_MAX_TEXTURE_SIZE (see
+     * {@code GL_MAX_VIEWPORT_DIMS} and {@code GL_MAX_TEXTURE_SIZE} (see
      * {@link javax.microedition.khronos.opengles.GL10#glGetIntegerv glGetIntegerv}).
      * An error due to invalid dimensions might not be reported until
      * updateTexImage() is called.
@@ -242,7 +242,7 @@
     /**
      * Update the texture image to the most recent frame from the image stream.  This may only be
      * called while the OpenGL ES context that owns the texture is current on the calling thread.
-     * It will implicitly bind its texture to the GL_TEXTURE_EXTERNAL_OES texture target.
+     * It will implicitly bind its texture to the {@code GL_TEXTURE_EXTERNAL_OES} texture target.
      */
     public void updateTexImage() {
         nativeUpdateTexImage();
@@ -251,6 +251,7 @@
     /**
      * Releases the the texture content. This is needed in single buffered mode to allow the image
      * content producer to take ownership of the image buffer.
+     * <p>
      * For more information see {@link #SurfaceTexture(int, boolean)}.
      */
     public void releaseTexImage() {
@@ -263,7 +264,7 @@
      * ES texture object will be deleted as a result of this call.  After calling this method all
      * calls to {@link #updateTexImage} will throw an {@link java.lang.IllegalStateException} until
      * a successful call to {@link #attachToGLContext} is made.
-     *
+     * <p>
      * This can be used to access the SurfaceTexture image contents from multiple OpenGL ES
      * contexts.  Note, however, that the image contents are only accessible from one OpenGL ES
      * context at a time.
@@ -279,8 +280,8 @@
      * Attach the SurfaceTexture to the OpenGL ES context that is current on the calling thread.  A
      * new OpenGL ES texture object is created and populated with the SurfaceTexture image frame
      * that was current at the time of the last call to {@link #detachFromGLContext}.  This new
-     * texture is bound to the GL_TEXTURE_EXTERNAL_OES texture target.
-     *
+     * texture is bound to the {@code GL_TEXTURE_EXTERNAL_OES} texture target.
+     * <p>
      * This can be used to access the SurfaceTexture image contents from multiple OpenGL ES
      * contexts.  Note, however, that the image contents are only accessible from one OpenGL ES
      * context at a time.
@@ -297,16 +298,16 @@
 
     /**
      * Retrieve the 4x4 texture coordinate transform matrix associated with the texture image set by
-     * the most recent call to updateTexImage.
-     *
+     * the most recent call to {@link #updateTexImage}.
+     * <p>
      * This transform matrix maps 2D homogeneous texture coordinates of the form (s, t, 0, 1) with s
      * and t in the inclusive range [0, 1] to the texture coordinate that should be used to sample
      * that location from the texture.  Sampling the texture outside of the range of this transform
      * is undefined.
-     *
+     * <p>
      * The matrix is stored in column-major order so that it may be passed directly to OpenGL ES via
-     * the glLoadMatrixf or glUniformMatrix4fv functions.
-     *
+     * the {@code glLoadMatrixf} or {@code glUniformMatrix4fv} functions.
+     * <p>
      * If the underlying buffer has a crop associated with it, the transformation will also include
      * a slight scale to cut off a 1-texel border around the edge of the crop. This ensures that
      * when the texture is bilinear sampled that no texels outside of the buffer's valid region
@@ -326,7 +327,7 @@
 
     /**
      * Retrieve the timestamp associated with the texture image set by the most recent call to
-     * updateTexImage.
+     * {@link #updateTexImage}.
      *
      * <p>This timestamp is in nanoseconds, and is normally monotonically increasing. The timestamp
      * should be unaffected by time-of-day adjustments. The specific meaning and zero point of the
@@ -337,8 +338,8 @@
      *
      * <p>For camera sources, timestamps should be strictly monotonic. Timestamps from MediaPlayer
      * sources may be reset when the playback position is set. For EGL and Vulkan producers, the
-     * timestamp is the desired present time set with the EGL_ANDROID_presentation_time or
-     * VK_GOOGLE_display_timing extensions.</p>
+     * timestamp is the desired present time set with the {@code EGL_ANDROID_presentation_time} or
+     * {@code VK_GOOGLE_display_timing} extensions.</p>
      */
 
     public long getTimestamp() {
@@ -346,16 +347,17 @@
     }
 
     /**
-     * release() frees all the buffers and puts the SurfaceTexture into the
+     * {@code release()} frees all the buffers and puts the SurfaceTexture into the
      * 'abandoned' state. Once put in this state the SurfaceTexture can never
      * leave it. When in the 'abandoned' state, all methods of the
-     * IGraphicBufferProducer interface will fail with the NO_INIT error.
-     *
+     * {@code IGraphicBufferProducer} interface will fail with the {@code NO_INIT}
+     * error.
+     * <p>
      * Note that while calling this method causes all the buffers to be freed
      * from the perspective of the the SurfaceTexture, if there are additional
      * references on the buffers (e.g. if a buffer is referenced by a client or
      * by OpenGL ES as a texture) then those buffer will remain allocated.
-     *
+     * <p>
      * Always call this method when you are done with SurfaceTexture. Failing
      * to do so may delay resource deallocation for a significant amount of
      * time.
@@ -367,7 +369,7 @@
     }
 
     /**
-     * Returns true if the SurfaceTexture was released.
+     * Returns {@code true} if the SurfaceTexture was released.
      *
      * @see #release()
      */
@@ -400,7 +402,7 @@
     }
 
     /**
-     * Returns true if the SurfaceTexture is single-buffered
+     * Returns {@code true} if the SurfaceTexture is single-buffered.
      * @hide
      */
     public boolean isSingleBuffered() {
diff --git a/identity/java/android/security/identity/IdentityCredential.java b/identity/java/android/security/identity/IdentityCredential.java
index 1db2f63..b351b3d 100644
--- a/identity/java/android/security/identity/IdentityCredential.java
+++ b/identity/java/android/security/identity/IdentityCredential.java
@@ -95,9 +95,7 @@
     /**
      * Sets whether to allow using an authentication key which use count has been exceeded if no
      * other key is available. This must be called prior to calling
-     * {@link #getEntries(byte[], Map, byte[], byte[])} or using a
-     * {@link android.hardware.biometrics.BiometricPrompt.CryptoObject} which references this
-     * object.
+     * {@link #getEntries(byte[], Map, byte[], byte[])}.
      *
      * By default this is set to true.
      *
@@ -123,13 +121,14 @@
      * entries.
      *
      * <p>It is the responsibility of the calling application to know if authentication is needed
-     * and use e.g. {@link android.hardware.biometrics.BiometricPrompt}) to make the user
+     * and use e.g. {@link android.hardware.biometrics.BiometricPrompt} to make the user
      * authenticate using a {@link android.hardware.biometrics.BiometricPrompt.CryptoObject} which
      * references this object. If needed, this must be done before calling
      * {@link #getEntries(byte[], Map, byte[], byte[])}.
      *
-     * <p>If this method returns successfully (i.e. without throwing an exception), it must not be
-     * called again on this instance.
+     * <p>It is permissible to call this method multiple times using the same instance but if this
+     * is done, the {@code sessionTranscript} parameter must be identical for each call. If this is
+     * not the case, the {@link SessionTranscriptMismatchException} exception is thrown.
      *
      * <p>If not {@code null} the {@code requestMessage} parameter must contain data for the request
      * from the verifier. The content can be defined in the way appropriate for the credential, byt
@@ -141,6 +140,9 @@
      *     the example below.</li>
      * </ul>
      *
+     * <p>If these requirements are not met the {@link InvalidRequestMessageException} exception
+     * is thrown.
+     *
      * <p>Here's an example of CBOR which conforms to this requirement:
      * <pre>
      *   ItemsRequest = {
@@ -149,6 +151,8 @@
      *     ? "RequestInfo" : {* tstr => any} ; Additional info the reader wants to provide
      *   }
      *
+     *   DocType = tstr
+     *
      *   NameSpaces = {
      *     + NameSpace => DataElements    ; Requested data elements for each NameSpace
      *   }
@@ -172,16 +176,18 @@
      *     EReaderKeyBytes
      *   ]
      *
-     *   DeviceEngagementBytes = #6.24(bstr .cbor DeviceEngagement)
-     *   EReaderKeyBytes = #6.24(bstr .cbor EReaderKey.Pub)
+     *   DeviceEngagementBytes = #6.24(bstr .cbor DeviceEngagement)  ; Bytes of DeviceEngagement
+     *   EReaderKeyBytes = #6.24(bstr .cbor EReaderKey.Pub)  ; Bytes of EReaderKey.pub
+     *
+     *   EReaderKey.Pub = COSE_Key    ; Ephemeral public key provided by reader
      * </pre>
      *
-     * <p>If the SessionTranscript is not empty, a COSE_Key structure for the public part
-     * of the key-pair previously generated by {@link #createEphemeralKeyPair()} must appear
-     * somewhere in {@code DeviceEngagement} and the X and Y coordinates must both be present
+     * <p>where a {@code COSE_Key} structure for the public part of the key-pair previously
+     * generated by {@link #createEphemeralKeyPair()} must appear somewhere in
+     * {@code DeviceEngagement} and the X and Y coordinates must both be present
      * in uncompressed form.
      *
-     * <p>If {@code readerAuth} is not {@code null} it must be the bytes of a COSE_Sign1
+     * <p>If {@code readerAuth} is not {@code null} it must be the bytes of a {@code COSE_Sign1}
      * structure as defined in RFC 8152. For the payload nil shall be used and the
      * detached payload is the ReaderAuthentication CBOR described below.
      * <pre>
@@ -194,20 +200,23 @@
      *     ItemsRequestBytes = #6.24(bstr .cbor ItemsRequest)   ; Bytes of ItemsRequest
      * </pre>
      *
-     * <p>The public key corresponding to the key used to made signature, can be
-     * found in the {@code x5chain} unprotected header element of the COSE_Sign1
-     * structure (as as described in 'draft-ietf-cose-x509-04'). There will be at
-     * least one certificate in said element and there may be more (and if so,
+     * <p>where {@code ItemsRequestBytes} are the bytes in the {@code requestMessage} parameter.
+     *
+     * <p>The public key corresponding to the key used to make the signature, can be found in the
+     * {@code x5chain} unprotected header element of the {@code COSE_Sign1} structure (as as
+     * described in
+     * <a href="https://tools.ietf.org/html/draft-ietf-cose-x509-04">draft-ietf-cose-x509-04</a>).
+     * There will be at least one certificate in said element and there may be more (and if so,
      * each certificate must be signed by its successor).
      *
-     * <p>Data elements protected by reader authentication is returned if, and only if, they are
+     * <p>Data elements protected by reader authentication are returned if, and only if, they are
      * mentioned in {@code requestMessage}, {@code requestMessage} is signed by the top-most
-     * certificate in {@code readerCertificateChain}, and the data element is configured
-     * with an {@link AccessControlProfile} with a {@link X509Certificate} in
-     * {@code readerCertificateChain}.
+     * certificate in the reader's certificate chain, and the data element is configured
+     * with an {@link AccessControlProfile} configured with an X.509 certificate which appears
+     * in the certificate chain.
      *
      * <p>Note that only items referenced in {@code entriesToRequest} are returned - the
-     * {@code requestMessage} parameter is only used to for enforcing reader authentication.
+     * {@code requestMessage} parameter is used only for enforcing reader authentication.
      *
      * <p>The reason for having {@code requestMessage} and {@code entriesToRequest} as separate
      * parameters is that the former represents a request from the remote verifier device
@@ -219,13 +228,12 @@
      * @param entriesToRequest       The entries to request, organized as a map of namespace
      *                               names with each value being a collection of data elements
      *                               in the given namespace.
-     * @param readerSignature        COSE_Sign1 structure as described above or {@code null}
-     *                               if reader authentication is not being used.
+     * @param readerSignature        A {@code COSE_Sign1} structure as described above or
+     *                               {@code null} if reader authentication is not being used.
      * @return A {@link ResultData} object containing entry data organized by namespace and a
      *         cryptographically authenticated representation of the same data.
      * @throws SessionTranscriptMismatchException     Thrown when trying use multiple different
-     *                                                session transcripts in the same presentation
-     *                                                session.
+     *                                                session transcripts.
      * @throws NoAuthenticationKeyAvailableException  if authentication keys were never
      *                                                provisioned, the method
      *                                             {@link #setAvailableAuthenticationKeys(int, int)}
@@ -255,8 +263,8 @@
      * Sets the number of dynamic authentication keys the {@code IdentityCredential} will maintain,
      * and the number of times each should be used.
      *
-     * <p>{@code IdentityCredential}s will select the least-used dynamic authentication key each
-     * time {@link #getEntries(byte[], Map, byte[], byte[])} is called. {@code IdentityCredential}s
+     * <p>The Identity Credential system will select the least-used dynamic authentication key each
+     * time {@link #getEntries(byte[], Map, byte[], byte[])} is called. Identity Credentials
      * for which this method has not been called behave as though it had been called wit
      * {@code keyCount} 0 and {@code maxUsesPerKey} 1.
      *
@@ -274,9 +282,10 @@
      * <p>When there aren't enough certified dynamic authentication keys, either because the key
      * count has been increased or because one or more keys have reached their usage count, this
      * method will generate replacement keys and certificates and return them for issuer
-     * certification. The issuer certificates and associated static authentication data must then
-     * be provided back to the {@code IdentityCredential} using
-     * {@link #storeStaticAuthenticationData(X509Certificate, byte[])}.
+     * certification.  The issuer certificates and associated static authentication data must then
+     * be provided back to the Identity Credential using
+     * {@link #storeStaticAuthenticationData(X509Certificate, byte[])}.  The private part of
+     * each authentication key never leaves secure hardware.
      *
      * <p>Each X.509 certificate is signed by CredentialKey. The certificate chain for CredentialKey
      * can be obtained using the {@link #getCredentialKeyCertificateChain()} method.
diff --git a/identity/java/android/security/identity/IdentityCredentialStore.java b/identity/java/android/security/identity/IdentityCredentialStore.java
index a1dfc77..4f834d2 100644
--- a/identity/java/android/security/identity/IdentityCredentialStore.java
+++ b/identity/java/android/security/identity/IdentityCredentialStore.java
@@ -78,17 +78,21 @@
 
     /**
      * Specifies that the cipher suite that will be used to secure communications between the reader
-     * is:
+     * and the prover is using the following primitives
      *
      * <ul>
-     * <li>ECDHE with HKDF-SHA-256 for key agreement.</li>
-     * <li>AES-256 with GCM block mode for authenticated encryption (nonces are incremented by one
-     * for every message).</li>
-     * <li>ECDSA with SHA-256 for signing (used for signing session transcripts to defeat
-     * man-in-the-middle attacks), signing keys are not ephemeral. See {@link IdentityCredential}
-     * for details on reader and prover signing keys.</li>
+     * <li>ECKA-DH (Elliptic Curve Key Agreement Algorithm - Diffie-Hellman, see BSI TR-03111).</li>
+     *
+     * <li>HKDF-SHA-256 (see RFC 5869).</li>
+     *
+     * <li>AES-256-GCM (see NIST SP 800-38D).</li>
+     *
+     * <li>HMAC-SHA-256 (see RFC 2104).</li>
      * </ul>
      *
+     * <p>The exact way these primitives are combined to derive the session key is specified in
+     * section 9.2.1.4 of ISO/IEC 18013-5 (see description of cipher suite '1').<p>
+     *
      * <p>
      * At present this is the only supported cipher suite.
      */
@@ -135,9 +139,20 @@
     /**
      * Creates a new credential.
      *
+     * <p>When a credential is created, a cryptographic key-pair - CredentialKey - is created which
+     * is used to authenticate the store to the Issuing Authority.  The private part of this
+     * key-pair never leaves secure hardware and the public part can be obtained using
+     * {@link WritableIdentityCredential#getCredentialKeyCertificateChain(byte[])} on the
+     * returned object.
+     *
+     * <p>In addition, all of the Credential data content is imported and a certificate for the
+     * CredentialKey and a signature produced with the CredentialKey are created.  These latter
+     * values may be checked by an issuing authority to verify that the data was imported into
+     * secure hardware and that it was imported unmodified.
+     *
      * @param credentialName The name used to identify the credential.
      * @param docType        The document type for the credential.
-     * @return A @{link WritableIdentityCredential} that can be used to create a new credential.
+     * @return A {@link WritableIdentityCredential} that can be used to create a new credential.
      * @throws AlreadyPersonalizedException if a credential with the given name already exists.
      * @throws DocTypeNotSupportedException if the given document type isn't supported by the store.
      */
@@ -148,6 +163,10 @@
     /**
      * Retrieve a named credential.
      *
+     * <p>The cipher suite used to communicate with the remote verifier must also be specified.
+     * Currently only a single cipher-suite is supported. Support for other cipher suites may be
+     * added in a future version of this API.
+     *
      * @param credentialName the name of the credential to retrieve.
      * @param cipherSuite    the cipher suite to use for communicating with the verifier.
      * @return The named credential, or null if not found.
diff --git a/identity/java/android/security/identity/ResultData.java b/identity/java/android/security/identity/ResultData.java
index 13552d6..37de2c4 100644
--- a/identity/java/android/security/identity/ResultData.java
+++ b/identity/java/android/security/identity/ResultData.java
@@ -34,23 +34,23 @@
     /** Value was successfully retrieved. */
     public static final int STATUS_OK = 0;
 
-    /** Requested entry does not exist. */
+    /** The entry does not exist. */
     public static final int STATUS_NO_SUCH_ENTRY = 1;
 
-    /** Requested entry was not requested. */
+    /** The entry was not requested. */
     public static final int STATUS_NOT_REQUESTED = 2;
 
-    /** Requested entry wasn't in the request message. */
+    /** The entry wasn't in the request message. */
     public static final int STATUS_NOT_IN_REQUEST_MESSAGE = 3;
 
-    /** The requested entry was not retrieved because user authentication wasn't performed. */
+    /** The entry was not retrieved because user authentication failed. */
     public static final int STATUS_USER_AUTHENTICATION_FAILED = 4;
 
-    /** The requested entry was not retrieved because reader authentication wasn't performed. */
+    /** The entry was not retrieved because reader authentication failed. */
     public static final int STATUS_READER_AUTHENTICATION_FAILED = 5;
 
     /**
-     * The requested entry was not retrieved because it was configured without any access
+     * The entry was not retrieved because it was configured without any access
      * control profile.
      */
     public static final int STATUS_NO_ACCESS_CONTROL_PROFILES = 6;
@@ -88,11 +88,10 @@
      *
      *   DeviceEngagementBytes = #6.24(bstr .cbor DeviceEngagement)
      *   EReaderKeyBytes = #6.24(bstr .cbor EReaderKey.Pub)
-     *
      *   DeviceNameSpacesBytes = #6.24(bstr .cbor DeviceNameSpaces)
      * </pre>
      *
-     * where
+     * <p>where
      *
      * <pre>
      *   DeviceNameSpaces = {
@@ -116,15 +115,16 @@
     public abstract @NonNull byte[] getAuthenticatedData();
 
     /**
-     * Returns a message authentication code over the data returned by
-     * {@link #getAuthenticatedData}, to prove to the reader that the data is from a trusted
-     * credential.
+     * Returns a message authentication code over the {@code DeviceAuthentication} CBOR
+     * specified in {@link #getAuthenticatedData()}, to prove to the reader that the data
+     * is from a trusted credential.
      *
      * <p>The MAC proves to the reader that the data is from a trusted credential. This code is
      * produced by using the key agreement and key derivation function from the ciphersuite
      * with the authentication private key and the reader ephemeral public key to compute a
      * shared message authentication code (MAC) key, then using the MAC function from the
-     * ciphersuite to compute a MAC of the authenticated data.
+     * ciphersuite to compute a MAC of the authenticated data. See section 9.2.3.5 of
+     * ISO/IEC 18013-5 for details of this operation.
      *
      * <p>If the {@code sessionTranscript} parameter passed to
      * {@link IdentityCredential#getEntries(byte[], Map, byte[], byte[])} was {@code null}
@@ -157,7 +157,7 @@
     /**
      * Get the names of all entries.
      *
-     * This includes the name of entries that wasn't successfully retrieved.
+     * <p>This includes the name of entries that wasn't successfully retrieved.
      *
      * @param namespaceName the namespace name to get entries for.
      * @return A collection of names or {@code null} if there are no entries for the given
@@ -168,7 +168,7 @@
     /**
      * Get the names of all entries that was successfully retrieved.
      *
-     * This only return entries for which {@link #getStatus(String, String)} will return
+     * <p>This only return entries for which {@link #getStatus(String, String)} will return
      * {@link #STATUS_OK}.
      *
      * @param namespaceName the namespace name to get entries for.
@@ -181,16 +181,15 @@
     /**
      * Gets the status of an entry.
      *
-     * This returns {@link #STATUS_OK} if the value was retrieved, {@link #STATUS_NO_SUCH_ENTRY}
+     * <p>This returns {@link #STATUS_OK} if the value was retrieved, {@link #STATUS_NO_SUCH_ENTRY}
      * if the given entry wasn't retrieved, {@link #STATUS_NOT_REQUESTED} if it wasn't requested,
      * {@link #STATUS_NOT_IN_REQUEST_MESSAGE} if the request message was set but the entry wasn't
-     * present in the request message,
-     * {@link #STATUS_USER_AUTHENTICATION_FAILED} if the value
+     * present in the request message, {@link #STATUS_USER_AUTHENTICATION_FAILED} if the value
      * wasn't retrieved because the necessary user authentication wasn't performed,
-     * {@link #STATUS_READER_AUTHENTICATION_FAILED} if the supplied reader certificate chain
-     * didn't match the set of certificates the entry was provisioned with, or
-     * {@link #STATUS_NO_ACCESS_CONTROL_PROFILES} if the entry was configured without any
-     * access control profiles.
+     * {@link #STATUS_READER_AUTHENTICATION_FAILED} if the supplied reader certificate chain didn't
+     * match the set of certificates the entry was provisioned with, or
+     * {@link #STATUS_NO_ACCESS_CONTROL_PROFILES} if the entry was configured without any access
+     * control profiles.
      *
      * @param namespaceName the namespace name of the entry.
      * @param name the name of the entry to get the value for.
@@ -201,7 +200,7 @@
     /**
      * Gets the raw CBOR data for the value of an entry.
      *
-     * This should only be called on an entry for which the {@link #getStatus(String, String)}
+     * <p>This should only be called on an entry for which the {@link #getStatus(String, String)}
      * method returns {@link #STATUS_OK}.
      *
      * @param namespaceName the namespace name of the entry.
diff --git a/identity/java/android/security/identity/WritableIdentityCredential.java b/identity/java/android/security/identity/WritableIdentityCredential.java
index e2a389b..c7aa328 100644
--- a/identity/java/android/security/identity/WritableIdentityCredential.java
+++ b/identity/java/android/security/identity/WritableIdentityCredential.java
@@ -41,15 +41,16 @@
      * <a href="https://source.android.com/security/keystore/attestation">Android Keystore</a>
      * attestation extension which describes the key and the security hardware in which it lives.
      *
-     * <p>Additionally, the attestation extension will contain the tag TODO_IC_KEY which indicates
-     * it is an Identity Credential key (which can only sign/MAC very specific messages) and not
-     * an Android Keystore key (which can be used to sign/MAC anything).
+     * <p>Additionally, the attestation extension will contain the tag Tag::IDENTITY_CREDENTIAL_KEY
+     * which indicates it is an Identity Credential key (which can only sign/MAC very specific
+     * messages) and not an Android Keystore key (which can be used to sign/MAC anything).
      *
      * <p>The issuer <b>MUST</b> carefully examine this certificate chain including (but not
-     * limited to) checking that the root certificate is well-known, the tag TODO_IC_KEY is
-     * present, the passed in challenge is present, the device has verified boot enabled, that each
-     * certificate in the chain is signed by its successor, that none of the certificates have been
-     * revoked and so on.
+     * limited to) checking that the root certificate is well-known, the tag
+     * Tag::IDENTITY_CREDENTIAL_KEY present, the passed in challenge is present, the tag
+     * Tag::ATTESTATION_APPLICATION_ID is set to the expected Android application, the device
+     * has verified boot enabled, each certificate in the chain is signed by its successor,
+     * none of the certificates have been revoked, and so on.
      *
      * <p>It is not strictly necessary to use this method to provision a credential if the issuing
      * authority doesn't care about the nature of the security hardware. If called, however, this
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 3e1f72d..8ea6883 100755
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -2713,6 +2713,32 @@
     }
 
     /**
+     * @hide
+     */
+    public static String audioFocusToString(int focus) {
+        switch (focus) {
+            case AUDIOFOCUS_NONE:
+                return "AUDIOFOCUS_NONE";
+            case AUDIOFOCUS_GAIN:
+                return "AUDIOFOCUS_GAIN";
+            case AUDIOFOCUS_GAIN_TRANSIENT:
+                return "AUDIOFOCUS_GAIN_TRANSIENT";
+            case AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:
+                return "AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK";
+            case AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE:
+                return "AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE";
+            case AUDIOFOCUS_LOSS:
+                return "AUDIOFOCUS_LOSS";
+            case AUDIOFOCUS_LOSS_TRANSIENT:
+                return "AUDIOFOCUS_LOSS_TRANSIENT";
+            case AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: // Note CAN_DUCK not MAY_DUCK.
+                return "AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK";
+            default:
+                return "AUDIO_FOCUS_UNKNOWN(" + focus + ")";
+        }
+    }
+
+    /**
      * Used to indicate no audio focus has been gained or lost, or requested.
      */
     public static final int AUDIOFOCUS_NONE = 0;
diff --git a/media/java/android/media/AudioPortEventHandler.java b/media/java/android/media/AudioPortEventHandler.java
index 14249cb..8e8dfaf 100644
--- a/media/java/android/media/AudioPortEventHandler.java
+++ b/media/java/android/media/AudioPortEventHandler.java
@@ -78,7 +78,8 @@
                                     listeners.add((AudioManager.OnAudioPortUpdateListener)msg.obj);
                                 }
                             } else {
-                                listeners = mListeners;
+                                listeners = (ArrayList<AudioManager.OnAudioPortUpdateListener>)
+                                        mListeners.clone();
                             }
                         }
                         // reset audio port cache if the event corresponds to a change coming
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index c11762b..373f6e1 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -214,6 +214,175 @@
         }
     }
 
+    /**
+     * @hide
+     * Convert a native audio format integer constant to a string.
+     */
+    public static String audioFormatToString(int audioFormat) {
+        switch (audioFormat) {
+            case /* AUDIO_FORMAT_INVALID         */ 0xFFFFFFFF:
+                return "AUDIO_FORMAT_INVALID";
+            case /* AUDIO_FORMAT_DEFAULT         */ 0:
+                return "AUDIO_FORMAT_DEFAULT";
+            case /* AUDIO_FORMAT_MP3             */ 0x01000000:
+                return "AUDIO_FORMAT_MP3";
+            case /* AUDIO_FORMAT_AMR_NB          */ 0x02000000:
+                return "AUDIO_FORMAT_AMR_NB";
+            case /* AUDIO_FORMAT_AMR_WB          */ 0x03000000:
+                return "AUDIO_FORMAT_AMR_WB";
+            case /* AUDIO_FORMAT_AAC             */ 0x04000000:
+                return "AUDIO_FORMAT_AAC";
+            case /* AUDIO_FORMAT_HE_AAC_V1       */ 0x05000000:
+                return "AUDIO_FORMAT_HE_AAC_V1";
+            case /* AUDIO_FORMAT_HE_AAC_V2       */ 0x06000000:
+                return "AUDIO_FORMAT_HE_AAC_V2";
+            case /* AUDIO_FORMAT_VORBIS          */ 0x07000000:
+                return "AUDIO_FORMAT_VORBIS";
+            case /* AUDIO_FORMAT_OPUS            */ 0x08000000:
+                return "AUDIO_FORMAT_OPUS";
+            case /* AUDIO_FORMAT_AC3             */ 0x09000000:
+                return "AUDIO_FORMAT_AC3";
+            case /* AUDIO_FORMAT_E_AC3           */ 0x0A000000:
+                return "AUDIO_FORMAT_E_AC3";
+            case /* AUDIO_FORMAT_DTS             */ 0x0B000000:
+                return "AUDIO_FORMAT_DTS";
+            case /* AUDIO_FORMAT_DTS_HD          */ 0x0C000000:
+                return "AUDIO_FORMAT_DTS_HD";
+            case /* AUDIO_FORMAT_IEC61937        */ 0x0D000000:
+                return "AUDIO_FORMAT_IEC61937";
+            case /* AUDIO_FORMAT_DOLBY_TRUEHD    */ 0x0E000000:
+                return "AUDIO_FORMAT_DOLBY_TRUEHD";
+            case /* AUDIO_FORMAT_EVRC            */ 0x10000000:
+                return "AUDIO_FORMAT_EVRC";
+            case /* AUDIO_FORMAT_EVRCB           */ 0x11000000:
+                return "AUDIO_FORMAT_EVRCB";
+            case /* AUDIO_FORMAT_EVRCWB          */ 0x12000000:
+                return "AUDIO_FORMAT_EVRCWB";
+            case /* AUDIO_FORMAT_EVRCNW          */ 0x13000000:
+                return "AUDIO_FORMAT_EVRCNW";
+            case /* AUDIO_FORMAT_AAC_ADIF        */ 0x14000000:
+                return "AUDIO_FORMAT_AAC_ADIF";
+            case /* AUDIO_FORMAT_WMA             */ 0x15000000:
+                return "AUDIO_FORMAT_WMA";
+            case /* AUDIO_FORMAT_WMA_PRO         */ 0x16000000:
+                return "AUDIO_FORMAT_WMA_PRO";
+            case /* AUDIO_FORMAT_AMR_WB_PLUS     */ 0x17000000:
+                return "AUDIO_FORMAT_AMR_WB_PLUS";
+            case /* AUDIO_FORMAT_MP2             */ 0x18000000:
+                return "AUDIO_FORMAT_MP2";
+            case /* AUDIO_FORMAT_QCELP           */ 0x19000000:
+                return "AUDIO_FORMAT_QCELP";
+            case /* AUDIO_FORMAT_DSD             */ 0x1A000000:
+                return "AUDIO_FORMAT_DSD";
+            case /* AUDIO_FORMAT_FLAC            */ 0x1B000000:
+                return "AUDIO_FORMAT_FLAC";
+            case /* AUDIO_FORMAT_ALAC            */ 0x1C000000:
+                return "AUDIO_FORMAT_ALAC";
+            case /* AUDIO_FORMAT_APE             */ 0x1D000000:
+                return "AUDIO_FORMAT_APE";
+            case /* AUDIO_FORMAT_AAC_ADTS        */ 0x1E000000:
+                return "AUDIO_FORMAT_AAC_ADTS";
+            case /* AUDIO_FORMAT_SBC             */ 0x1F000000:
+                return "AUDIO_FORMAT_SBC";
+            case /* AUDIO_FORMAT_APTX            */ 0x20000000:
+                return "AUDIO_FORMAT_APTX";
+            case /* AUDIO_FORMAT_APTX_HD         */ 0x21000000:
+                return "AUDIO_FORMAT_APTX_HD";
+            case /* AUDIO_FORMAT_AC4             */ 0x22000000:
+                return "AUDIO_FORMAT_AC4";
+            case /* AUDIO_FORMAT_LDAC            */ 0x23000000:
+                return "AUDIO_FORMAT_LDAC";
+            case /* AUDIO_FORMAT_MAT             */ 0x24000000:
+                return "AUDIO_FORMAT_MAT";
+            case /* AUDIO_FORMAT_AAC_LATM        */ 0x25000000:
+                return "AUDIO_FORMAT_AAC_LATM";
+            case /* AUDIO_FORMAT_CELT            */ 0x26000000:
+                return "AUDIO_FORMAT_CELT";
+            case /* AUDIO_FORMAT_APTX_ADAPTIVE   */ 0x27000000:
+                return "AUDIO_FORMAT_APTX_ADAPTIVE";
+            case /* AUDIO_FORMAT_LHDC            */ 0x28000000:
+                return "AUDIO_FORMAT_LHDC";
+            case /* AUDIO_FORMAT_LHDC_LL         */ 0x29000000:
+                return "AUDIO_FORMAT_LHDC_LL";
+            case /* AUDIO_FORMAT_APTX_TWSP       */ 0x2A000000:
+                return "AUDIO_FORMAT_APTX_TWSP";
+
+            /* Aliases */
+            case /* AUDIO_FORMAT_PCM_16_BIT        */ 0x1:
+                return "AUDIO_FORMAT_PCM_16_BIT";        // (PCM | PCM_SUB_16_BIT)
+            case /* AUDIO_FORMAT_PCM_8_BIT         */ 0x2:
+                return "AUDIO_FORMAT_PCM_8_BIT";        // (PCM | PCM_SUB_8_BIT)
+            case /* AUDIO_FORMAT_PCM_32_BIT        */ 0x3:
+                return "AUDIO_FORMAT_PCM_32_BIT";        // (PCM | PCM_SUB_32_BIT)
+            case /* AUDIO_FORMAT_PCM_8_24_BIT      */ 0x4:
+                return "AUDIO_FORMAT_PCM_8_24_BIT";        // (PCM | PCM_SUB_8_24_BIT)
+            case /* AUDIO_FORMAT_PCM_FLOAT         */ 0x5:
+                return "AUDIO_FORMAT_PCM_FLOAT";        // (PCM | PCM_SUB_FLOAT)
+            case /* AUDIO_FORMAT_PCM_24_BIT_PACKED */ 0x6:
+                return "AUDIO_FORMAT_PCM_24_BIT_PACKED";        // (PCM | PCM_SUB_24_BIT_PACKED)
+            case /* AUDIO_FORMAT_AAC_MAIN          */ 0x4000001:
+                return "AUDIO_FORMAT_AAC_MAIN";  // (AAC | AAC_SUB_MAIN)
+            case /* AUDIO_FORMAT_AAC_LC            */ 0x4000002:
+                return "AUDIO_FORMAT_AAC_LC";  // (AAC | AAC_SUB_LC)
+            case /* AUDIO_FORMAT_AAC_SSR           */ 0x4000004:
+                return "AUDIO_FORMAT_AAC_SSR";  // (AAC | AAC_SUB_SSR)
+            case /* AUDIO_FORMAT_AAC_LTP           */ 0x4000008:
+                return "AUDIO_FORMAT_AAC_LTP";  // (AAC | AAC_SUB_LTP)
+            case /* AUDIO_FORMAT_AAC_HE_V1         */ 0x4000010:
+                return "AUDIO_FORMAT_AAC_HE_V1";  // (AAC | AAC_SUB_HE_V1)
+            case /* AUDIO_FORMAT_AAC_SCALABLE      */ 0x4000020:
+                return "AUDIO_FORMAT_AAC_SCALABLE";  // (AAC | AAC_SUB_SCALABLE)
+            case /* AUDIO_FORMAT_AAC_ERLC          */ 0x4000040:
+                return "AUDIO_FORMAT_AAC_ERLC";  // (AAC | AAC_SUB_ERLC)
+            case /* AUDIO_FORMAT_AAC_LD            */ 0x4000080:
+                return "AUDIO_FORMAT_AAC_LD";  // (AAC | AAC_SUB_LD)
+            case /* AUDIO_FORMAT_AAC_HE_V2         */ 0x4000100:
+                return "AUDIO_FORMAT_AAC_HE_V2";  // (AAC | AAC_SUB_HE_V2)
+            case /* AUDIO_FORMAT_AAC_ELD           */ 0x4000200:
+                return "AUDIO_FORMAT_AAC_ELD";  // (AAC | AAC_SUB_ELD)
+            case /* AUDIO_FORMAT_AAC_XHE           */ 0x4000300:
+                return "AUDIO_FORMAT_AAC_XHE";  // (AAC | AAC_SUB_XHE)
+            case /* AUDIO_FORMAT_AAC_ADTS_MAIN     */ 0x1e000001:
+                return "AUDIO_FORMAT_AAC_ADTS_MAIN"; // (AAC_ADTS | AAC_SUB_MAIN)
+            case /* AUDIO_FORMAT_AAC_ADTS_LC       */ 0x1e000002:
+                return "AUDIO_FORMAT_AAC_ADTS_LC"; // (AAC_ADTS | AAC_SUB_LC)
+            case /* AUDIO_FORMAT_AAC_ADTS_SSR      */ 0x1e000004:
+                return "AUDIO_FORMAT_AAC_ADTS_SSR"; // (AAC_ADTS | AAC_SUB_SSR)
+            case /* AUDIO_FORMAT_AAC_ADTS_LTP      */ 0x1e000008:
+                return "AUDIO_FORMAT_AAC_ADTS_LTP"; // (AAC_ADTS | AAC_SUB_LTP)
+            case /* AUDIO_FORMAT_AAC_ADTS_HE_V1    */ 0x1e000010:
+                return "AUDIO_FORMAT_AAC_ADTS_HE_V1"; // (AAC_ADTS | AAC_SUB_HE_V1)
+            case /* AUDIO_FORMAT_AAC_ADTS_SCALABLE */ 0x1e000020:
+                return "AUDIO_FORMAT_AAC_ADTS_SCALABLE"; // (AAC_ADTS | AAC_SUB_SCALABLE)
+            case /* AUDIO_FORMAT_AAC_ADTS_ERLC     */ 0x1e000040:
+                return "AUDIO_FORMAT_AAC_ADTS_ERLC"; // (AAC_ADTS | AAC_SUB_ERLC)
+            case /* AUDIO_FORMAT_AAC_ADTS_LD       */ 0x1e000080:
+                return "AUDIO_FORMAT_AAC_ADTS_LD"; // (AAC_ADTS | AAC_SUB_LD)
+            case /* AUDIO_FORMAT_AAC_ADTS_HE_V2    */ 0x1e000100:
+                return "AUDIO_FORMAT_AAC_ADTS_HE_V2"; // (AAC_ADTS | AAC_SUB_HE_V2)
+            case /* AUDIO_FORMAT_AAC_ADTS_ELD      */ 0x1e000200:
+                return "AUDIO_FORMAT_AAC_ADTS_ELD"; // (AAC_ADTS | AAC_SUB_ELD)
+            case /* AUDIO_FORMAT_AAC_ADTS_XHE      */ 0x1e000300:
+                return "AUDIO_FORMAT_AAC_ADTS_XHE"; // (AAC_ADTS | AAC_SUB_XHE)
+            case /* AUDIO_FORMAT_AAC_LATM_LC       */ 0x25000002:
+                return "AUDIO_FORMAT_AAC_LATM_LC"; // (AAC_LATM | AAC_SUB_LC)
+            case /* AUDIO_FORMAT_AAC_LATM_HE_V1    */ 0x25000010:
+                return "AUDIO_FORMAT_AAC_LATM_HE_V1"; // (AAC_LATM | AAC_SUB_HE_V1)
+            case /* AUDIO_FORMAT_AAC_LATM_HE_V2    */ 0x25000100:
+                return "AUDIO_FORMAT_AAC_LATM_HE_V2"; // (AAC_LATM | AAC_SUB_HE_V2)
+            case /* AUDIO_FORMAT_E_AC3_JOC         */ 0xA000001:
+                return "AUDIO_FORMAT_E_AC3_JOC";  // (E_AC3 | E_AC3_SUB_JOC)
+            case /* AUDIO_FORMAT_MAT_1_0           */ 0x24000001:
+                return "AUDIO_FORMAT_MAT_1_0"; // (MAT | MAT_SUB_1_0)
+            case /* AUDIO_FORMAT_MAT_2_0           */ 0x24000002:
+                return "AUDIO_FORMAT_MAT_2_0"; // (MAT | MAT_SUB_2_0)
+            case /* AUDIO_FORMAT_MAT_2_1           */ 0x24000003:
+                return "AUDIO_FORMAT_MAT_2_1"; // (MAT | MAT_SUB_2_1)
+            default:
+                return "AUDIO_FORMAT_(" + audioFormat + ")";
+        }
+    }
+
     /* Routing bits for the former setRouting/getRouting API */
     /** @hide @deprecated */
     @Deprecated public static final int ROUTE_EARPIECE          = (1 << 0);
diff --git a/media/java/android/media/IMediaRouter2.aidl b/media/java/android/media/IMediaRouter2.aidl
index dc06153..a8b82ba 100644
--- a/media/java/android/media/IMediaRouter2.aidl
+++ b/media/java/android/media/IMediaRouter2.aidl
@@ -24,11 +24,15 @@
  * @hide
  */
 oneway interface IMediaRouter2 {
-    void notifyRestoreRoute();
     void notifyRoutesAdded(in List<MediaRoute2Info> routes);
     void notifyRoutesRemoved(in List<MediaRoute2Info> routes);
     void notifyRoutesChanged(in List<MediaRoute2Info> routes);
     void notifySessionCreated(int requestId, in @nullable RoutingSessionInfo sessionInfo);
     void notifySessionInfoChanged(in RoutingSessionInfo sessionInfo);
     void notifySessionReleased(in RoutingSessionInfo sessionInfo);
+    /**
+     * Gets hints of the new session for the given route.
+     * Call MediaRouterService#notifySessionHintsForCreatingSession to pass the result.
+     */
+    void getSessionHintsForCreatingSession(long uniqueRequestId, in MediaRoute2Info route);
 }
diff --git a/media/java/android/media/IMediaRouterService.aidl b/media/java/android/media/IMediaRouterService.aidl
index 0d87736..52bac67 100644
--- a/media/java/android/media/IMediaRouterService.aidl
+++ b/media/java/android/media/IMediaRouterService.aidl
@@ -59,6 +59,8 @@
 
     void requestCreateSessionWithRouter2(IMediaRouter2 router, int requestId,
             in MediaRoute2Info route, in @nullable Bundle sessionHints);
+    void notifySessionHintsForCreatingSession(IMediaRouter2 router, long uniqueRequestId,
+                in MediaRoute2Info route, in @nullable Bundle sessionHints);
     void selectRouteWithRouter2(IMediaRouter2 router, String sessionId, in MediaRoute2Info route);
     void deselectRouteWithRouter2(IMediaRouter2 router, String sessionId, in MediaRoute2Info route);
     void transferToRouteWithRouter2(IMediaRouter2 router, String sessionId,
diff --git a/media/java/android/media/MediaCas.java b/media/java/android/media/MediaCas.java
index 97d32d8..c652628 100644
--- a/media/java/android/media/MediaCas.java
+++ b/media/java/android/media/MediaCas.java
@@ -388,7 +388,7 @@
             @Override
             public void onReclaimResources() {
                 synchronized (mSessionMap) {
-                    mSessionMap.forEach((casSession, sessionResourceId) -> casSession.close());
+                    mSessionMap.forEach((casSession, sessionResourceHandle) -> casSession.close());
                 }
                 mEventHandler.sendMessage(mEventHandler.obtainMessage(
                         EventHandler.MSG_CAS_RESOURCE_LOST));
@@ -868,7 +868,7 @@
         }
     }
 
-    private int getSessionResourceId() throws MediaCasException {
+    private int getSessionResourceHandle() throws MediaCasException {
         validateInternalStates();
 
         int[] sessionResourceHandle = new int[1];
@@ -881,14 +881,14 @@
                     "insufficient resource to Open Session");
             }
         }
-        return  sessionResourceHandle[0];
+        return sessionResourceHandle[0];
     }
 
-    private void addSessionToResourceMap(Session session, int sessionResourceId) {
+    private void addSessionToResourceMap(Session session, int sessionResourceHandle) {
 
-        if (sessionResourceId != -1) {
+        if (sessionResourceHandle != TunerResourceManager.INVALID_RESOURCE_HANDLE) {
             synchronized (mSessionMap) {
-                mSessionMap.put(session, sessionResourceId);
+                mSessionMap.put(session, sessionResourceHandle);
             }
         }
     }
@@ -918,13 +918,13 @@
      * @throws MediaCasStateException for CAS-specific state exceptions.
      */
     public Session openSession() throws MediaCasException {
-        int sessionResourceId = getSessionResourceId();
+        int sessionResourceHandle = getSessionResourceHandle();
 
         try {
             OpenSessionCallback cb = new OpenSessionCallback();
             mICas.openSession(cb);
             MediaCasException.throwExceptionIfNeeded(cb.mStatus);
-            addSessionToResourceMap(cb.mSession, sessionResourceId);
+            addSessionToResourceMap(cb.mSession, sessionResourceHandle);
             return cb.mSession;
         } catch (RemoteException e) {
             cleanupAndRethrowIllegalState();
@@ -952,7 +952,7 @@
     @Nullable
     public Session openSession(@SessionUsage int sessionUsage, @ScramblingMode int scramblingMode)
             throws MediaCasException {
-        int sessionResourceId = getSessionResourceId();
+        int sessionResourceHandle = getSessionResourceHandle();
 
         if (mICasV12 == null) {
             Log.d(TAG, "Open Session with scrambling mode is only supported by cas@1.2+ interface");
@@ -963,7 +963,7 @@
             OpenSession_1_2_Callback cb = new OpenSession_1_2_Callback();
             mICasV12.openSession_1_2(sessionUsage, scramblingMode, cb);
             MediaCasException.throwExceptionIfNeeded(cb.mStatus);
-            addSessionToResourceMap(cb.mSession, sessionResourceId);
+            addSessionToResourceMap(cb.mSession, sessionResourceHandle);
             return cb.mSession;
         } catch (RemoteException e) {
             cleanupAndRethrowIllegalState();
diff --git a/media/java/android/media/MediaFormat.java b/media/java/android/media/MediaFormat.java
index cbf2364..1bfa999 100644
--- a/media/java/android/media/MediaFormat.java
+++ b/media/java/android/media/MediaFormat.java
@@ -1312,7 +1312,7 @@
     }
 
     /**
-     * Returns the value of an long key, or the default value if the key is missing.
+     * Returns the value of a long key, or the default value if the key is missing.
      *
      * @return defaultValue if the key does not exist or the stored value for the key is null
      * @throws ClassCastException if the stored value for the key is int, float, ByteBuffer or
@@ -1340,19 +1340,15 @@
     }
 
     /**
-     * Returns the value of an float key, or the default value if the key is missing.
+     * Returns the value of a float key, or the default value if the key is missing.
      *
      * @return defaultValue if the key does not exist or the stored value for the key is null
      * @throws ClassCastException if the stored value for the key is int, long, ByteBuffer or
      *         String
      */
     public final float getFloat(@NonNull String name, float defaultValue) {
-        try {
-            return getFloat(name);
-        } catch (NullPointerException  e) {
-            /* no such field or field is null */
-            return defaultValue;
-        }
+        Object value = mMap.get(name);
+        return value != null ? (float) value : defaultValue;
     }
 
     /**
@@ -1366,7 +1362,7 @@
     }
 
     /**
-     * Returns the value of an string key, or the default value if the key is missing.
+     * Returns the value of a string key, or the default value if the key is missing.
      *
      * @return defaultValue if the key does not exist or the stored value for the key is null
      * @throws ClassCastException if the stored value for the key is int, long, float or ByteBuffer
diff --git a/media/java/android/media/MediaMetrics.java b/media/java/android/media/MediaMetrics.java
index 540955f..f6f482d 100644
--- a/media/java/android/media/MediaMetrics.java
+++ b/media/java/android/media/MediaMetrics.java
@@ -38,6 +38,117 @@
 public class MediaMetrics {
     public static final String TAG = "MediaMetrics";
 
+    public static final String SEPARATOR = ".";
+
+    /**
+     * A list of established MediaMetrics names that can be used for Items.
+     */
+    public static class Name {
+        public static final String AUDIO = "audio";
+        public static final String AUDIO_BLUETOOTH = AUDIO + SEPARATOR + "bluetooth";
+        public static final String AUDIO_DEVICE = AUDIO + SEPARATOR + "device";
+        public static final String AUDIO_FOCUS = AUDIO + SEPARATOR + "focus";
+        public static final String AUDIO_FORCE_USE = AUDIO + SEPARATOR + "forceUse";
+        public static final String AUDIO_MIC = AUDIO + SEPARATOR + "mic";
+        public static final String AUDIO_SERVICE = AUDIO + SEPARATOR + "service";
+        public static final String AUDIO_VOLUME = AUDIO + SEPARATOR + "volume";
+        public static final String AUDIO_VOLUME_EVENT = AUDIO_VOLUME + SEPARATOR + "event";
+    }
+
+    /**
+     * A list of established string values.
+     */
+    public static class Value {
+        public static final String CONNECT = "connect";
+        public static final String CONNECTED = "connected";
+        public static final String DISCONNECT = "disconnect";
+        public static final String DISCONNECTED = "disconnected";
+        public static final String DOWN = "down";
+        public static final String MUTE = "mute";
+        public static final String NO = "no";
+        public static final String OFF = "off";
+        public static final String ON = "on";
+        public static final String UNMUTE = "unmute";
+        public static final String UP = "up";
+        public static final String YES = "yes";
+    }
+
+    /**
+     * A list of standard property keys for consistent use and type.
+     */
+    public static class Property {
+        // A use for Bluetooth or USB device addresses
+        public static final Key<String> ADDRESS = createKey("address", String.class);
+        // A string representing the Audio Attributes
+        public static final Key<String> ATTRIBUTES = createKey("attributes", String.class);
+
+        // The calling package responsible for the state change
+        public static final Key<String> CALLING_PACKAGE =
+                createKey("callingPackage", String.class);
+
+        // The client name
+        public static final Key<String> CLIENT_NAME = createKey("clientName", String.class);
+
+        // The device type
+        public static final Key<Integer> DELAY_MS = createKey("delayMs", Integer.class);
+
+        // The device type
+        public static final Key<String> DEVICE = createKey("device", String.class);
+
+        // For volume changes, up or down
+        public static final Key<String> DIRECTION = createKey("direction", String.class);
+
+        // A reason for early return or error
+        public static final Key<String> EARLY_RETURN =
+                createKey("earlyReturn", String.class);
+        // ENCODING_ ... string to match AudioFormat encoding
+        public static final Key<String> ENCODING = createKey("encoding", String.class);
+
+        public static final Key<String> EVENT = createKey("event#", String.class);
+
+        // event generated is external (yes, no)
+        public static final Key<String> EXTERNAL = createKey("external", String.class);
+
+        public static final Key<Integer> FLAGS = createKey("flags", Integer.class);
+        public static final Key<String> FOCUS_CHANGE_HINT =
+                createKey("focusChangeHint", String.class);
+        public static final Key<String> FORCE_USE_DUE_TO =
+                createKey("forceUseDueTo", String.class);
+        public static final Key<String> FORCE_USE_MODE =
+                createKey("forceUseMode", String.class);
+        public static final Key<Double> GAIN_DB =
+                createKey("gainDb", Double.class);
+        public static final Key<String> GROUP =
+                createKey("group", String.class);
+        // For volume
+        public static final Key<Integer> INDEX = createKey("index", Integer.class);
+        public static final Key<Integer> MAX_INDEX = createKey("maxIndex", Integer.class);
+        public static final Key<Integer> MIN_INDEX = createKey("minIndex", Integer.class);
+        public static final Key<String> MODE =
+                createKey("mode", String.class); // audio_mode
+        public static final Key<String> MUTE =
+                createKey("mute", String.class); // microphone, on or off.
+
+        // Bluetooth or Usb device name
+        public static final Key<String> NAME =
+                createKey("name", String.class);
+
+        // Number of observers
+        public static final Key<Integer> OBSERVERS =
+                createKey("observers", Integer.class);
+
+        public static final Key<String> REQUEST =
+                createKey("request", String.class);
+
+        // For Bluetooth
+        public static final Key<String> SCO_AUDIO_MODE =
+                createKey("scoAudioMode", String.class);
+        public static final Key<Integer> SDK = createKey("sdk", Integer.class);
+        public static final Key<String> STATE = createKey("state", String.class);
+        public static final Key<Integer> STATUS = createKey("status", Integer.class);
+        public static final Key<String> STREAM_TYPE = createKey("streamType", String.class);
+    }
+
     /**
      * The TYPE constants below should match those in native MediaMetricsItem.h
      */
diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java
index bd8fb96..0ea9624 100644
--- a/media/java/android/media/MediaRouter2.java
+++ b/media/java/android/media/MediaRouter2.java
@@ -237,9 +237,9 @@
                 } catch (RemoteException ex) {
                     Log.e(TAG, "Unable to unregister media router.", ex);
                 }
+                mStub = null;
             }
             mShouldUpdateRoutes = true;
-            mStub = null;
         }
     }
 
@@ -690,6 +690,31 @@
         matchingController.releaseInternal(/* shouldReleaseSession= */ false);
     }
 
+    void onGetControllerHintsForCreatingSessionOnHandler(long uniqueRequestId,
+            MediaRoute2Info route) {
+        OnGetControllerHintsListener listener = mOnGetControllerHintsListener;
+        Bundle controllerHints = null;
+        if (listener != null) {
+            controllerHints = listener.onGetControllerHints(route);
+            if (controllerHints != null) {
+                controllerHints = new Bundle(controllerHints);
+            }
+        }
+
+        MediaRouter2Stub stub;
+        synchronized (sRouterLock) {
+            stub = mStub;
+        }
+        if (stub != null) {
+            try {
+                mMediaRouterService.notifySessionHintsForCreatingSession(
+                        stub, uniqueRequestId, route, controllerHints);
+            } catch (RemoteException ex) {
+                Log.e(TAG, "getSessionHintsOnHandler: Unable to request.", ex);
+            }
+        }
+    }
+
     private List<MediaRoute2Info> filterRoutes(List<MediaRoute2Info> routes,
             RouteDiscoveryPreference discoveryRequest) {
         return routes.stream()
@@ -820,13 +845,14 @@
      */
     public interface OnGetControllerHintsListener {
         /**
-         * Called when the {@link MediaRouter2} is about to request
-         * the media route provider service to create a controller with the given route.
+         * Called when the {@link MediaRouter2} or the system is about to request
+         * a media route provider service to create a controller with the given route.
          * The {@link Bundle} returned here will be sent to media route provider service as a hint.
          * <p>
-         * To send hints when creating the controller, set the listener before calling
-         * {@link #transferTo(MediaRoute2Info)}. The method will be called
-         * on the same thread which calls {@link #transferTo(MediaRoute2Info)}.
+         * Since controller creation can be requested by the {@link MediaRouter2} and the system,
+         * set the listener as soon as possible after acquiring {@link MediaRouter2} instance.
+         * The method will be called on the same thread that calls
+         * {@link #transferTo(MediaRoute2Info)} or the main thread if it is requested by the system.
          *
          * @param route The route to create controller with
          * @return An optional bundle of app-specific arguments to send to the provider,
@@ -1378,9 +1404,6 @@
 
     class MediaRouter2Stub extends IMediaRouter2.Stub {
         @Override
-        public void notifyRestoreRoute() throws RemoteException {}
-
-        @Override
         public void notifyRoutesAdded(List<MediaRoute2Info> routes) {
             mHandler.sendMessage(obtainMessage(MediaRouter2::addRoutesOnHandler,
                     MediaRouter2.this, routes));
@@ -1415,5 +1438,13 @@
             mHandler.sendMessage(obtainMessage(MediaRouter2::releaseControllerOnHandler,
                     MediaRouter2.this, sessionInfo));
         }
+
+        @Override
+        public void getSessionHintsForCreatingSession(long uniqueRequestId,
+                @NonNull MediaRoute2Info route) {
+            mHandler.sendMessage(obtainMessage(
+                    MediaRouter2::onGetControllerHintsForCreatingSessionOnHandler,
+                    MediaRouter2.this, uniqueRequestId, route));
+        }
     }
 }
diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java
index b694fd0..3b570b6 100644
--- a/media/java/android/media/MediaRouter2Manager.java
+++ b/media/java/android/media/MediaRouter2Manager.java
@@ -147,14 +147,16 @@
         }
 
         synchronized (sLock) {
-            if (mCallbackRecords.size() == 0 && mClient != null) {
-                try {
-                    mMediaRouterService.unregisterManager(mClient);
-                } catch (RemoteException ex) {
-                    Log.e(TAG, "Unable to unregister media router manager", ex);
+            if (mCallbackRecords.size() == 0) {
+                if (mClient != null) {
+                    try {
+                        mMediaRouterService.unregisterManager(mClient);
+                    } catch (RemoteException ex) {
+                        Log.e(TAG, "Unable to unregister media router manager", ex);
+                    }
+                    mClient = null;
                 }
-                //TODO: clear mRoutes?
-                mClient = null;
+                mRoutes.clear();
                 mPreferredFeaturesMap.clear();
             }
         }
diff --git a/media/java/android/media/audiopolicy/AudioVolumeGroupChangeHandler.java b/media/java/android/media/audiopolicy/AudioVolumeGroupChangeHandler.java
index adf4d3d..022cfee 100644
--- a/media/java/android/media/audiopolicy/AudioVolumeGroupChangeHandler.java
+++ b/media/java/android/media/audiopolicy/AudioVolumeGroupChangeHandler.java
@@ -80,7 +80,8 @@
                                         (AudioManager.VolumeGroupCallback) msg.obj);
                             }
                         } else {
-                            listeners = mListeners;
+                            listeners = (ArrayList<AudioManager.VolumeGroupCallback>)
+                                    mListeners.clone();
                         }
                     }
                     if (listeners.isEmpty()) {
diff --git a/media/java/android/media/tv/tunerresourcemanager/ITunerResourceManager.aidl b/media/java/android/media/tv/tunerresourcemanager/ITunerResourceManager.aidl
index 7077cd1..487b444 100644
--- a/media/java/android/media/tv/tunerresourcemanager/ITunerResourceManager.aidl
+++ b/media/java/android/media/tv/tunerresourcemanager/ITunerResourceManager.aidl
@@ -218,11 +218,11 @@
      * <p><strong>Note:</strong> {@link #updateCasInfo(int, int)} must be called before this request.
      *
      * @param request {@link CasSessionRequest} information of the current request.
-     * @param sessionResourceId a one-element array to return the granted cas session id.
+     * @param casSessionHandle a one-element array to return the granted cas session handle.
      *
      * @return true if there is CAS session granted.
      */
-    boolean requestCasSession(in CasSessionRequest request, out int[] sessionResourceId);
+    boolean requestCasSession(in CasSessionRequest request, out int[] casSessionHandle);
 
     /*
      * This API is used by the Tuner framework to request an available Lnb from the TunerHAL.
@@ -276,7 +276,7 @@
      *
      * <p>Client must call this whenever it releases a descrambler.
      *
-     * @param demuxHandle the handle of the released Tuner Descrambler.
+     * @param descramblerHandle the handle of the released Tuner Descrambler.
      * @param clientId the id of the client that is releasing the descrambler.
      */
     void releaseDescrambler(in int descramblerHandle, int clientId);
@@ -288,10 +288,10 @@
      *
      * <p><strong>Note:</strong> {@link #updateCasInfo(int, int)} must be called before this release.
      *
-     * @param sessionResourceId the id of the released CAS session.
+     * @param casSessionHandle the handle of the released CAS session.
      * @param clientId the id of the client that is releasing the cas session.
      */
-    void releaseCasSession(in int sessionResourceId, int clientId);
+    void releaseCasSession(in int casSessionHandle, int clientId);
 
     /*
      * Notifies the TRM that the Lnb with the given handle was released.
diff --git a/media/java/android/media/tv/tunerresourcemanager/TunerResourceManager.java b/media/java/android/media/tv/tunerresourcemanager/TunerResourceManager.java
index b4dcc5d..be102d8 100644
--- a/media/java/android/media/tv/tunerresourcemanager/TunerResourceManager.java
+++ b/media/java/android/media/tv/tunerresourcemanager/TunerResourceManager.java
@@ -362,17 +362,16 @@
      * request.
      *
      * @param request {@link CasSessionRequest} information of the current request.
-     * @param sessionResourceId a one-element array to return the granted cas session id.
-     *                          If no CAS granted, this will return
-     *                          {@link #INVALID_CAS_SESSION_RESOURCE_ID}.
+     * @param casSessionHandle a one-element array to return the granted cas session handel.
+     *                         If no CAS granted, this will return {@link #INVALID_RESOURCE_HANDLE}.
      *
      * @return true if there is CAS session granted.
      */
     public boolean requestCasSession(@NonNull CasSessionRequest request,
-                @NonNull int[] sessionResourceId) {
+                @NonNull int[] casSessionHandle) {
         boolean result = false;
         try {
-            result = mService.requestCasSession(request, sessionResourceId);
+            result = mService.requestCasSession(request, casSessionHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -471,12 +470,12 @@
      * <p><strong>Note:</strong> {@link #updateCasInfo(int, int)} must be called before this
      * release.
      *
-     * @param sessionResourceId the id of the released CAS session.
+     * @param casSessionHandle the handle of the released CAS session.
      * @param clientId the id of the client that is releasing the cas session.
      */
-    public void releaseCasSession(int sessionResourceId, int clientId) {
+    public void releaseCasSession(int casSessionHandle, int clientId) {
         try {
-            mService.releaseCasSession(sessionResourceId, clientId);
+            mService.releaseCasSession(casSessionHandle, clientId);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/media/java/android/mtp/MtpDatabase.java b/media/java/android/mtp/MtpDatabase.java
index e6962a1..aba74e5 100755
--- a/media/java/android/mtp/MtpDatabase.java
+++ b/media/java/android/mtp/MtpDatabase.java
@@ -46,6 +46,7 @@
 import android.view.WindowManager;
 
 import com.android.internal.annotations.VisibleForNative;
+import com.android.internal.annotations.VisibleForTesting;
 
 import dalvik.system.CloseGuard;
 
@@ -408,7 +409,8 @@
     }
 
     @VisibleForNative
-    private int beginSendObject(String path, int format, int parent, int storageId) {
+    @VisibleForTesting
+    public int beginSendObject(String path, int format, int parent, int storageId) {
         MtpStorageManager.MtpObject parentObj =
                 parent == 0 ? mManager.getStorageRoot(storageId) : mManager.getObject(parent);
         if (parentObj == null) {
@@ -452,7 +454,8 @@
     }
 
     @VisibleForNative
-    private int getNumObjects(int storageID, int format, int parent) {
+    @VisibleForTesting
+    public int getNumObjects(int storageID, int format, int parent) {
         List<MtpStorageManager.MtpObject> objs = mManager.getObjects(parent,
                 format, storageID);
         if (objs == null) {
@@ -830,7 +833,8 @@
     }
 
     @VisibleForNative
-    private boolean getThumbnailInfo(int handle, long[] outLongs) {
+    @VisibleForTesting
+    public boolean getThumbnailInfo(int handle, long[] outLongs) {
         MtpStorageManager.MtpObject obj = mManager.getObject(handle);
         if (obj == null) {
             return false;
@@ -866,7 +870,8 @@
     }
 
     @VisibleForNative
-    private byte[] getThumbnailData(int handle) {
+    @VisibleForTesting
+    public byte[] getThumbnailData(int handle) {
         MtpStorageManager.MtpObject obj = mManager.getObject(handle);
         if (obj == null) {
             return null;
diff --git a/media/java/android/mtp/MtpStorage.java b/media/java/android/mtp/MtpStorage.java
index ba75263..88c32a3 100644
--- a/media/java/android/mtp/MtpStorage.java
+++ b/media/java/android/mtp/MtpStorage.java
@@ -36,7 +36,7 @@
 
     public MtpStorage(StorageVolume volume, int storageId) {
         mStorageId = storageId;
-        mPath = volume.getInternalPath();
+        mPath = volume.getPath();
         mDescription = volume.getDescription(null);
         mRemovable = volume.isRemovable();
         mMaxFileSize = volume.getMaxFileSize();
diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java
index 6ca564f..6a1e965 100644
--- a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java
+++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java
@@ -36,6 +36,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import android.content.Context;
@@ -47,6 +48,7 @@
 import android.media.MediaRouter2Utils;
 import android.media.RouteDiscoveryPreference;
 import android.media.RoutingSessionInfo;
+import android.os.Bundle;
 import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
@@ -73,6 +75,8 @@
     private static final String TAG = "MediaRouter2ManagerTest";
     private static final int WAIT_TIME_MS = 2000;
     private static final int TIMEOUT_MS = 5000;
+    private static final String TEST_KEY = "test_key";
+    private static final String TEST_VALUE = "test_value";
 
     private Context mContext;
     private MediaRouter2Manager mManager;
@@ -160,6 +164,7 @@
         });
 
         MediaRoute2Info routeToRemove = routes.get(ROUTE_ID2);
+        assertNotNull(routeToRemove);
 
         StubMediaRoute2ProviderService sInstance =
                 StubMediaRoute2ProviderService.getInstance();
@@ -171,6 +176,52 @@
         assertTrue(addedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
     }
 
+    @Test
+    public void testGetRoutes_removedRoute_returnsCorrectRoutes() throws Exception {
+        CountDownLatch addedLatch = new CountDownLatch(1);
+        CountDownLatch removedLatch = new CountDownLatch(1);
+
+        RouteCallback routeCallback = new RouteCallback() {
+            // Used to ensure the removed route is added.
+            @Override
+            public void onRoutesAdded(List<MediaRoute2Info> routes) {
+                if (removedLatch.getCount() > 0) {
+                    return;
+                }
+                addedLatch.countDown();
+            }
+
+            @Override
+            public void onRoutesRemoved(List<MediaRoute2Info> routes) {
+                removedLatch.countDown();
+            }
+        };
+
+        mRouter2.registerRouteCallback(mExecutor, routeCallback,
+                new RouteDiscoveryPreference.Builder(FEATURES_ALL, true).build());
+        mRouteCallbacks.add(routeCallback);
+
+        Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(FEATURES_ALL);
+        MediaRoute2Info routeToRemove = routes.get(ROUTE_ID2);
+        assertNotNull(routeToRemove);
+
+        StubMediaRoute2ProviderService sInstance =
+                StubMediaRoute2ProviderService.getInstance();
+        assertNotNull(sInstance);
+        sInstance.removeRoute(ROUTE_ID2);
+
+        // Wait until the route is removed.
+        assertTrue(removedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+        Map<String, MediaRoute2Info> newRoutes = waitAndGetRoutesWithManager(FEATURES_ALL);
+        assertNull(newRoutes.get(ROUTE_ID2));
+
+        // Revert the removal.
+        sInstance.addRoute(routeToRemove);
+        assertTrue(addedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        mRouter2.unregisterRouteCallback(routeCallback);
+    }
+
     /**
      * Tests if we get proper routes for application that has special route feature.
      */
@@ -465,6 +516,56 @@
         assertEquals(VOLUME_MAX, variableVolumeRoute.getVolumeMax());
     }
 
+    @Test
+    public void testRouter2SetOnGetControllerHintsListener() throws Exception {
+        Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(FEATURES_ALL);
+        addRouterCallback(new RouteCallback() {});
+
+        MediaRoute2Info route = routes.get(ROUTE_ID1);
+        assertNotNull(route);
+
+        final Bundle controllerHints = new Bundle();
+        controllerHints.putString(TEST_KEY, TEST_VALUE);
+        final CountDownLatch hintLatch = new CountDownLatch(1);
+        final MediaRouter2.OnGetControllerHintsListener listener =
+                route1 -> {
+                    hintLatch.countDown();
+                    return controllerHints;
+                };
+
+        final CountDownLatch successLatch = new CountDownLatch(1);
+        final CountDownLatch failureLatch = new CountDownLatch(1);
+
+        addManagerCallback(new MediaRouter2Manager.Callback() {
+            @Override
+            public void onTransferred(RoutingSessionInfo oldSession,
+                    RoutingSessionInfo newSession) {
+                assertTrue(newSession.getSelectedRoutes().contains(route.getId()));
+                // The StubMediaRoute2ProviderService is supposed to set control hints
+                // with the given controllerHints.
+                Bundle controlHints = newSession.getControlHints();
+                assertNotNull(controlHints);
+                assertTrue(controlHints.containsKey(TEST_KEY));
+                assertEquals(TEST_VALUE, controlHints.getString(TEST_KEY));
+
+                successLatch.countDown();
+            }
+
+            @Override
+            public void onTransferFailed(RoutingSessionInfo session,
+                    MediaRoute2Info requestedRoute) {
+                failureLatch.countDown();
+            }
+        });
+
+        mRouter2.setOnGetControllerHintsListener(listener);
+        mManager.selectRoute(mPackageName, route);
+        assertTrue(hintLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertTrue(successLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+        assertFalse(failureLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
     Map<String, MediaRoute2Info> waitAndGetRoutesWithManager(List<String> routeFeatures)
             throws Exception {
         CountDownLatch addedLatch = new CountDownLatch(1);
@@ -475,8 +576,8 @@
         MediaRouter2Manager.Callback managerCallback = new MediaRouter2Manager.Callback() {
             @Override
             public void onRoutesAdded(List<MediaRoute2Info> routes) {
-                for (int i = 0; i < routes.size(); i++) {
-                    if (!routes.get(i).isSystemRoute()) {
+                for (MediaRoute2Info route : routes) {
+                    if (!route.isSystemRoute()) {
                         addedLatch.countDown();
                         break;
                     }
diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java
index 6d46ba5..4e398f2 100644
--- a/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java
+++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java
@@ -65,9 +65,9 @@
     public static final String ROUTE_NAME_VARIABLE_VOLUME = "Variable Volume Route";
 
     public static final String FEATURE_SAMPLE =
-            "com.android.mediarouteprovider.FEATURE_SAMPLE";
+            "com.android.mediaroutertest.FEATURE_SAMPLE";
     public static final String FEATURE_SPECIAL =
-            "com.android.mediarouteprovider.FEATURE_SPECIAL";
+            "com.android.mediaroutertest..FEATURE_SPECIAL";
 
     Map<String, MediaRoute2Info> mRoutes = new HashMap<>();
     Map<String, String> mRouteIdToSessionId = new HashMap<>();
diff --git a/media/tests/MtpTests/res/raw/test_bad_thumb.jpg b/media/tests/MtpTests/res/raw/test_bad_thumb.jpg
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/media/tests/MtpTests/res/raw/test_bad_thumb.jpg
diff --git a/media/tests/MtpTests/src/android/mtp/MtpDatabaseTest.java b/media/tests/MtpTests/src/android/mtp/MtpDatabaseTest.java
new file mode 100644
index 0000000..e2e8ff4
--- /dev/null
+++ b/media/tests/MtpTests/src/android/mtp/MtpDatabaseTest.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.mtp;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.Build;
+import android.os.FileUtils;
+import android.os.UserHandle;
+import android.os.storage.StorageManager;
+import android.os.storage.StorageVolume;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.Preconditions;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Tests for MtpDatabase functionality.
+ */
+@RunWith(AndroidJUnit4.class)
+public class MtpDatabaseTest {
+    private static final String TAG = MtpDatabaseTest.class.getSimpleName();
+
+    private final Context mContext = InstrumentationRegistry.getContext();
+
+    private static final File mBaseDir = InstrumentationRegistry.getContext().getExternalCacheDir();
+    private static final String MAIN_STORAGE_DIR = mBaseDir.getPath() + "/" + TAG + "/";
+    private static final String TEST_DIRNAME = "/TestIs";
+
+    private static final int MAIN_STORAGE_ID = 0x10001;
+    private static final int SCND_STORAGE_ID = 0x20001;
+    private static final String MAIN_STORAGE_ID_STR = Integer.toHexString(MAIN_STORAGE_ID);
+    private static final String SCND_STORAGE_ID_STR = Integer.toHexString(SCND_STORAGE_ID);
+
+    private static final File mMainStorageDir = new File(MAIN_STORAGE_DIR);
+
+    private static ServerHolder mServerHolder;
+    private MtpDatabase mMtpDatabase;
+
+    private static void logMethodName() {
+        Log.d(TAG, Thread.currentThread().getStackTrace()[3].getMethodName());
+    }
+
+    private static File createNewDir(File parent, String name) {
+        File ret = new File(parent, name);
+        if (!ret.mkdir())
+            throw new AssertionError(
+                    "Failed to create file: name=" + name + ", " + parent.getPath());
+        return ret;
+    }
+
+    private static void writeNewFile(File newFile) {
+        try {
+            new FileOutputStream(newFile).write(new byte[] {0, 0, 0});
+        } catch (IOException e) {
+            Assert.fail();
+        }
+    }
+
+    private static void writeNewFileFromByte(File newFile, byte[] byteData) {
+        try {
+            new FileOutputStream(newFile).write(byteData);
+        } catch (IOException e) {
+            Assert.fail();
+        }
+    }
+
+    private static class ServerHolder {
+        @NonNull final MtpServer server;
+        @NonNull final MtpDatabase database;
+
+        ServerHolder(@NonNull MtpServer server, @NonNull MtpDatabase database) {
+            Preconditions.checkNotNull(server);
+            Preconditions.checkNotNull(database);
+            this.server = server;
+            this.database = database;
+        }
+
+        void close() {
+            this.database.setServer(null);
+        }
+    }
+
+    private class OnServerTerminated implements Runnable {
+        @Override
+        public void run() {
+            if (mServerHolder == null) {
+                Log.e(TAG, "mServerHolder is unexpectedly null.");
+                return;
+            }
+            mServerHolder.close();
+            mServerHolder = null;
+        }
+    }
+
+    @Before
+    public void setUp() {
+        FileUtils.deleteContentsAndDir(mMainStorageDir);
+        Assert.assertTrue(mMainStorageDir.mkdir());
+
+        StorageVolume mainStorage = new StorageVolume(MAIN_STORAGE_ID_STR,
+                mMainStorageDir, mMainStorageDir, "Primary Storage",
+				true, false, true, false, -1, UserHandle.CURRENT, "", "");
+
+        final StorageVolume primary = mainStorage;
+
+        mMtpDatabase = new MtpDatabase(mContext, null);
+
+        final MtpServer server =
+                new MtpServer(mMtpDatabase, null, false,
+                        new OnServerTerminated(), Build.MANUFACTURER,
+                        Build.MODEL, "1.0");
+        mMtpDatabase.setServer(server);
+        mServerHolder = new ServerHolder(server, mMtpDatabase);
+
+        mMtpDatabase.addStorage(mainStorage);
+    }
+
+    @After
+    public void tearDown() {
+        FileUtils.deleteContentsAndDir(mMainStorageDir);
+    }
+
+    private File stageFile(int resId, File file) throws IOException {
+        try (InputStream source = mContext.getResources().openRawResource(resId);
+                OutputStream target = new FileOutputStream(file)) {
+            android.os.FileUtils.copy(source, target);
+        }
+        return file;
+    }
+
+    /**
+     * Refer to BitmapUtilTests, but keep here,
+	 * so as to be aware of the behavior or interface change there
+     */
+    private void assertBitmapSize(int expectedWidth, int expectedHeight, Bitmap bitmap) {
+        Assert.assertTrue(
+                "Abnormal bitmap.width: " + bitmap.getWidth(), bitmap.getWidth() >= expectedWidth);
+        Assert.assertTrue(
+                "Abnormal bitmap.height: " + bitmap.getHeight(),
+                bitmap.getHeight() >= expectedHeight);
+    }
+
+    private byte[] createJpegRawData(int sourceWidth, int sourceHeight) throws IOException {
+        return createRawData(Bitmap.CompressFormat.JPEG, sourceWidth, sourceHeight);
+    }
+
+    private byte[] createPngRawData(int sourceWidth, int sourceHeight) throws IOException {
+        return createRawData(Bitmap.CompressFormat.PNG, sourceWidth, sourceHeight);
+    }
+
+    private byte[] createRawData(Bitmap.CompressFormat format, int sourceWidth, int sourceHeight)
+            throws IOException {
+        // Create a temp bitmap as our source
+        Bitmap b = Bitmap.createBitmap(sourceWidth, sourceHeight, Bitmap.Config.ARGB_8888);
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        b.compress(format, 50, outputStream);
+        final byte[] data = outputStream.toByteArray();
+        outputStream.close();
+        return data;
+    }
+
+    /**
+     * Decodes the bitmap with the given sample size
+     */
+    public static Bitmap decodeBitmapFromBytes(byte[] bytes, int sampleSize) {
+        final BitmapFactory.Options options;
+        if (sampleSize <= 1) {
+            options = null;
+        } else {
+            options = new BitmapFactory.Options();
+            options.inSampleSize = sampleSize;
+        }
+        return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
+    }
+
+    private void testThumbnail(int fileHandle, File imgFile, boolean isGoodThumb)
+            throws IOException {
+        boolean isValidThumb;
+        byte[] byteArray;
+        long[] outLongs = new long[3];
+
+        isValidThumb = mMtpDatabase.getThumbnailInfo(fileHandle, outLongs);
+        Assert.assertTrue(isValidThumb);
+
+        byteArray = mMtpDatabase.getThumbnailData(fileHandle);
+
+        if (isGoodThumb) {
+            Assert.assertNotNull("Fail to generate thumbnail:" + imgFile.getPath(), byteArray);
+
+            Bitmap testBitmap = decodeBitmapFromBytes(byteArray, 4);
+            assertBitmapSize(32, 16, testBitmap);
+        } else Assert.assertNull("Bad image should return null:" + imgFile.getPath(), byteArray);
+    }
+
+    @Test
+    @SmallTest
+    public void testMtpDatabaseThumbnail() throws IOException {
+        int baseHandle;
+        int handleJpgBadThumb, handleJpgNoThumb, handleJpgBad;
+        int handlePng1, handlePngBad;
+        final String baseTestDirStr = mMainStorageDir.getPath() + TEST_DIRNAME;
+
+        logMethodName();
+
+        Log.d(TAG, "testMtpDatabaseThumbnail: Generate and insert tested files.");
+
+        baseHandle = mMtpDatabase.beginSendObject(baseTestDirStr,
+                MtpConstants.FORMAT_ASSOCIATION, 0, MAIN_STORAGE_ID);
+
+        File baseDir = new File(baseTestDirStr);
+        baseDir.mkdirs();
+
+        final File jpgfileBadThumb = new File(baseDir, "jpgfileBadThumb.jpg");
+        final File jpgFileNoThumb = new File(baseDir, "jpgFileNoThumb.jpg");
+        final File jpgfileBad = new File(baseDir, "jpgfileBad.jpg");
+        final File pngFile1 = new File(baseDir, "pngFile1.png");
+        final File pngFileBad = new File(baseDir, "pngFileBad.png");
+
+        handleJpgBadThumb = mMtpDatabase.beginSendObject(jpgfileBadThumb.getPath(),
+                MtpConstants.FORMAT_EXIF_JPEG, baseHandle, MAIN_STORAGE_ID);
+        stageFile(R.raw.test_bad_thumb, jpgfileBadThumb);
+
+        handleJpgNoThumb = mMtpDatabase.beginSendObject(jpgFileNoThumb.getPath(),
+                MtpConstants.FORMAT_EXIF_JPEG, baseHandle, MAIN_STORAGE_ID);
+        writeNewFileFromByte(jpgFileNoThumb, createJpegRawData(128, 64));
+
+        handleJpgBad = mMtpDatabase.beginSendObject(jpgfileBad.getPath(),
+                MtpConstants.FORMAT_EXIF_JPEG, baseHandle, MAIN_STORAGE_ID);
+        writeNewFile(jpgfileBad);
+
+        handlePng1 = mMtpDatabase.beginSendObject(pngFile1.getPath(),
+                MtpConstants.FORMAT_PNG, baseHandle, MAIN_STORAGE_ID);
+        writeNewFileFromByte(pngFile1, createPngRawData(128, 64));
+
+        handlePngBad = mMtpDatabase.beginSendObject(pngFileBad.getPath(),
+                MtpConstants.FORMAT_PNG, baseHandle, MAIN_STORAGE_ID);
+        writeNewFile(pngFileBad);
+
+        Log.d(TAG, "testMtpDatabaseThumbnail: Test bad JPG");
+
+        testThumbnail(handleJpgBadThumb, jpgfileBadThumb, false);
+
+        testThumbnail(handleJpgNoThumb, jpgFileNoThumb, false);
+
+        testThumbnail(handleJpgBad, jpgfileBad, false);
+
+        Log.d(TAG, "testMtpDatabaseThumbnail: Test PNG");
+
+        testThumbnail(handlePng1, pngFile1, true);
+
+        Log.d(TAG, "testMtpDatabaseThumbnail: Test bad PNG");
+
+        testThumbnail(handlePngBad, pngFileBad, false);
+    }
+
+    @Test
+    @SmallTest
+    public void testMtpDatabaseExtStorage() throws IOException {
+        int numObj;
+        StorageVolume[] mVolumes;
+
+        logMethodName();
+
+        mVolumes = StorageManager.getVolumeList(UserHandle.myUserId(), 0);
+        // Currently it may need manual setup for 2nd storage on virtual device testing.
+        // Thus only run test when 2nd storage exists.
+        Assume.assumeTrue(
+                "Skip when 2nd storage not available, volume numbers = " + mVolumes.length,
+                mVolumes.length >= 2);
+
+        for (int ii = 0; ii < mVolumes.length; ii++) {
+            StorageVolume volume = mVolumes[ii];
+            // Skip Actual Main storage (Internal Storage),
+            // since we use manipulated path as testing Main storage
+            if (ii > 0)
+                mMtpDatabase.addStorage(volume);
+        }
+
+        numObj = mMtpDatabase.getNumObjects(SCND_STORAGE_ID, 0, 0xFFFFFFFF);
+        Assert.assertTrue(
+                "Fail to get objects in 2nd storage, object numbers = " + numObj, numObj >= 0);
+    }
+}
diff --git a/packages/CarSystemUI/res-keyguard/layout/keyguard_password_view.xml b/packages/CarSystemUI/res-keyguard/layout/keyguard_password_view.xml
index 7004fb6..8b235e6 100644
--- a/packages/CarSystemUI/res-keyguard/layout/keyguard_password_view.xml
+++ b/packages/CarSystemUI/res-keyguard/layout/keyguard_password_view.xml
@@ -29,8 +29,6 @@
     android:orientation="vertical"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    androidprv:layout_maxWidth="@dimen/keyguard_security_width"
-    androidprv:layout_maxHeight="@dimen/keyguard_security_height"
     android:gravity="center">
 
     <include layout="@layout/keyguard_message_area" />
diff --git a/packages/CarSystemUI/res/layout/car_top_navigation_bar.xml b/packages/CarSystemUI/res/layout/car_top_navigation_bar.xml
index ce0d31c..a7347f2 100644
--- a/packages/CarSystemUI/res/layout/car_top_navigation_bar.xml
+++ b/packages/CarSystemUI/res/layout/car_top_navigation_bar.xml
@@ -45,7 +45,7 @@
                 systemui:intent="intent:#Intent;action=android.car.intent.action.TOGGLE_HVAC_CONTROLS;end"
             />
 
-            <com.android.systemui.statusbar.hvac.AnimatedTemperatureView
+            <com.android.systemui.car.hvac.AnimatedTemperatureView
                 android:id="@+id/lefttext"
                 android:layout_width="wrap_content"
                 android:layout_height="match_parent"
@@ -127,7 +127,7 @@
                 systemui:intent="intent:#Intent;action=android.car.intent.action.TOGGLE_HVAC_CONTROLS;end"
             />
 
-            <com.android.systemui.statusbar.hvac.AnimatedTemperatureView
+            <com.android.systemui.car.hvac.AnimatedTemperatureView
                 android:id="@+id/righttext"
                 android:layout_width="wrap_content"
                 android:layout_height="match_parent"
diff --git a/packages/CarSystemUI/res/layout/car_top_navigation_bar_unprovisioned.xml b/packages/CarSystemUI/res/layout/car_top_navigation_bar_unprovisioned.xml
index a71567c..aa0a8c5 100644
--- a/packages/CarSystemUI/res/layout/car_top_navigation_bar_unprovisioned.xml
+++ b/packages/CarSystemUI/res/layout/car_top_navigation_bar_unprovisioned.xml
@@ -45,7 +45,7 @@
           systemui:intent="intent:#Intent;action=android.car.intent.action.TOGGLE_HVAC_CONTROLS;end"
           />
 
-      <com.android.systemui.statusbar.hvac.AnimatedTemperatureView
+      <com.android.systemui.car.hvac.AnimatedTemperatureView
           android:id="@+id/lefttext"
           android:layout_width="wrap_content"
           android:layout_height="match_parent"
@@ -123,7 +123,7 @@
           systemui:intent="intent:#Intent;action=android.car.intent.action.TOGGLE_HVAC_CONTROLS;end"
           />
 
-      <com.android.systemui.statusbar.hvac.AnimatedTemperatureView
+      <com.android.systemui.car.hvac.AnimatedTemperatureView
           android:id="@+id/righttext"
           android:layout_width="wrap_content"
           android:layout_height="match_parent"
diff --git a/packages/CarSystemUI/res/values/config.xml b/packages/CarSystemUI/res/values/config.xml
index e6fb501..52d13c3 100644
--- a/packages/CarSystemUI/res/values/config.xml
+++ b/packages/CarSystemUI/res/values/config.xml
@@ -86,29 +86,30 @@
     <string-array name="config_systemUIServiceComponents" translatable="false">
         <item>com.android.systemui.util.NotificationChannels</item>
         <item>com.android.systemui.keyguard.KeyguardViewMediator</item>
-        <item>com.android.systemui.recents.Recents</item>
-        <item>com.android.systemui.volume.VolumeUI</item>
-        <item>com.android.systemui.stackdivider.Divider</item>
+<!--        <item>com.android.systemui.recents.Recents</item>-->
+<!--        <item>com.android.systemui.volume.VolumeUI</item>-->
+<!--        <item>com.android.systemui.stackdivider.Divider</item>-->
 <!--        <item>com.android.systemui.statusbar.phone.StatusBar</item>-->
         <item>com.android.systemui.usb.StorageNotification</item>
         <item>com.android.systemui.power.PowerUI</item>
         <item>com.android.systemui.media.RingtonePlayer</item>
-        <item>com.android.systemui.keyboard.KeyboardUI</item>
-        <item>com.android.systemui.pip.PipUI</item>
-        <item>com.android.systemui.shortcut.ShortcutKeyDispatcher</item>
+<!--        <item>com.android.systemui.keyboard.KeyboardUI</item>-->
+<!--        <item>com.android.systemui.pip.PipUI</item>-->
+<!--        <item>com.android.systemui.shortcut.ShortcutKeyDispatcher</item>-->
         <item>@string/config_systemUIVendorServiceComponent</item>
         <item>com.android.systemui.util.leak.GarbageMonitor$Service</item>
-        <item>com.android.systemui.LatencyTester</item>
-        <item>com.android.systemui.globalactions.GlobalActionsComponent</item>
+<!--        <item>com.android.systemui.LatencyTester</item>-->
+<!--        <item>com.android.systemui.globalactions.GlobalActionsComponent</item>-->
         <item>com.android.systemui.ScreenDecorations</item>
         <item>com.android.systemui.biometrics.AuthController</item>
-        <item>com.android.systemui.SliceBroadcastRelayHandler</item>
+<!--        <item>com.android.systemui.SliceBroadcastRelayHandler</item>-->
         <item>com.android.systemui.SizeCompatModeActivityController</item>
-        <item>com.android.systemui.statusbar.notification.InstantAppNotifier</item>
+<!--        <item>com.android.systemui.statusbar.notification.InstantAppNotifier</item>-->
         <item>com.android.systemui.theme.ThemeOverlayController</item>
         <item>com.android.systemui.navigationbar.car.CarNavigationBar</item>
         <item>com.android.systemui.toast.ToastUI</item>
-        <item>com.android.systemui.voicerecognition.car.ConnectedDeviceVoiceRecognitionNotifier</item>
+        <item>com.android.systemui.car.voicerecognition.ConnectedDeviceVoiceRecognitionNotifier</item>
         <item>com.android.systemui.window.SystemUIOverlayWindowManager</item>
+        <item>com.android.systemui.car.volume.VolumeUI</item>
     </string-array>
 </resources>
diff --git a/packages/CarSystemUI/src/com/android/systemui/CarSystemUIBinder.java b/packages/CarSystemUI/src/com/android/systemui/CarSystemUIBinder.java
index 59fa9d0..91e222c 100644
--- a/packages/CarSystemUI/src/com/android/systemui/CarSystemUIBinder.java
+++ b/packages/CarSystemUI/src/com/android/systemui/CarSystemUIBinder.java
@@ -19,6 +19,8 @@
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.bubbles.dagger.BubbleModule;
 import com.android.systemui.car.notification.CarNotificationModule;
+import com.android.systemui.car.voicerecognition.ConnectedDeviceVoiceRecognitionNotifier;
+import com.android.systemui.car.volume.VolumeUI;
 import com.android.systemui.globalactions.GlobalActionsComponent;
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.keyguard.dagger.KeyguardModule;
@@ -38,8 +40,6 @@
 import com.android.systemui.theme.ThemeOverlayController;
 import com.android.systemui.toast.ToastUI;
 import com.android.systemui.util.leak.GarbageMonitor;
-import com.android.systemui.voicerecognition.car.ConnectedDeviceVoiceRecognitionNotifier;
-import com.android.systemui.volume.VolumeUI;
 import com.android.systemui.window.OverlayWindowModule;
 import com.android.systemui.window.SystemUIOverlayWindowManager;
 
@@ -65,7 +65,7 @@
     @ClassKey(Divider.class)
     public abstract SystemUI bindDivider(Divider sysui);
 
-    /** */
+    /** Inject Car Navigation Bar. */
     @Binds
     @IntoMap
     @ClassKey(CarNavigationBar.class)
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/AnimatedTemperatureView.java b/packages/CarSystemUI/src/com/android/systemui/car/hvac/AnimatedTemperatureView.java
similarity index 97%
rename from packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/AnimatedTemperatureView.java
rename to packages/CarSystemUI/src/com/android/systemui/car/hvac/AnimatedTemperatureView.java
index 908aaad..a729431 100644
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/AnimatedTemperatureView.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/hvac/AnimatedTemperatureView.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2018 The Android Open Source Project
+ * 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.
@@ -11,10 +11,10 @@
  * 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
+ * limitations under the License.
  */
 
-package com.android.systemui.statusbar.hvac;
+package com.android.systemui.car.hvac;
 
 import android.animation.ObjectAnimator;
 import android.annotation.SuppressLint;
@@ -35,7 +35,6 @@
 import android.widget.TextView;
 
 import com.android.systemui.R;
-import com.android.systemui.navigationbar.car.hvac.TemperatureView;
 
 /**
  * Simple text display of HVAC properties, It is designed to show mTemperature and is configured in
diff --git a/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/hvac/HvacController.java b/packages/CarSystemUI/src/com/android/systemui/car/hvac/HvacController.java
similarity index 98%
rename from packages/CarSystemUI/src/com/android/systemui/navigationbar/car/hvac/HvacController.java
rename to packages/CarSystemUI/src/com/android/systemui/car/hvac/HvacController.java
index fd9c488..af8ddb6 100644
--- a/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/hvac/HvacController.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/hvac/HvacController.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2019 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.navigationbar.car.hvac;
+package com.android.systemui.car.hvac;
 
 import static android.car.VehicleAreaType.VEHICLE_AREA_TYPE_GLOBAL;
 import static android.car.VehiclePropertyIds.HVAC_TEMPERATURE_DISPLAY_UNITS;
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/TemperatureBackgroundAnimator.java b/packages/CarSystemUI/src/com/android/systemui/car/hvac/TemperatureBackgroundAnimator.java
similarity index 96%
rename from packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/TemperatureBackgroundAnimator.java
rename to packages/CarSystemUI/src/com/android/systemui/car/hvac/TemperatureBackgroundAnimator.java
index 3c6d623..a4c4573 100644
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/TemperatureBackgroundAnimator.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/hvac/TemperatureBackgroundAnimator.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2018 The Android Open Source Project
+ * 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.
@@ -11,15 +11,15 @@
  * 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
+ * limitations under the License.
  */
 
-package com.android.systemui.statusbar.hvac;
+package com.android.systemui.car.hvac;
 
-import static com.android.systemui.statusbar.hvac.AnimatedTemperatureView.isHorizontal;
-import static com.android.systemui.statusbar.hvac.AnimatedTemperatureView.isLeft;
-import static com.android.systemui.statusbar.hvac.AnimatedTemperatureView.isTop;
-import static com.android.systemui.statusbar.hvac.AnimatedTemperatureView.isVertical;
+import static com.android.systemui.car.hvac.AnimatedTemperatureView.isHorizontal;
+import static com.android.systemui.car.hvac.AnimatedTemperatureView.isLeft;
+import static com.android.systemui.car.hvac.AnimatedTemperatureView.isTop;
+import static com.android.systemui.car.hvac.AnimatedTemperatureView.isVertical;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/TemperatureColorStore.java b/packages/CarSystemUI/src/com/android/systemui/car/hvac/TemperatureColorStore.java
similarity index 97%
rename from packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/TemperatureColorStore.java
rename to packages/CarSystemUI/src/com/android/systemui/car/hvac/TemperatureColorStore.java
index a40ffaf..9a7b0b9 100644
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/TemperatureColorStore.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/hvac/TemperatureColorStore.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2018 The Android Open Source Project
+ * 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.
@@ -11,10 +11,10 @@
  * 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
+ * limitations under the License.
  */
 
-package com.android.systemui.statusbar.hvac;
+package com.android.systemui.car.hvac;
 
 import android.graphics.Color;
 
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/TemperatureTextAnimator.java b/packages/CarSystemUI/src/com/android/systemui/car/hvac/TemperatureTextAnimator.java
similarity index 95%
rename from packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/TemperatureTextAnimator.java
rename to packages/CarSystemUI/src/com/android/systemui/car/hvac/TemperatureTextAnimator.java
index 8ee5ef6..74d9704 100644
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/TemperatureTextAnimator.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/hvac/TemperatureTextAnimator.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2018 The Android Open Source Project
+ * 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.
@@ -11,13 +11,13 @@
  * 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
+ * limitations under the License.
  */
 
-package com.android.systemui.statusbar.hvac;
+package com.android.systemui.car.hvac;
 
-import static com.android.systemui.statusbar.hvac.AnimatedTemperatureView.isHorizontal;
-import static com.android.systemui.statusbar.hvac.AnimatedTemperatureView.isLeft;
+import static com.android.systemui.car.hvac.AnimatedTemperatureView.isHorizontal;
+import static com.android.systemui.car.hvac.AnimatedTemperatureView.isLeft;
 
 import android.annotation.NonNull;
 import android.view.animation.AccelerateDecelerateInterpolator;
diff --git a/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/hvac/TemperatureTextView.java b/packages/CarSystemUI/src/com/android/systemui/car/hvac/TemperatureTextView.java
similarity index 95%
rename from packages/CarSystemUI/src/com/android/systemui/navigationbar/car/hvac/TemperatureTextView.java
rename to packages/CarSystemUI/src/com/android/systemui/car/hvac/TemperatureTextView.java
index ad4fcd9..521a665 100644
--- a/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/hvac/TemperatureTextView.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/hvac/TemperatureTextView.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2019 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.navigationbar.car.hvac;
+package com.android.systemui.car.hvac;
 
 import android.content.Context;
 import android.content.res.TypedArray;
diff --git a/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/hvac/TemperatureView.java b/packages/CarSystemUI/src/com/android/systemui/car/hvac/TemperatureView.java
similarity index 93%
rename from packages/CarSystemUI/src/com/android/systemui/navigationbar/car/hvac/TemperatureView.java
rename to packages/CarSystemUI/src/com/android/systemui/car/hvac/TemperatureView.java
index 963f318..6b903fa 100644
--- a/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/hvac/TemperatureView.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/hvac/TemperatureView.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2019 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.navigationbar.car.hvac;
+package com.android.systemui.car.hvac;
 
 /**
  * Interface for Views that display temperature HVAC properties
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/keyguard/CarKeyguardViewController.java b/packages/CarSystemUI/src/com/android/systemui/car/keyguard/CarKeyguardViewController.java
index 60ee19e..4fde309 100644
--- a/packages/CarSystemUI/src/com/android/systemui/car/keyguard/CarKeyguardViewController.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/keyguard/CarKeyguardViewController.java
@@ -205,6 +205,9 @@
 
     @Override
     public void onCancelClicked() {
+        getOverlayViewGlobalStateController().setWindowFocusable(/* focusable= */ false);
+        getOverlayViewGlobalStateController().setWindowNeedsInput(/* needsInput= */ false);
+
         mBouncer.hide(/* destroyView= */ true);
         mKeyguardCancelClickedListener.onCancelClicked();
     }
@@ -226,7 +229,8 @@
 
     @Override
     public void setNeedsInput(boolean needsInput) {
-        getLayout().setFocusable(needsInput);
+        getOverlayViewGlobalStateController().setWindowFocusable(needsInput);
+        getOverlayViewGlobalStateController().setWindowNeedsInput(needsInput);
     }
 
     /**
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/notification/CarHeadsUpNotificationSystemContainer.java b/packages/CarSystemUI/src/com/android/systemui/car/notification/CarHeadsUpNotificationSystemContainer.java
index 53e5d9f..20fcca0 100644
--- a/packages/CarSystemUI/src/com/android/systemui/car/notification/CarHeadsUpNotificationSystemContainer.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/notification/CarHeadsUpNotificationSystemContainer.java
@@ -30,7 +30,6 @@
 import com.android.car.notification.headsup.CarHeadsUpNotificationContainer;
 import com.android.systemui.car.CarDeviceProvisionedController;
 import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.statusbar.car.CarStatusBar;
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
@@ -43,7 +42,7 @@
 @Singleton
 public class CarHeadsUpNotificationSystemContainer implements CarHeadsUpNotificationContainer {
     private final CarDeviceProvisionedController mCarDeviceProvisionedController;
-    private final Lazy<CarStatusBar> mCarStatusBarLazy;
+    private final Lazy<NotificationPanelViewController> mNotificationPanelViewControllerLazy;
 
     private final ViewGroup mWindow;
     private final FrameLayout mHeadsUpContentFrame;
@@ -55,10 +54,9 @@
             @Main Resources resources,
             CarDeviceProvisionedController deviceProvisionedController,
             WindowManager windowManager,
-            // TODO: Remove dependency on CarStatusBar
-            Lazy<CarStatusBar> carStatusBarLazy) {
+            Lazy<NotificationPanelViewController> notificationPanelViewControllerLazy) {
         mCarDeviceProvisionedController = deviceProvisionedController;
-        mCarStatusBarLazy = carStatusBarLazy;
+        mNotificationPanelViewControllerLazy = notificationPanelViewControllerLazy;
 
         boolean showOnBottom = resources.getBoolean(R.bool.config_showHeadsUpNotificationOnBottom);
 
@@ -87,7 +85,8 @@
 
     private void animateShow() {
         if ((mEnableHeadsUpNotificationWhenNotificationShadeOpen
-                || !mCarStatusBarLazy.get().isPanelExpanded()) && isCurrentUserSetup()) {
+                || !mNotificationPanelViewControllerLazy.get().isPanelExpanded())
+                && mCarDeviceProvisionedController.isCurrentUserFullySetup()) {
             mWindow.setVisibility(View.VISIBLE);
         }
     }
@@ -114,9 +113,4 @@
     public boolean isVisible() {
         return mWindow.getVisibility() == View.VISIBLE;
     }
-
-    private boolean isCurrentUserSetup() {
-        return mCarDeviceProvisionedController.isCurrentUserSetup()
-                && !mCarDeviceProvisionedController.isCurrentUserSetupInProgress();
-    }
 }
diff --git a/packages/CarSystemUI/src/com/android/systemui/sideloaded/car/CarSideLoadedAppDetector.java b/packages/CarSystemUI/src/com/android/systemui/car/sideloaded/CarSideLoadedAppDetector.java
similarity index 98%
rename from packages/CarSystemUI/src/com/android/systemui/sideloaded/car/CarSideLoadedAppDetector.java
rename to packages/CarSystemUI/src/com/android/systemui/car/sideloaded/CarSideLoadedAppDetector.java
index c0dbb58..f145b14 100644
--- a/packages/CarSystemUI/src/com/android/systemui/sideloaded/car/CarSideLoadedAppDetector.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/sideloaded/CarSideLoadedAppDetector.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.sideloaded.car;
+package com.android.systemui.car.sideloaded;
 
 import android.annotation.NonNull;
 import android.app.ActivityManager;
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/userswitcher/UserGridRecyclerView.java b/packages/CarSystemUI/src/com/android/systemui/car/userswitcher/UserGridRecyclerView.java
index 58add17..2ff6670 100644
--- a/packages/CarSystemUI/src/com/android/systemui/car/userswitcher/UserGridRecyclerView.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/userswitcher/UserGridRecyclerView.java
@@ -368,7 +368,7 @@
 
         private void applyCarSysUIDialogFlags(AlertDialog dialog) {
             final Window window = dialog.getWindow();
-            window.setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
+            window.setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
             window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
                     | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
             window.getAttributes().setFitInsetsTypes(
diff --git a/packages/CarSystemUI/src/com/android/systemui/voicerecognition/car/ConnectedDeviceVoiceRecognitionNotifier.java b/packages/CarSystemUI/src/com/android/systemui/car/voicerecognition/ConnectedDeviceVoiceRecognitionNotifier.java
similarity index 98%
rename from packages/CarSystemUI/src/com/android/systemui/voicerecognition/car/ConnectedDeviceVoiceRecognitionNotifier.java
rename to packages/CarSystemUI/src/com/android/systemui/car/voicerecognition/ConnectedDeviceVoiceRecognitionNotifier.java
index 2f79f96..c054d20 100644
--- a/packages/CarSystemUI/src/com/android/systemui/voicerecognition/car/ConnectedDeviceVoiceRecognitionNotifier.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/voicerecognition/ConnectedDeviceVoiceRecognitionNotifier.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.voicerecognition.car;
+package com.android.systemui.car.voicerecognition;
 
 import android.bluetooth.BluetoothHeadsetClient;
 import android.content.BroadcastReceiver;
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/volume/VolumeUI.java b/packages/CarSystemUI/src/com/android/systemui/car/volume/VolumeUI.java
new file mode 100644
index 0000000..2bdb85f
--- /dev/null
+++ b/packages/CarSystemUI/src/com/android/systemui/car/volume/VolumeUI.java
@@ -0,0 +1,125 @@
+/*
+ * 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.car.volume;
+
+import android.car.Car;
+import android.car.media.CarAudioManager;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.Handler;
+import android.util.Log;
+
+import com.android.systemui.R;
+import com.android.systemui.SystemUI;
+import com.android.systemui.car.CarServiceProvider;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.volume.VolumeDialogComponent;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import dagger.Lazy;
+
+/** The entry point for controlling the volume ui in cars. */
+@Singleton
+public class VolumeUI extends SystemUI {
+
+    private static final String TAG = "VolumeUI";
+    private final Resources mResources;
+    private final Handler mMainHandler;
+    private final CarServiceProvider mCarServiceProvider;
+    private final Lazy<VolumeDialogComponent> mVolumeDialogComponentLazy;
+
+    private final CarAudioManager.CarVolumeCallback mVolumeChangeCallback =
+            new CarAudioManager.CarVolumeCallback() {
+                @Override
+                public void onGroupVolumeChanged(int zoneId, int groupId, int flags) {
+                    if (mVolumeDialogComponent == null) {
+                        mMainHandler.post(() -> {
+                            mVolumeDialogComponent = mVolumeDialogComponentLazy.get();
+                            mVolumeDialogComponent.register();
+                        });
+                        mCarAudioManager.unregisterCarVolumeCallback(mVolumeChangeCallback);
+                    }
+                }
+
+                @Override
+                public void onMasterMuteChanged(int zoneId, int flags) {
+                    // ignored
+                }
+            };
+
+    private boolean mEnabled;
+    private CarAudioManager mCarAudioManager;
+    private VolumeDialogComponent mVolumeDialogComponent;
+
+    @Inject
+    public VolumeUI(
+            Context context,
+            @Main Resources resources,
+            @Main Handler mainHandler,
+            CarServiceProvider carServiceProvider,
+            Lazy<VolumeDialogComponent> volumeDialogComponentLazy
+    ) {
+        super(context);
+        mResources = resources;
+        mMainHandler = mainHandler;
+        mCarServiceProvider = carServiceProvider;
+        mVolumeDialogComponentLazy = volumeDialogComponentLazy;
+    }
+
+    @Override
+    public void start() {
+        boolean enableVolumeUi = mResources.getBoolean(R.bool.enable_volume_ui);
+        mEnabled = enableVolumeUi;
+        if (!mEnabled) return;
+
+        mCarServiceProvider.addListener(car -> {
+            if (mCarAudioManager != null) {
+                return;
+            }
+
+            mCarAudioManager = (CarAudioManager) car.getCarManager(Car.AUDIO_SERVICE);
+            Log.d(TAG, "Registering mVolumeChangeCallback.");
+            // This volume call back is never unregistered because CarStatusBar is
+            // never destroyed.
+            mCarAudioManager.registerCarVolumeCallback(mVolumeChangeCallback);
+        });
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        if (!mEnabled) return;
+        if (mVolumeDialogComponent != null) {
+            mVolumeDialogComponent.onConfigurationChanged(newConfig);
+        }
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.print("mEnabled="); pw.println(mEnabled);
+        if (!mEnabled) return;
+        if (mVolumeDialogComponent != null) {
+            mVolumeDialogComponent.dump(fd, pw, args);
+        }
+    }
+}
diff --git a/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBar.java b/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBar.java
index 9fdfc0f..4e315c6 100644
--- a/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBar.java
+++ b/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBar.java
@@ -51,8 +51,6 @@
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.statusbar.AutoHideUiElement;
 import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.NavigationBarController;
-import com.android.systemui.statusbar.SuperStatusBarViewFactory;
 import com.android.systemui.statusbar.phone.AutoHideController;
 import com.android.systemui.statusbar.phone.BarTransitions;
 import com.android.systemui.statusbar.phone.PhoneStatusBarPolicy;
@@ -79,8 +77,6 @@
     private final ButtonSelectionStateListener mButtonSelectionStateListener;
     private final Handler mMainHandler;
     private final Lazy<KeyguardStateController> mKeyguardStateControllerLazy;
-    private final Lazy<NavigationBarController> mNavigationBarControllerLazy;
-    private final SuperStatusBarViewFactory mSuperStatusBarViewFactory;
     private final ButtonSelectionStateController mButtonSelectionStateController;
     private final PhoneStatusBarPolicy mIconPolicy;
     private final StatusBarIconController mIconController;
@@ -126,8 +122,6 @@
             ButtonSelectionStateListener buttonSelectionStateListener,
             @Main Handler mainHandler,
             Lazy<KeyguardStateController> keyguardStateControllerLazy,
-            Lazy<NavigationBarController> navigationBarControllerLazy,
-            SuperStatusBarViewFactory superStatusBarViewFactory,
             ButtonSelectionStateController buttonSelectionStateController,
             PhoneStatusBarPolicy iconPolicy,
             StatusBarIconController iconController
@@ -142,8 +136,6 @@
         mButtonSelectionStateListener = buttonSelectionStateListener;
         mMainHandler = mainHandler;
         mKeyguardStateControllerLazy = keyguardStateControllerLazy;
-        mNavigationBarControllerLazy = navigationBarControllerLazy;
-        mSuperStatusBarViewFactory = superStatusBarViewFactory;
         mButtonSelectionStateController = buttonSelectionStateController;
         mIconPolicy = iconPolicy;
         mIconController = iconController;
@@ -319,11 +311,6 @@
                     result.mImeWindowVis, result.mImeBackDisposition,
                     result.mShowImeSwitcher);
         }
-
-        // There has been a car customized nav bar on the default display, so just create nav bars
-        // on external displays.
-        mNavigationBarControllerLazy.get().createNavigationBars(/* includeDefaultDisplay= */ false,
-                result);
     }
 
     private void buildNavBarWindows() {
diff --git a/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBarController.java b/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBarController.java
index 37a8225..8f3ae1a 100644
--- a/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBarController.java
+++ b/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBarController.java
@@ -24,7 +24,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.systemui.R;
-import com.android.systemui.navigationbar.car.hvac.HvacController;
+import com.android.systemui.car.hvac.HvacController;
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
diff --git a/packages/CarSystemUI/src/com/android/systemui/volume/CarVolumeDialogComponent.java b/packages/CarSystemUI/src/com/android/systemui/volume/CarVolumeDialogComponent.java
index 5a34436..35ab322 100644
--- a/packages/CarSystemUI/src/com/android/systemui/volume/CarVolumeDialogComponent.java
+++ b/packages/CarSystemUI/src/com/android/systemui/volume/CarVolumeDialogComponent.java
@@ -45,6 +45,7 @@
     @Override
     protected VolumeDialog createDefault() {
         mCarVolumeDialog = new CarVolumeDialogImpl(mContext);
+        mCarVolumeDialog.show(Events.SHOW_REASON_VOLUME_CHANGED);
         return mCarVolumeDialog;
     }
 }
diff --git a/packages/CarSystemUI/src/com/android/systemui/volume/CarVolumeDialogImpl.java b/packages/CarSystemUI/src/com/android/systemui/volume/CarVolumeDialogImpl.java
index 873d7d7..2218eb5 100644
--- a/packages/CarSystemUI/src/com/android/systemui/volume/CarVolumeDialogImpl.java
+++ b/packages/CarSystemUI/src/com/android/systemui/volume/CarVolumeDialogImpl.java
@@ -234,6 +234,20 @@
         cleanupAudioManager();
     }
 
+    /**
+     * Reveals volume dialog.
+     */
+    public void show(int reason) {
+        mHandler.obtainMessage(H.SHOW, reason).sendToTarget();
+    }
+
+    /**
+     * Hides volume dialog.
+     */
+    public void dismiss(int reason) {
+        mHandler.obtainMessage(H.DISMISS, reason).sendToTarget();
+    }
+
     private void initDialog() {
         loadAudioUsageItems();
         mCarVolumeLineItems.clear();
diff --git a/packages/CarSystemUI/src/com/android/systemui/window/OverlayViewGlobalStateController.java b/packages/CarSystemUI/src/com/android/systemui/window/OverlayViewGlobalStateController.java
index 402d742..5fe03f1 100644
--- a/packages/CarSystemUI/src/com/android/systemui/window/OverlayViewGlobalStateController.java
+++ b/packages/CarSystemUI/src/com/android/systemui/window/OverlayViewGlobalStateController.java
@@ -123,6 +123,12 @@
         mSystemUIOverlayWindowController.setWindowFocusable(focusable);
     }
 
+    /** Sets the {@link android.view.WindowManager.LayoutParams#FLAG_ALT_FOCUSABLE_IM} flag of the
+     * sysui overlay window */
+    public void setWindowNeedsInput(boolean needsInput) {
+        mSystemUIOverlayWindowController.setWindowNeedsInput(needsInput);
+    }
+
     /** Returns {@code true} if the window is focusable. */
     public boolean isWindowFocusable() {
         return mSystemUIOverlayWindowController.isWindowFocusable();
diff --git a/packages/CarSystemUI/src/com/android/systemui/window/SystemUIOverlayWindowController.java b/packages/CarSystemUI/src/com/android/systemui/window/SystemUIOverlayWindowController.java
index 0dbe1a3..5df5d6e 100644
--- a/packages/CarSystemUI/src/com/android/systemui/window/SystemUIOverlayWindowController.java
+++ b/packages/CarSystemUI/src/com/android/systemui/window/SystemUIOverlayWindowController.java
@@ -90,7 +90,7 @@
         mLp = new WindowManager.LayoutParams(
                 ViewGroup.LayoutParams.MATCH_PARENT,
                 ViewGroup.LayoutParams.MATCH_PARENT,
-                WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL,
+                WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE,
                 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                         | WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING
                         | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
@@ -132,6 +132,16 @@
         updateWindow();
     }
 
+    /** Sets the window to enable IME. */
+    public void setWindowNeedsInput(boolean needsInput) {
+        if (needsInput) {
+            mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+        } else {
+            mLpChanged.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+        }
+        updateWindow();
+    }
+
     /** Returns {@code true} if the window is visible */
     public boolean isWindowVisible() {
         return mVisible;
diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/hvac/HvacControllerTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/car/hvac/HvacControllerTest.java
similarity index 97%
rename from packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/hvac/HvacControllerTest.java
rename to packages/CarSystemUI/tests/src/com/android/systemui/car/hvac/HvacControllerTest.java
index a71d1db..7996170 100644
--- a/packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/hvac/HvacControllerTest.java
+++ b/packages/CarSystemUI/tests/src/com/android/systemui/car/hvac/HvacControllerTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2019 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.navigationbar.car.hvac;
+package com.android.systemui.car.hvac;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/car/notification/CarHeadsUpNotificationSystemContainerTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/car/notification/CarHeadsUpNotificationSystemContainerTest.java
index 05b8e6a..6ac72a6 100644
--- a/packages/CarSystemUI/tests/src/com/android/systemui/car/notification/CarHeadsUpNotificationSystemContainerTest.java
+++ b/packages/CarSystemUI/tests/src/com/android/systemui/car/notification/CarHeadsUpNotificationSystemContainerTest.java
@@ -31,7 +31,6 @@
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.car.CarDeviceProvisionedController;
-import com.android.systemui.statusbar.car.CarStatusBar;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -48,7 +47,7 @@
     @Mock
     private CarDeviceProvisionedController mCarDeviceProvisionedController;
     @Mock
-    private CarStatusBar mCarStatusBar;
+    private NotificationPanelViewController mNotificationPanelViewController;
     @Mock
     private WindowManager mWindowManager;
 
@@ -61,7 +60,7 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
 
-        when(mCarStatusBar.isPanelExpanded()).thenReturn(false);
+        when(mNotificationPanelViewController.isPanelExpanded()).thenReturn(false);
         when(mCarDeviceProvisionedController.isCurrentUserSetup()).thenReturn(true);
         when(mCarDeviceProvisionedController.isCurrentUserSetupInProgress()).thenReturn(false);
 
@@ -72,14 +71,14 @@
 
         mDefaultController = new CarHeadsUpNotificationSystemContainer(mContext,
                 testableResources.getResources(), mCarDeviceProvisionedController, mWindowManager,
-                () -> mCarStatusBar);
+                () -> mNotificationPanelViewController);
 
         testableResources.addOverride(
                 R.bool.config_enableHeadsUpNotificationWhenNotificationShadeOpen, true);
 
         mOverrideEnabledController = new CarHeadsUpNotificationSystemContainer(mContext,
                 testableResources.getResources(), mCarDeviceProvisionedController, mWindowManager,
-                () -> mCarStatusBar);
+                () -> mNotificationPanelViewController);
     }
 
     @Test
@@ -120,14 +119,14 @@
 
     @Test
     public void testDisplayNotification_notificationPanelExpanded_isInvisible() {
-        when(mCarStatusBar.isPanelExpanded()).thenReturn(true);
+        when(mNotificationPanelViewController.isPanelExpanded()).thenReturn(true);
         mDefaultController.displayNotification(mNotificationView);
         assertThat(mDefaultController.isVisible()).isFalse();
     }
 
     @Test
     public void testDisplayNotification_notificationPanelExpandedEnabledHUNWhenOpen_isVisible() {
-        when(mCarStatusBar.isPanelExpanded()).thenReturn(true);
+        when(mNotificationPanelViewController.isPanelExpanded()).thenReturn(true);
         mOverrideEnabledController.displayNotification(mNotificationView);
         assertThat(mOverrideEnabledController.isVisible()).isTrue();
     }
diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/sideloaded/car/CarSideLoadedAppDetectorTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/car/sideloaded/CarSideLoadedAppDetectorTest.java
similarity index 99%
rename from packages/CarSystemUI/tests/src/com/android/systemui/sideloaded/car/CarSideLoadedAppDetectorTest.java
rename to packages/CarSystemUI/tests/src/com/android/systemui/car/sideloaded/CarSideLoadedAppDetectorTest.java
index aebb0e0..80f3d1ee 100644
--- a/packages/CarSystemUI/tests/src/com/android/systemui/sideloaded/car/CarSideLoadedAppDetectorTest.java
+++ b/packages/CarSystemUI/tests/src/com/android/systemui/car/sideloaded/CarSideLoadedAppDetectorTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.sideloaded.car;
+package com.android.systemui.car.sideloaded;
 
 import static com.google.common.truth.Truth.assertThat;
 
diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/voicerecognition/car/ConnectedDeviceVoiceRecognitionNotifierTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/car/voicerecognition/ConnectedDeviceVoiceRecognitionNotifierTest.java
similarity index 93%
rename from packages/CarSystemUI/tests/src/com/android/systemui/voicerecognition/car/ConnectedDeviceVoiceRecognitionNotifierTest.java
rename to packages/CarSystemUI/tests/src/com/android/systemui/car/voicerecognition/ConnectedDeviceVoiceRecognitionNotifierTest.java
index 38b47d0..eca51e3 100644
--- a/packages/CarSystemUI/tests/src/com/android/systemui/voicerecognition/car/ConnectedDeviceVoiceRecognitionNotifierTest.java
+++ b/packages/CarSystemUI/tests/src/com/android/systemui/car/voicerecognition/ConnectedDeviceVoiceRecognitionNotifierTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2019 The Android Open Source Project
+ * 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.
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.systemui.voicerecognition.car;
+package com.android.systemui.car.voicerecognition;
 
-import static com.android.systemui.voicerecognition.car.ConnectedDeviceVoiceRecognitionNotifier.INVALID_VALUE;
-import static com.android.systemui.voicerecognition.car.ConnectedDeviceVoiceRecognitionNotifier.VOICE_RECOGNITION_STARTED;
+import static com.android.systemui.car.voicerecognition.ConnectedDeviceVoiceRecognitionNotifier.INVALID_VALUE;
+import static com.android.systemui.car.voicerecognition.ConnectedDeviceVoiceRecognitionNotifier.VOICE_RECOGNITION_STARTED;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.never;
diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/CarNavigationBarControllerTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/CarNavigationBarControllerTest.java
index bbcd0d4..28c69c7 100644
--- a/packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/CarNavigationBarControllerTest.java
+++ b/packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/CarNavigationBarControllerTest.java
@@ -31,7 +31,7 @@
 
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.navigationbar.car.hvac.HvacController;
+import com.android.systemui.car.hvac.HvacController;
 import com.android.systemui.plugins.DarkIconDispatcher;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
 
diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/CarNavigationBarTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/CarNavigationBarTest.java
index 3ecb29f..f789d38 100644
--- a/packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/CarNavigationBarTest.java
+++ b/packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/CarNavigationBarTest.java
@@ -37,8 +37,6 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.car.CarDeviceProvisionedController;
 import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.NavigationBarController;
-import com.android.systemui.statusbar.SuperStatusBarViewFactory;
 import com.android.systemui.statusbar.phone.AutoHideController;
 import com.android.systemui.statusbar.phone.PhoneStatusBarPolicy;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
@@ -73,10 +71,6 @@
     @Mock
     private KeyguardStateController mKeyguardStateController;
     @Mock
-    private NavigationBarController mNavigationBarController;
-    @Mock
-    private SuperStatusBarViewFactory mSuperStatusBarViewFactory;
-    @Mock
     private ButtonSelectionStateController mButtonSelectionStateController;
     @Mock
     private PhoneStatusBarPolicy mIconPolicy;
@@ -91,9 +85,8 @@
         mCarNavigationBar = new CarNavigationBar(mContext, mTestableResources.getResources(),
                 mCarNavigationBarController, mWindowManager, mDeviceProvisionedController,
                 new CommandQueue(mContext), mAutoHideController, mButtonSelectionStateListener,
-                mHandler, () -> mKeyguardStateController, () -> mNavigationBarController,
-                mSuperStatusBarViewFactory, mButtonSelectionStateController, mIconPolicy,
-                mIconController);
+                mHandler, () -> mKeyguardStateController, mButtonSelectionStateController,
+                mIconPolicy, mIconController);
     }
 
     @Test
diff --git a/packages/CtsShim/build/Android.bp b/packages/CtsShim/build/Android.bp
index 54986a4..be79010 100644
--- a/packages/CtsShim/build/Android.bp
+++ b/packages/CtsShim/build/Android.bp
@@ -70,6 +70,7 @@
     // v2/v3 signature.
     use_embedded_native_libs: true,
     apex_available: [
+        "//apex_available:platform",
         "com.android.apex.cts.shim.v1",
         "com.android.apex.cts.shim.v2",
         "com.android.apex.cts.shim.v2_no_hashtree",
@@ -120,6 +121,7 @@
     },
     manifest: "shim/AndroidManifestTargetPSdk.xml",
     apex_available: [
+        "//apex_available:platform",
         "com.android.apex.cts.shim.v2_apk_in_apex_sdk_target_p",
     ],
 }
@@ -140,6 +142,7 @@
 
     manifest: "shim/AndroidManifest.xml",
     apex_available: [
+        "//apex_available:platform",
         "com.android.apex.cts.shim.v1",
         "com.android.apex.cts.shim.v2",
         "com.android.apex.cts.shim.v2_no_hashtree",
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 1007d83..7baaf49 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1080,14 +1080,14 @@
     <string name="power_suggestion_battery_run_out">Battery may run out by <xliff:g id="time" example="12 PM">%1$s</xliff:g></string>
 
     <!-- [CHAR_LIMIT=60] label for estimated remaining duration of battery when under a certain amount -->
-    <string name="power_remaining_less_than_duration_only">Less than <xliff:g id="threshold">%1$s</xliff:g> remaining</string>
+    <string name="power_remaining_less_than_duration_only">Less than <xliff:g id="threshold">%1$s</xliff:g> left</string>
     <!-- [CHAR_LIMIT=60] label for estimated remaining duration of battery when under a certain amount with the percentage -->
-    <string name="power_remaining_less_than_duration">Less than <xliff:g id="threshold">%1$s</xliff:g> remaining (<xliff:g id="level">%2$s</xliff:g>)</string>
+    <string name="power_remaining_less_than_duration">Less than <xliff:g id="threshold">%1$s</xliff:g> left (<xliff:g id="level">%2$s</xliff:g>)</string>
 
     <!-- Used to let users know that they have more than some amount of battery life remaining with percentage. ex: 75% - more than 1 day remaining [CHAR LIMIT = 80] -->
-    <string name="power_remaining_more_than_subtext">More than <xliff:g id="time_remaining">%1$s</xliff:g> remaining (<xliff:g id="level">%2$s</xliff:g>)</string>
+    <string name="power_remaining_more_than_subtext">More than <xliff:g id="time_remaining">%1$s</xliff:g> left (<xliff:g id="level">%2$s</xliff:g>)</string>
     <!-- Used to let users know that they have more than some amount of battery life remaining. ex: more than 1 day remaining [CHAR LIMIT = 40] -->
-    <string name="power_remaining_only_more_than_subtext">More than <xliff:g id="time_remaining">%1$s</xliff:g> remaining</string>
+    <string name="power_remaining_only_more_than_subtext">More than <xliff:g id="time_remaining">%1$s</xliff:g> left</string>
 
     <!-- [CHAR_LIMIT=50] Short label for imminent shutdown warning of device -->
     <string name="power_remaining_duration_only_shutdown_imminent" product="default">Phone may shut down soon</string>
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java
index e551b69..ee8fb38 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java
@@ -38,7 +38,7 @@
 
     BluetoothMediaDevice(Context context, CachedBluetoothDevice device,
             MediaRouter2Manager routerManager, MediaRoute2Info info, String packageName) {
-        super(context, MediaDeviceType.TYPE_BLUETOOTH_DEVICE, routerManager, info, packageName);
+        super(context, routerManager, info, packageName);
         mCachedDevice = device;
         initDeviceRecord();
     }
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java
index 85fa988..83a9671 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java
@@ -38,7 +38,7 @@
 
     InfoMediaDevice(Context context, MediaRouter2Manager routerManager, MediaRoute2Info info,
             String packageName) {
-        super(context, MediaDeviceType.TYPE_CAST_DEVICE, routerManager, info, packageName);
+        super(context, routerManager, info, packageName);
         initDeviceRecord();
     }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
index 39e6a12..6aff301 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
@@ -15,6 +15,16 @@
  */
 package com.android.settingslib.media;
 
+import static android.media.MediaRoute2Info.TYPE_BLUETOOTH_A2DP;
+import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER;
+import static android.media.MediaRoute2Info.TYPE_GROUP;
+import static android.media.MediaRoute2Info.TYPE_HEARING_AID;
+import static android.media.MediaRoute2Info.TYPE_REMOTE_SPEAKER;
+import static android.media.MediaRoute2Info.TYPE_REMOTE_TV;
+import static android.media.MediaRoute2Info.TYPE_UNKNOWN;
+import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES;
+import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET;
+
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.graphics.drawable.Drawable;
@@ -38,13 +48,21 @@
     private static final String TAG = "MediaDevice";
 
     @Retention(RetentionPolicy.SOURCE)
-    @IntDef({MediaDeviceType.TYPE_CAST_DEVICE,
+    @IntDef({MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE,
+            MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE,
+            MediaDeviceType.TYPE_FAST_PAIR_BLUETOOTH_DEVICE,
             MediaDeviceType.TYPE_BLUETOOTH_DEVICE,
+            MediaDeviceType.TYPE_CAST_DEVICE,
+            MediaDeviceType.TYPE_CAST_GROUP_DEVICE,
             MediaDeviceType.TYPE_PHONE_DEVICE})
     public @interface MediaDeviceType {
-        int TYPE_PHONE_DEVICE = 1;
-        int TYPE_CAST_DEVICE = 2;
-        int TYPE_BLUETOOTH_DEVICE = 3;
+        int TYPE_USB_C_AUDIO_DEVICE = 1;
+        int TYPE_3POINT5_MM_AUDIO_DEVICE = 2;
+        int TYPE_FAST_PAIR_BLUETOOTH_DEVICE = 3;
+        int TYPE_BLUETOOTH_DEVICE = 4;
+        int TYPE_CAST_DEVICE = 5;
+        int TYPE_CAST_GROUP_DEVICE = 6;
+        int TYPE_PHONE_DEVICE = 7;
     }
 
     @VisibleForTesting
@@ -58,13 +76,43 @@
     protected final MediaRouter2Manager mRouterManager;
     protected final String mPackageName;
 
-    MediaDevice(Context context, @MediaDeviceType int type, MediaRouter2Manager routerManager,
-            MediaRoute2Info info, String packageName) {
-        mType = type;
+    MediaDevice(Context context, MediaRouter2Manager routerManager, MediaRoute2Info info,
+            String packageName) {
         mContext = context;
         mRouteInfo = info;
         mRouterManager = routerManager;
         mPackageName = packageName;
+        setType(info);
+    }
+
+    private void setType(MediaRoute2Info info) {
+        if (info == null) {
+            mType = MediaDeviceType.TYPE_BLUETOOTH_DEVICE;
+            return;
+        }
+
+        switch (info.getType()) {
+            case TYPE_GROUP:
+                mType = MediaDeviceType.TYPE_CAST_GROUP_DEVICE;
+                break;
+            case TYPE_BUILTIN_SPEAKER:
+                mType = MediaDeviceType.TYPE_PHONE_DEVICE;
+                break;
+            case TYPE_WIRED_HEADSET:
+            case TYPE_WIRED_HEADPHONES:
+                mType = MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE;
+                break;
+            case TYPE_HEARING_AID:
+            case TYPE_BLUETOOTH_A2DP:
+                mType = MediaDeviceType.TYPE_BLUETOOTH_DEVICE;
+                break;
+            case TYPE_UNKNOWN:
+            case TYPE_REMOTE_TV:
+            case TYPE_REMOTE_SPEAKER:
+            default:
+                mType = MediaDeviceType.TYPE_CAST_DEVICE;
+                break;
+        }
     }
 
     void initDeviceRecord() {
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
index af88723..c6c5ade 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
@@ -42,7 +42,7 @@
 
     PhoneMediaDevice(Context context, MediaRouter2Manager routerManager, MediaRoute2Info info,
             String packageName) {
-        super(context, MediaDeviceType.TYPE_PHONE_DEVICE, routerManager, info, packageName);
+        super(context, routerManager, info, packageName);
 
         initDeviceRecord();
     }
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java b/packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java
index 653c8ad..a210e90 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java
@@ -33,6 +33,7 @@
 import android.util.Slog;
 import android.view.LayoutInflater;
 import android.view.View;
+import android.view.ViewGroup;
 import android.widget.CompoundButton;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
@@ -357,46 +358,46 @@
             }
         });
 
-        // minus button
-        final ImageView button1 = (ImageView) row.findViewById(android.R.id.button1);
-        button1.setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                onClickTimeButton(row, tag, false /*down*/, rowId);
-                tag.lines.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
-            }
-        });
-
-        // plus button
-        final ImageView button2 = (ImageView) row.findViewById(android.R.id.button2);
-        button2.setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                onClickTimeButton(row, tag, true /*up*/, rowId);
-                tag.lines.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
-            }
-        });
-
         final long time = ZenModeConfig.tryParseCountdownConditionId(conditionId);
+        final ImageView minusButton = (ImageView) row.findViewById(android.R.id.button1);
+        final ImageView plusButton = (ImageView) row.findViewById(android.R.id.button2);
         if (rowId == COUNTDOWN_CONDITION_INDEX && time > 0) {
-            button1.setVisibility(View.VISIBLE);
-            button2.setVisibility(View.VISIBLE);
+            minusButton.setOnClickListener(new View.OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    onClickTimeButton(row, tag, false /*down*/, rowId);
+                    tag.lines.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
+                }
+            });
+
+            plusButton.setOnClickListener(new View.OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    onClickTimeButton(row, tag, true /*up*/, rowId);
+                    tag.lines.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
+                }
+            });
             if (mBucketIndex > -1) {
-                button1.setEnabled(mBucketIndex > 0);
-                button2.setEnabled(mBucketIndex < MINUTE_BUCKETS.length - 1);
+                minusButton.setEnabled(mBucketIndex > 0);
+                plusButton.setEnabled(mBucketIndex < MINUTE_BUCKETS.length - 1);
             } else {
                 final long span = time - System.currentTimeMillis();
-                button1.setEnabled(span > MIN_BUCKET_MINUTES * MINUTES_MS);
+                minusButton.setEnabled(span > MIN_BUCKET_MINUTES * MINUTES_MS);
                 final Condition maxCondition = ZenModeConfig.toTimeCondition(mContext,
                         MAX_BUCKET_MINUTES, ActivityManager.getCurrentUser());
-                button2.setEnabled(!Objects.equals(condition.summary, maxCondition.summary));
+                plusButton.setEnabled(!Objects.equals(condition.summary, maxCondition.summary));
             }
 
-            button1.setAlpha(button1.isEnabled() ? 1f : .5f);
-            button2.setAlpha(button2.isEnabled() ? 1f : .5f);
+            minusButton.setAlpha(minusButton.isEnabled() ? 1f : .5f);
+            plusButton.setAlpha(plusButton.isEnabled() ? 1f : .5f);
         } else {
-            button1.setVisibility(View.GONE);
-            button2.setVisibility(View.GONE);
+            if (minusButton != null) {
+                ((ViewGroup) row).removeView(minusButton);
+            }
+
+            if (plusButton != null) {
+                ((ViewGroup) row).removeView(plusButton);
+            }
         }
     }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java
index c713d78..d7e76a1 100644
--- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java
@@ -133,6 +133,35 @@
         }
     }
 
+    /**
+     * Fetches initial state as if a WifiManager.NETWORK_STATE_CHANGED_ACTION have been received.
+     * This replaces the dependency on the initial sticky broadcast.
+     */
+    public void fetchInitialState() {
+        if (mWifiManager == null) {
+            return;
+        }
+        updateWifiState();
+        final NetworkInfo networkInfo =
+                mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
+        connected = networkInfo != null && networkInfo.isConnected();
+        mWifiInfo = null;
+        ssid = null;
+        if (connected) {
+            mWifiInfo = mWifiManager.getConnectionInfo();
+            if (mWifiInfo != null) {
+                if (mWifiInfo.isPasspointAp() || mWifiInfo.isOsuAp()) {
+                    ssid = mWifiInfo.getPasspointProviderFriendlyName();
+                } else {
+                    ssid = getValidSsid(mWifiInfo);
+                }
+                updateRssi(mWifiInfo.getRssi());
+                maybeRequestNetworkScore();
+            }
+        }
+        updateStatusLabel();
+    }
+
     public void handleBroadcast(Intent intent) {
         if (mWifiManager == null) {
             return;
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java
index 4b08387..db05b76 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java
@@ -15,6 +15,10 @@
  */
 package com.android.settingslib.media;
 
+import static android.media.MediaRoute2Info.TYPE_BLUETOOTH_A2DP;
+import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER;
+import static android.media.MediaRoute2Info.TYPE_REMOTE_SPEAKER;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.Mockito.verify;
@@ -144,12 +148,19 @@
         when(mCachedDevice2.isConnected()).thenReturn(true);
         when(mCachedDevice3.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
         when(mCachedDevice3.isConnected()).thenReturn(true);
+        when(mBluetoothRouteInfo1.getType()).thenReturn(TYPE_BLUETOOTH_A2DP);
+        when(mBluetoothRouteInfo2.getType()).thenReturn(TYPE_BLUETOOTH_A2DP);
+        when(mBluetoothRouteInfo3.getType()).thenReturn(TYPE_BLUETOOTH_A2DP);
         when(mRouteInfo1.getId()).thenReturn(ROUTER_ID_1);
         when(mRouteInfo2.getId()).thenReturn(ROUTER_ID_2);
         when(mRouteInfo3.getId()).thenReturn(ROUTER_ID_3);
         when(mRouteInfo1.getName()).thenReturn(DEVICE_NAME_1);
         when(mRouteInfo2.getName()).thenReturn(DEVICE_NAME_2);
         when(mRouteInfo3.getName()).thenReturn(DEVICE_NAME_3);
+        when(mRouteInfo1.getType()).thenReturn(TYPE_REMOTE_SPEAKER);
+        when(mRouteInfo2.getType()).thenReturn(TYPE_REMOTE_SPEAKER);
+        when(mRouteInfo3.getType()).thenReturn(TYPE_REMOTE_SPEAKER);
+        when(mPhoneRouteInfo.getType()).thenReturn(TYPE_BUILTIN_SPEAKER);
         when(mLocalBluetoothManager.getProfileManager()).thenReturn(mProfileManager);
         when(mProfileManager.getA2dpProfile()).thenReturn(mA2dpProfile);
         when(mProfileManager.getHearingAidProfile()).thenReturn(mHapProfile);
@@ -271,12 +282,12 @@
 
     @Test
     public void compareTo_info_bluetooth_infoFirst() {
-        mMediaDevices.add(mBluetoothMediaDevice1);
         mMediaDevices.add(mInfoMediaDevice1);
+        mMediaDevices.add(mBluetoothMediaDevice1);
 
-        assertThat(mMediaDevices.get(0)).isEqualTo(mBluetoothMediaDevice1);
-        Collections.sort(mMediaDevices, COMPARATOR);
         assertThat(mMediaDevices.get(0)).isEqualTo(mInfoMediaDevice1);
+        Collections.sort(mMediaDevices, COMPARATOR);
+        assertThat(mMediaDevices.get(0)).isEqualTo(mBluetoothMediaDevice1);
     }
 
     @Test
@@ -327,7 +338,7 @@
     // 5.mBluetoothMediaDevice2: * 2 times usage
     // 6.mBluetoothMediaDevice3: * 1 time usage
     // 7.mPhoneMediaDevice:      * 0 time usage
-    // Order: 7 -> 2 -> 1 -> 3 -> 5 -> 4 -> 6
+    // Order: 7 -> 2 -> 1 -> 5 -> 3 -> 6 -> 4
     @Test
     public void compareTo_mixedDevices_carKitFirst() {
         when(mDevice1.getBluetoothClass()).thenReturn(mCarkitClass);
@@ -352,10 +363,10 @@
         assertThat(mMediaDevices.get(0)).isEqualTo(mPhoneMediaDevice);
         assertThat(mMediaDevices.get(1)).isEqualTo(mBluetoothMediaDevice1);
         assertThat(mMediaDevices.get(2)).isEqualTo(mInfoMediaDevice1);
-        assertThat(mMediaDevices.get(3)).isEqualTo(mInfoMediaDevice2);
-        assertThat(mMediaDevices.get(4)).isEqualTo(mBluetoothMediaDevice2);
-        assertThat(mMediaDevices.get(5)).isEqualTo(mInfoMediaDevice3);
-        assertThat(mMediaDevices.get(6)).isEqualTo(mBluetoothMediaDevice3);
+        assertThat(mMediaDevices.get(3)).isEqualTo(mBluetoothMediaDevice2);
+        assertThat(mMediaDevices.get(4)).isEqualTo(mInfoMediaDevice2);
+        assertThat(mMediaDevices.get(5)).isEqualTo(mBluetoothMediaDevice3);
+        assertThat(mMediaDevices.get(6)).isEqualTo(mInfoMediaDevice3);
     }
 
     // 1.mInfoMediaDevice1:      Last Selected device
@@ -365,7 +376,7 @@
     // 5.mBluetoothMediaDevice2: * 4 times usage not connected
     // 6.mBluetoothMediaDevice3: * 1 time usage
     // 7.mPhoneMediaDevice:      * 0 time usage
-    // Order: 7 -> 1 -> 3 -> 4 -> 6 -> 2 -> 5
+    // Order: 7 -> 1 -> 3 -> 6 -> 4  -> 2 -> 5
     @Test
     public void compareTo_mixedDevices_connectDeviceFirst() {
         when(mDevice1.getBluetoothClass()).thenReturn(mCarkitClass);
@@ -394,8 +405,8 @@
         assertThat(mMediaDevices.get(0)).isEqualTo(mPhoneMediaDevice);
         assertThat(mMediaDevices.get(1)).isEqualTo(mInfoMediaDevice1);
         assertThat(mMediaDevices.get(2)).isEqualTo(mInfoMediaDevice2);
-        assertThat(mMediaDevices.get(3)).isEqualTo(mInfoMediaDevice3);
-        assertThat(mMediaDevices.get(4)).isEqualTo(mBluetoothMediaDevice3);
+        assertThat(mMediaDevices.get(3)).isEqualTo(mBluetoothMediaDevice3);
+        assertThat(mMediaDevices.get(4)).isEqualTo(mInfoMediaDevice3);
         assertThat(mMediaDevices.get(5)).isEqualTo(mBluetoothMediaDevice1);
         assertThat(mMediaDevices.get(6)).isEqualTo(mBluetoothMediaDevice2);
     }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/utils/PowerUtilTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/utils/PowerUtilTest.java
index 342d127a..4b779ac 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/utils/PowerUtilTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/utils/PowerUtilTest.java
@@ -137,9 +137,9 @@
                 true /* basedOnUsage */);
 
         // shortened string should not have percentage
-        assertThat(info).isEqualTo("Less than 15 min remaining");
+        assertThat(info).isEqualTo("Less than 15 min left");
         // Add percentage to string when provided
-        assertThat(info2).isEqualTo("Less than 15 min remaining (10%)");
+        assertThat(info2).isEqualTo("Less than 15 min left (10%)");
     }
 
     @Test
@@ -171,9 +171,9 @@
                 true /* basedOnUsage */);
 
         // shortened string should not have percentage
-        assertThat(info).isEqualTo("More than 2 days remaining");
+        assertThat(info).isEqualTo("More than 2 days left");
         // Add percentage to string when provided
-        assertThat(info2).isEqualTo("More than 2 days remaining (10%)");
+        assertThat(info2).isEqualTo("More than 2 days left (10%)");
     }
 
     @Test
@@ -181,7 +181,7 @@
         String info = PowerUtil.getBatteryTipStringFormatted(mContext,
                 THREE_DAYS_MILLIS);
 
-        assertThat(info).isEqualTo("More than 3 days remaining");
+        assertThat(info).isEqualTo("More than 3 days left");
     }
 
     @Test
diff --git a/packages/SystemUI/res/drawable/bubble_manage_menu_row.xml b/packages/SystemUI/res/drawable/bubble_manage_menu_row.xml
new file mode 100644
index 0000000..a793680
--- /dev/null
+++ b/packages/SystemUI/res/drawable/bubble_manage_menu_row.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_pressed="true">
+        <ripple android:color="#99999999" />
+    </item>
+</selector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/screenshot_cancel.xml b/packages/SystemUI/res/drawable/screenshot_cancel.xml
index be3c598..f0dfd21 100644
--- a/packages/SystemUI/res/drawable/screenshot_cancel.xml
+++ b/packages/SystemUI/res/drawable/screenshot_cancel.xml
@@ -20,9 +20,9 @@
         android:viewportWidth="48.0"
         android:viewportHeight="48.0">
     <path
-        android:pathData="M24,24m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
-        android:fillColor="@android:color/white"/>
+        android:fillColor="@color/global_screenshot_dismiss_background"
+        android:pathData="M24,24m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"/>
     <path
-        android:fillColor="@color/GM2_grey_500"
+        android:fillColor="@color/global_screenshot_dismiss_foreground"
         android:pathData="M31,18.41L29.59,17 24,22.59 18.41,17 17,18.41 22.59,24 17,29.59 18.41,31 24,25.41 29.59,31 31,29.59 25.41,24z"/>
 </vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/bubble_manage_menu.xml b/packages/SystemUI/res/layout/bubble_manage_menu.xml
new file mode 100644
index 0000000..129282d
--- /dev/null
+++ b/packages/SystemUI/res/layout/bubble_manage_menu.xml
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:background="@drawable/rounded_bg_full"
+    android:elevation="@dimen/bubble_manage_menu_elevation"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:id="@+id/bubble_manage_menu_dismiss_container"
+        android:background="@drawable/bubble_manage_menu_row"
+        android:layout_width="match_parent"
+        android:layout_height="48dp"
+        android:gravity="center_vertical"
+        android:paddingStart="16dp"
+        android:paddingEnd="16dp"
+        android:orientation="horizontal">
+
+        <ImageView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:src="@drawable/ic_remove_no_shadow"
+            android:tint="@color/global_actions_text"/>
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="16dp"
+            android:textAppearance="@*android:style/TextAppearance.DeviceDefault"
+            android:text="@string/bubble_dismiss_text" />
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/bubble_manage_menu_dont_bubble_container"
+        android:background="@drawable/bubble_manage_menu_row"
+        android:layout_width="match_parent"
+        android:layout_height="48dp"
+        android:gravity="center_vertical"
+        android:paddingStart="16dp"
+        android:paddingEnd="16dp"
+        android:orientation="horizontal">
+
+        <ImageView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:src="@drawable/ic_stop_bubble"
+            android:tint="@color/global_actions_text"/>
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="16dp"
+            android:textAppearance="@*android:style/TextAppearance.DeviceDefault"
+            android:text="@string/bubbles_dont_bubble_conversation" />
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/bubble_manage_menu_settings_container"
+        android:background="@drawable/bubble_manage_menu_row"
+        android:layout_width="match_parent"
+        android:layout_height="48dp"
+        android:gravity="center_vertical"
+        android:paddingStart="16dp"
+        android:paddingEnd="16dp"
+        android:orientation="horizontal">
+
+        <ImageView
+            android:id="@+id/bubble_manage_menu_settings_icon"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:src="@drawable/ic_remove_no_shadow"/>
+
+        <TextView
+            android:id="@+id/bubble_manage_menu_settings_name"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="16dp"
+            android:textAppearance="@*android:style/TextAppearance.DeviceDefault" />
+
+    </LinearLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/controls_management.xml b/packages/SystemUI/res/layout/controls_management.xml
index ae57563..835e54e 100644
--- a/packages/SystemUI/res/layout/controls_management.xml
+++ b/packages/SystemUI/res/layout/controls_management.xml
@@ -17,6 +17,7 @@
 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/controls_management_root"
     android:orientation="vertical"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
@@ -25,41 +26,15 @@
     android:paddingStart="@dimen/controls_management_side_padding"
     android:paddingEnd="@dimen/controls_management_side_padding" >
 
-    <LinearLayout
-        android:orientation="horizontal"
+
+    <TextView
+        android:id="@+id/title"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:focusable="false"
-        android:clickable="false"
-        android:gravity="center_vertical">
+        android:textAppearance="?android:attr/textAppearanceLarge"
+        android:textSize="@dimen/controls_title_size"
+        android:textAlignment="center" />
 
-        <FrameLayout
-            android:id="@+id/icon_frame"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:gravity="start|center_vertical"
-            android:minWidth="56dp"
-            android:visibility="gone"
-            android:paddingTop="@dimen/controls_app_icon_frame_top_padding"
-            android:paddingBottom="@dimen/controls_app_icon_frame_bottom_padding"
-            android:paddingEnd="@dimen/controls_app_icon_frame_side_padding"
-            android:paddingStart="@dimen/controls_app_icon_frame_side_padding" >
-
-            <ImageView
-                android:id="@android:id/icon"
-                android:layout_width="@dimen/controls_app_icon_size"
-                android:layout_height="@dimen/controls_app_icon_size" />
-        </FrameLayout>
-
-        <TextView
-            android:id="@+id/title"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:textAppearance="?android:attr/textAppearanceLarge"
-            android:textSize="@dimen/controls_title_size"
-            android:textAlignment="center" />
-
-    </LinearLayout>
 
 
     <TextView
diff --git a/packages/SystemUI/res/layout/global_screenshot.xml b/packages/SystemUI/res/layout/global_screenshot.xml
index d506e7e..db109fe 100644
--- a/packages/SystemUI/res/layout/global_screenshot.xml
+++ b/packages/SystemUI/res/layout/global_screenshot.xml
@@ -68,6 +68,7 @@
         android:visibility="gone"
         android:contentDescription="@string/screenshot_dismiss_ui_description">
         <ImageView
+            android:id="@+id/global_screenshot_dismiss_image"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:layout_margin="@dimen/screenshot_dismiss_button_margin"
diff --git a/packages/SystemUI/res/values-night/colors.xml b/packages/SystemUI/res/values-night/colors.xml
index 93aa270..2d51011 100644
--- a/packages/SystemUI/res/values-night/colors.xml
+++ b/packages/SystemUI/res/values-night/colors.xml
@@ -72,9 +72,14 @@
     <color name="global_actions_alert_text">@color/GM2_red_300</color>
 
     <!-- Global screenshot actions -->
-    <color name="global_screenshot_button_background">@color/GM2_grey_900</color>
+    <color name="global_screenshot_button_background">@color/GM2_grey_800</color>
     <color name="global_screenshot_button_ripple">#42FFFFFF</color>
-    <color name="global_screenshot_button_text">@color/GM2_blue_300</color>
+    <color name="global_screenshot_button_text">#FFFFFF</color>
+    <color name="global_screenshot_button_border">@color/GM2_grey_600</color>
+    <color name="global_screenshot_button_icon">@color/GM2_blue_300</color>
+    <color name="global_screenshot_dismiss_background">@color/GM2_grey_800</color>
+    <color name="global_screenshot_dismiss_foreground">#FFFFFF</color>
+
 
     <!-- Biometric dialog colors -->
     <color name="biometric_dialog_gray">#ff888888</color>
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index 4482cda..8b6b5f6 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -198,6 +198,8 @@
     <color name="global_screenshot_button_border">@color/GM2_grey_300</color>
     <color name="global_screenshot_button_ripple">#1f000000</color>
     <color name="global_screenshot_button_icon">@color/GM2_blue_500</color>
+    <color name="global_screenshot_dismiss_background">#FFFFFF</color>
+    <color name="global_screenshot_dismiss_foreground">@color/GM2_grey_500</color>
 
     <!-- GM2 colors -->
     <color name="GM2_grey_50">#F8F9FA</color>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 599ed16..179f8b8 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1014,6 +1014,9 @@
     <!-- Margins at the left and right of the power menu and home controls widgets. -->
     <dimen name="global_actions_side_margin">16dp</dimen>
 
+    <!-- Amount to shift the layout when exiting/entering for controls activities -->
+    <dimen name="global_actions_controls_y_translation">20dp</dimen>
+
     <!-- The maximum offset in either direction that elements are moved horizontally to prevent
          burn-in on AOD. -->
     <dimen name="burn_in_prevention_offset_x">8dp</dimen>
@@ -1197,6 +1200,7 @@
          snap to the dismiss target. -->
     <dimen name="bubble_dismiss_target_padding_x">40dp</dimen>
     <dimen name="bubble_dismiss_target_padding_y">20dp</dimen>
+    <dimen name="bubble_manage_menu_elevation">4dp</dimen>
 
     <dimen name="dismiss_circle_size">52dp</dimen>
     <dimen name="dismiss_target_x_size">24dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index cb20e7a..d639ed0 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2629,6 +2629,8 @@
     <string name="bubbles_user_education_manage">Tap Manage to turn off bubbles from this app</string>
     <!-- Button text for dismissing the bubble "manage" button tool tip  [CHAR LIMIT=20]-->
     <string name="bubbles_user_education_got_it">Got it</string>
+    <!-- Label for the button that takes the user to the notification settings for the given app. -->
+    <string name="bubbles_app_settings"><xliff:g id="notification_title" example="Android Messages">%1$s</xliff:g> settings</string>
 
     <!-- Notification content text when the system navigation mode changes as a result of changing the default launcher [CHAR LIMIT=NONE] -->
     <string name="notification_content_system_nav_changed">System navigation updated. To make changes, go to Settings.</string>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 7e24f5d..4ed819e 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -663,8 +663,13 @@
 
     <!-- Controls styles -->
     <style name="Theme.ControlsManagement" parent="@android:style/Theme.DeviceDefault.NoActionBar">
+        <item name="android:windowActivityTransitions">true</item>
+        <item name="android:windowContentTransitions">false</item>
         <item name="android:windowIsTranslucent">false</item>
-        <item name="wallpaperTextColor">@*android:color/primary_text_material_dark</item>
+        <item name="android:windowBackground">@android:color/black</item>
+        <item name="android:colorBackground">@android:color/black</item>
+        <item name="android:windowAnimationStyle">@null</item>
+        <item name="android:statusBarColor">@*android:color/transparent</item>
     </style>
 
     <style name="TextAppearance.Control">
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java
index aa2fe3c..57b3761 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java
@@ -16,7 +16,6 @@
 
 package com.android.keyguard;
 
-import android.app.Activity;
 import android.app.ActivityManager;
 import android.content.Context;
 import android.content.res.ColorStateList;
@@ -31,6 +30,8 @@
 import android.view.KeyEvent;
 import android.widget.FrameLayout;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.internal.widget.LockPatternUtils;
 import com.android.keyguard.KeyguardSecurityContainer.SecurityCallback;
 import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
@@ -101,7 +102,8 @@
     public static final boolean DEBUG = KeyguardConstants.DEBUG;
     private static final String TAG = "KeyguardViewBase";
 
-    private KeyguardSecurityContainer mSecurityContainer;
+    @VisibleForTesting
+    protected KeyguardSecurityContainer mSecurityContainer;
 
     public KeyguardHostView(Context context) {
         this(context, null);
@@ -446,4 +448,11 @@
     public SecurityMode getCurrentSecurityMode() {
         return mSecurityContainer.getCurrentSecurityMode();
     }
+
+    /**
+     * When bouncer was visible and is starting to become hidden.
+     */
+    public void onStartingToHide() {
+        mSecurityContainer.onStartingToHide();
+    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
index 718bcf1..65bf7e6 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
@@ -152,6 +152,11 @@
         mImm.hideSoftInputFromWindow(getWindowToken(), 0);
     }
 
+    @Override
+    public void onStartingToHide() {
+        mImm.hideSoftInputFromWindow(getWindowToken(), 0);
+    }
+
     private void updateSwitchImeButton() {
         // If there's more than one IME, enable the IME switcher button
         final boolean wasVisible = mSwitchImeButton.getVisibility() == View.VISIBLE;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index 502c078..9cfcc52 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -227,6 +227,13 @@
     }
 
     @Override
+    public void onStartingToHide() {
+        if (mCurrentSecuritySelection != SecurityMode.None) {
+            getSecurityView(mCurrentSecuritySelection).onStartingToHide();
+        }
+    }
+
+    @Override
     public boolean shouldDelayChildPressedState() {
         return true;
     }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java
index 20b1e0d..43cef3a 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java
@@ -159,4 +159,9 @@
     default boolean disallowInterceptTouch(MotionEvent event) {
         return false;
     }
+
+    /**
+     * When bouncer was visible but is being dragged down or dismissed.
+     */
+    default void onStartingToHide() {};
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 367058f..a96ef91 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -1301,6 +1301,7 @@
     private FingerprintManager mFpm;
     private FaceManager mFaceManager;
     private boolean mFingerprintLockedOut;
+    private TelephonyManager mTelephonyManager;
 
     /**
      * When we receive a
@@ -1728,10 +1729,22 @@
         }
         updateAirplaneModeState();
 
-        TelephonyManager telephony =
+        mTelephonyManager =
                 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
-        if (telephony != null) {
-            telephony.listen(mPhoneStateListener, LISTEN_ACTIVE_DATA_SUBSCRIPTION_ID_CHANGE);
+        if (mTelephonyManager != null) {
+            mTelephonyManager.listen(mPhoneStateListener,
+                    LISTEN_ACTIVE_DATA_SUBSCRIPTION_ID_CHANGE);
+            // Set initial sim states values.
+            for (int slot = 0; slot < mTelephonyManager.getActiveModemCount(); slot++) {
+                int state = mTelephonyManager.getSimState(slot);
+                int[] subIds = mSubscriptionManager.getSubscriptionIds(slot);
+                if (subIds != null) {
+                    for (int subId : subIds) {
+                        mHandler.obtainMessage(MSG_SIM_STATE_CHANGE, subId, slot, state)
+                                .sendToTarget();
+                    }
+                }
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java b/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java
index 409ae3f..c92174a 100644
--- a/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java
+++ b/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java
@@ -164,7 +164,9 @@
                 currentDrawPosition = getPaddingLeft();
             }
         } else {
-            currentDrawPosition = getWidth() / 2 - totalDrawingWidth / 2;
+            float maxRight = getWidth() - getPaddingRight() - totalDrawingWidth;
+            float center = getWidth() / 2f - totalDrawingWidth / 2f;
+            currentDrawPosition = center > 0 ? center : maxRight;
         }
         int length = mTextChars.size();
         Rect bounds = getCharBounds();
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
index 7262f8c..1f27ae2 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.accessibility;
 
+import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_GLOBAL_ACTIONS;
+
 import android.accessibilityservice.AccessibilityService;
 import android.app.PendingIntent;
 import android.app.RemoteAction;
@@ -282,8 +284,8 @@
 
     private void handleTakeScreenshot() {
         ScreenshotHelper screenshotHelper = new ScreenshotHelper(mContext);
-        screenshotHelper.takeScreenshot(WindowManager.TAKE_SCREENSHOT_FULLSCREEN,
-                true, true, new Handler(Looper.getMainLooper()), null);
+        screenshotHelper.takeScreenshot(WindowManager.TAKE_SCREENSHOT_FULLSCREEN, true, true,
+                SCREENSHOT_GLOBAL_ACTIONS, new Handler(Looper.getMainLooper()), null);
     }
 
     private void handleAccessibilityMenu() {
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
index 71f2bc0..38bfffb 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
@@ -90,6 +90,7 @@
     }
 
     private FlyoutMessage mFlyoutMessage;
+    private Drawable mBadgedAppIcon;
     private Bitmap mBadgedImage;
     private int mDotColor;
     private Path mDotPath;
@@ -133,6 +134,10 @@
         return mBadgedImage;
     }
 
+    public Drawable getBadgedAppIcon() {
+        return mBadgedAppIcon;
+    }
+
     @Override
     public int getDotColor() {
         return mDotColor;
@@ -239,6 +244,7 @@
         mAppName = info.appName;
         mFlyoutMessage = info.flyoutMessage;
 
+        mBadgedAppIcon = info.badgedAppIcon;
         mBadgedImage = info.badgedBubbleImage;
         mDotColor = info.dotColor;
         mDotPath = info.dotPath;
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index c6883c8..e488cf2 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -605,6 +605,9 @@
             if (mExpandListener != null) {
                 mStackView.setExpandListener(mExpandListener);
             }
+
+            mStackView.setUnbubbleConversationCallback(notificationEntry ->
+                    onUserChangedBubble(notificationEntry, false /* shouldBubble */));
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
index 3524696..bb23655 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
@@ -43,7 +43,6 @@
 import android.graphics.Rect;
 import android.graphics.drawable.ShapeDrawable;
 import android.os.RemoteException;
-import android.service.notification.StatusBarNotification;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.View;
@@ -55,14 +54,13 @@
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
 import com.android.systemui.recents.TriangleShape;
-import com.android.systemui.shared.system.SysUiStatsLog;
 import com.android.systemui.statusbar.AlphaOptimizedButton;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 
 /**
  * Container for the expanded bubble view, handles rendering the caret and settings icon.
  */
-public class BubbleExpandedView extends LinearLayout implements View.OnClickListener {
+public class BubbleExpandedView extends LinearLayout {
     private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleExpandedView" : TAG_BUBBLES;
 
     private enum ActivityViewStatus {
@@ -100,9 +98,6 @@
     private int mPointerWidth;
     private int mPointerHeight;
     private ShapeDrawable mPointerDrawable;
-    private Rect mTempRect = new Rect();
-    private int[] mTempLoc = new int[2];
-    private int mExpandedViewTouchSlop;
 
     @Nullable private Bubble mBubble;
 
@@ -224,7 +219,6 @@
         mMinHeight = res.getDimensionPixelSize(R.dimen.bubble_expanded_default_height);
         mOverflowHeight = res.getDimensionPixelSize(R.dimen.bubble_overflow_height);
         mPointerMargin = res.getDimensionPixelSize(R.dimen.bubble_pointer_margin);
-        mExpandedViewTouchSlop = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_slop);
     }
 
     @Override
@@ -239,7 +233,6 @@
         mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width);
         mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
 
-
         mPointerDrawable = new ShapeDrawable(TriangleShape.create(
                 mPointerWidth, mPointerHeight, true /* pointUp */));
         mPointerView.setBackground(mPointerDrawable);
@@ -248,7 +241,6 @@
         mSettingsIconHeight = getContext().getResources().getDimensionPixelSize(
                 R.dimen.bubble_manage_button_height);
         mSettingsIcon = findViewById(R.id.settings_button);
-        mSettingsIcon.setOnClickListener(this);
 
         mActivityView = new ActivityView(mContext, null /* attrs */, 0 /* defStyle */,
                 true /* singleTaskInstance */);
@@ -289,6 +281,19 @@
         return mBubble != null ? mBubble.getEntry() : null;
     }
 
+    void setManageClickListener(OnClickListener manageClickListener) {
+        findViewById(R.id.settings_button).setOnClickListener(manageClickListener);
+    }
+
+    /**
+     * Updates the ActivityView's obscured touchable region. This calls onLocationChanged, which
+     * results in a call to {@link BubbleStackView#subtractObscuredTouchableRegion}. This is useful
+     * if a view has been added or removed from on top of the ActivityView, such as the manage menu.
+     */
+    void updateObscuredTouchableRegion() {
+        mActivityView.onLocationChanged();
+    }
+
     void applyThemeAttrs() {
         final TypedArray ta = mContext.obtainStyledAttributes(
                 new int[] {
@@ -473,51 +478,6 @@
     }
 
     /**
-     * Whether the provided x, y values (in raw coordinates) are in a touchable area of the
-     * expanded view.
-     *
-     * The touchable areas are the ActivityView (plus some slop around it) and the manage button.
-     */
-    boolean intersectingTouchableContent(int rawX, int rawY) {
-        mTempRect.setEmpty();
-        if (mActivityView != null) {
-            mTempLoc = mActivityView.getLocationOnScreen();
-            mTempRect.set(mTempLoc[0] - mExpandedViewTouchSlop,
-                    mTempLoc[1] - mExpandedViewTouchSlop,
-                    mTempLoc[0] + mActivityView.getWidth() + mExpandedViewTouchSlop,
-                    mTempLoc[1] + mActivityView.getHeight() + mExpandedViewTouchSlop);
-        }
-        if (mTempRect.contains(rawX, rawY)) {
-            return true;
-        }
-        mTempLoc = mSettingsIcon.getLocationOnScreen();
-        mTempRect.set(mTempLoc[0],
-                mTempLoc[1],
-                mTempLoc[0] + mSettingsIcon.getWidth(),
-                mTempLoc[1] + mSettingsIcon.getHeight());
-        if (mTempRect.contains(rawX, rawY)) {
-            return true;
-        }
-        return false;
-    }
-
-    @Override
-    public void onClick(View view) {
-        if (mBubble == null) {
-            return;
-        }
-        int id = view.getId();
-        if (id == R.id.settings_button) {
-            Intent intent = mBubble.getSettingsIntent();
-            mStackView.collapseStack(() -> {
-                mContext.startActivityAsUser(intent, mBubble.getEntry().getSbn().getUser());
-                logBubbleClickEvent(mBubble,
-                        SysUiStatsLog.BUBBLE_UICHANGED__ACTION__HEADER_GO_TO_SETTINGS);
-            });
-        }
-    }
-
-    /**
      * Update appearance of the expanded view being displayed.
      */
     public void updateView() {
@@ -547,10 +507,8 @@
      * Position of the manage button displayed in the expanded view. Used for placing user
      * education about the manage button.
      */
-    public Rect getManageButtonLocationOnScreen() {
-        mTempLoc = mSettingsIcon.getLocationOnScreen();
-        return new Rect(mTempLoc[0], mTempLoc[1], mTempLoc[0] + mSettingsIcon.getWidth(),
-                mTempLoc[1] + mSettingsIcon.getHeight());
+    public void getManageButtonBoundsOnScreen(Rect rect) {
+        mSettingsIcon.getBoundsOnScreen(rect);
     }
 
     /**
@@ -611,26 +569,4 @@
         }
         return INVALID_DISPLAY;
     }
-
-    /**
-     * Logs bubble UI click event.
-     *
-     * @param bubble the bubble notification entry that user is interacting with.
-     * @param action the user interaction enum.
-     */
-    private void logBubbleClickEvent(Bubble bubble, int action) {
-        StatusBarNotification notification = bubble.getEntry().getSbn();
-        SysUiStatsLog.write(SysUiStatsLog.BUBBLE_UI_CHANGED,
-                notification.getPackageName(),
-                notification.getNotification().getChannelId(),
-                notification.getId(),
-                mStackView.getBubbleIndex(mStackView.getExpandedBubble()),
-                mStackView.getBubbleCount(),
-                action,
-                mStackView.getNormalizedXPosition(),
-                mStackView.getNormalizedYPosition(),
-                bubble.showInShade(),
-                bubble.isOngoing(),
-                false /* isAppForeground (unused) */);
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index 0aabdff..c906931 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -33,12 +33,14 @@
 import android.annotation.SuppressLint;
 import android.app.Notification;
 import android.content.Context;
+import android.content.Intent;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.graphics.Color;
 import android.graphics.ColorMatrix;
 import android.graphics.ColorMatrixColorFilter;
+import android.graphics.Outline;
 import android.graphics.Paint;
 import android.graphics.Point;
 import android.graphics.PointF;
@@ -47,6 +49,7 @@
 import android.graphics.Region;
 import android.os.Bundle;
 import android.os.Vibrator;
+import android.service.notification.StatusBarNotification;
 import android.util.Log;
 import android.view.Choreographer;
 import android.view.DisplayCutout;
@@ -55,6 +58,7 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.ViewOutlineProvider;
 import android.view.ViewTreeObserver;
 import android.view.WindowInsets;
 import android.view.WindowManager;
@@ -62,6 +66,7 @@
 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
 import android.view.animation.AccelerateDecelerateInterpolator;
 import android.widget.FrameLayout;
+import android.widget.ImageView;
 import android.widget.TextView;
 
 import androidx.annotation.MainThread;
@@ -83,6 +88,7 @@
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.SysUiStatsLog;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
 import com.android.systemui.util.DismissCircleView;
 import com.android.systemui.util.FloatingContentCoordinator;
@@ -97,6 +103,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.function.Consumer;
 
 /**
  * Renders bubbles in a stack and handles animating expanded and collapsed states.
@@ -224,7 +231,7 @@
     private int mPointerHeight;
     private int mStatusBarHeight;
     private int mImeOffset;
-    private BubbleViewProvider mExpandedBubble;
+    @Nullable private BubbleViewProvider mExpandedBubble;
     private boolean mIsExpanded;
 
     /** Whether the stack is currently on the left side of the screen, or animating there. */
@@ -244,6 +251,10 @@
     }
 
     private BubbleController.BubbleExpandListener mExpandListener;
+
+    /** Callback to run when we want to unbubble the given notification's conversation. */
+    private Consumer<NotificationEntry> mUnbubbleConversationCallback;
+
     private SysUiState mSysUiState;
 
     private boolean mViewUpdatedRequested = false;
@@ -255,9 +266,7 @@
 
     private LayoutInflater mInflater;
 
-    // Used for determining view / touch intersection
-    int[] mTempLoc = new int[2];
-    RectF mTempRect = new RectF();
+    private Rect mTempRect = new Rect();
 
     private final List<Rect> mSystemGestureExclusionRects = Collections.singletonList(new Rect());
 
@@ -471,6 +480,11 @@
                 return true;
             }
 
+            // If the manage menu is visible, just hide it.
+            if (mShowingManage) {
+                showManageMenu(false /* show */);
+            }
+
             if (mBubbleData.isExpanded()) {
                 maybeShowManageEducation(false /* show */);
 
@@ -627,6 +641,13 @@
     private BubbleManageEducationView mManageEducationView;
     private boolean mAnimatingManageEducationAway;
 
+    private ViewGroup mManageMenu;
+    private ImageView mManageSettingsIcon;
+    private TextView mManageSettingsText;
+    private boolean mShowingManage = false;
+    private PhysicsAnimator.SpringConfig mManageSpringConfig = new PhysicsAnimator.SpringConfig(
+            SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY);
+
     @SuppressLint("ClickableViewAccessibility")
     public BubbleStackView(Context context, BubbleData data,
             @Nullable SurfaceSynchronizer synchronizer,
@@ -689,6 +710,8 @@
         mExpandedViewContainer.setClipChildren(false);
         addView(mExpandedViewContainer);
 
+        setUpManageMenu();
+
         setUpFlyout();
         mFlyoutTransitionSpring.setSpring(new SpringForce()
                 .setStiffness(SpringForce.STIFFNESS_LOW)
@@ -838,7 +861,9 @@
         // ActivityViews, etc.) were touched. Collapse the stack if it's expanded.
         setOnTouchListener((view, ev) -> {
             if (ev.getAction() == MotionEvent.ACTION_DOWN) {
-                if (mBubbleData.isExpanded()) {
+                if (mShowingManage) {
+                    showManageMenu(false /* show */);
+                } else if (mBubbleData.isExpanded()) {
                     mBubbleData.setExpanded(false);
                 }
             }
@@ -847,6 +872,66 @@
         });
     }
 
+    private void setUpManageMenu() {
+        if (mManageMenu != null) {
+            removeView(mManageMenu);
+        }
+
+        mManageMenu = (ViewGroup) LayoutInflater.from(getContext()).inflate(
+                R.layout.bubble_manage_menu, this, false);
+        mManageMenu.setVisibility(View.INVISIBLE);
+
+        PhysicsAnimator.getInstance(mManageMenu).setDefaultSpringConfig(mManageSpringConfig);
+
+        final TypedArray ta = mContext.obtainStyledAttributes(
+                new int[] {android.R.attr.dialogCornerRadius});
+        final int menuCornerRadius = ta.getDimensionPixelSize(0, 0);
+        ta.recycle();
+
+        mManageMenu.setOutlineProvider(new ViewOutlineProvider() {
+            @Override
+            public void getOutline(View view, Outline outline) {
+                outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), menuCornerRadius);
+            }
+        });
+        mManageMenu.setClipToOutline(true);
+
+        mManageMenu.findViewById(R.id.bubble_manage_menu_dismiss_container).setOnClickListener(
+                view -> {
+                    showManageMenu(false /* show */);
+                    dismissBubbleIfExists(mBubbleData.getSelectedBubble());
+                });
+
+        mManageMenu.findViewById(R.id.bubble_manage_menu_dont_bubble_container).setOnClickListener(
+                view -> {
+                    showManageMenu(false /* show */);
+                    final Bubble bubble = mBubbleData.getSelectedBubble();
+                    if (bubble != null && mBubbleData.hasBubbleWithKey(bubble.getKey())) {
+                        mUnbubbleConversationCallback.accept(bubble.getEntry());
+                    }
+                });
+
+        mManageMenu.findViewById(R.id.bubble_manage_menu_settings_container).setOnClickListener(
+                view -> {
+                    showManageMenu(false /* show */);
+                    final Bubble bubble = mBubbleData.getSelectedBubble();
+                    if (bubble != null && mBubbleData.hasBubbleWithKey(bubble.getKey())) {
+                        final Intent intent = bubble.getSettingsIntent();
+                        collapseStack(() -> {
+                            mContext.startActivityAsUser(
+                                    intent, bubble.getEntry().getSbn().getUser());
+                            logBubbleClickEvent(
+                                    bubble,
+                                    SysUiStatsLog.BUBBLE_UICHANGED__ACTION__HEADER_GO_TO_SETTINGS);
+                        });
+                    }
+                });
+
+        mManageSettingsIcon = mManageMenu.findViewById(R.id.bubble_manage_menu_settings_icon);
+        mManageSettingsText = mManageMenu.findViewById(R.id.bubble_manage_menu_settings_name);
+        addView(mManageMenu);
+    }
+
     private void setUpUserEducation() {
         if (mUserEducationView != null) {
             removeView(mUserEducationView);
@@ -934,6 +1019,7 @@
         setUpFlyout();
         setUpOverflow();
         setUpUserEducation();
+        setUpManageMenu();
     }
 
     /** Respond to the phone being rotated by repositioning the stack and hiding any flyouts. */
@@ -960,6 +1046,9 @@
                 Math.max(0f, Math.min(1f, mVerticalPosPercentBeforeRotation));
         addOnLayoutChangeListener(mOrientationChangedListener);
         hideFlyoutImmediate();
+
+        mManageMenu.setVisibility(View.INVISIBLE);
+        mShowingManage = false;
     }
 
     @Override
@@ -1100,6 +1189,12 @@
         mExpandListener = listener;
     }
 
+    /** Sets the function to call to un-bubble the given conversation. */
+    public void setUnbubbleConversationCallback(
+            Consumer<NotificationEntry> unbubbleConversationCallback) {
+        mUnbubbleConversationCallback = unbubbleConversationCallback;
+    }
+
     /**
      * Whether the stack of bubbles is expanded or not.
      */
@@ -1361,15 +1456,14 @@
             mManageEducationView.setAlpha(0);
             mManageEducationView.setVisibility(VISIBLE);
             mManageEducationView.post(() -> {
-                final Rect position =
-                        mExpandedBubble.getExpandedView().getManageButtonLocationOnScreen();
+                mExpandedBubble.getExpandedView().getManageButtonBoundsOnScreen(mTempRect);
                 final int viewHeight = mManageEducationView.getManageViewHeight();
                 final int inset = getResources().getDimensionPixelSize(
                         R.dimen.bubbles_manage_education_top_inset);
                 mManageEducationView.bringToFront();
-                mManageEducationView.setManageViewPosition(position.left,
-                        position.top - viewHeight + inset);
-                mManageEducationView.setPointerPosition(position.centerX() - position.left);
+                mManageEducationView.setManageViewPosition(mTempRect.left,
+                        mTempRect.top - viewHeight + inset);
+                mManageEducationView.setPointerPosition(mTempRect.centerX() - mTempRect.left);
                 mManageEducationView.animate()
                         .setDuration(ANIMATE_STACK_USER_EDUCATION_DURATION)
                         .setInterpolator(FAST_OUT_SLOW_IN).alpha(1);
@@ -1443,6 +1537,9 @@
     }
 
     private void animateCollapse() {
+        // Hide the menu if it's visible.
+        showManageMenu(false);
+
         mIsExpanded = false;
         final BubbleViewProvider previouslySelected = mExpandedBubble;
         beforeExpandedViewAnimation();
@@ -1570,9 +1667,9 @@
      */
     @Override
     public void subtractObscuredTouchableRegion(Region touchableRegion, View view) {
-        // If the notification shade is expanded, we shouldn't let the ActivityView steal any touch
-        // events from any location.
-        if (mNotificationShadeWindowController.getPanelExpanded()) {
+        // If the notification shade is expanded, or the manage menu is open, we shouldn't let the
+        // ActivityView steal any touch events from any location.
+        if (mNotificationShadeWindowController.getPanelExpanded() || mShowingManage) {
             touchableRegion.setEmpty();
         }
     }
@@ -1658,17 +1755,20 @@
     private void dismissMagnetizedObject() {
         if (mIsExpanded) {
             final View draggedOutBubbleView = (View) mMagnetizedObject.getUnderlyingObject();
-            final Bubble draggedOutBubble = mBubbleData.getBubbleWithView(draggedOutBubbleView);
+            dismissBubbleIfExists(mBubbleData.getBubbleWithView(draggedOutBubbleView));
 
-            if (mBubbleData.hasBubbleWithKey(draggedOutBubble.getKey())) {
-                mBubbleData.notificationEntryRemoved(
-                        draggedOutBubble.getEntry(), BubbleController.DISMISS_USER_GESTURE);
-            }
         } else {
             mBubbleData.dismissAll(BubbleController.DISMISS_USER_GESTURE);
         }
     }
 
+    private void dismissBubbleIfExists(@Nullable Bubble bubble) {
+        if (bubble != null && mBubbleData.hasBubbleWithKey(bubble.getKey())) {
+            mBubbleData.notificationEntryRemoved(
+                    bubble.getEntry(), BubbleController.DISMISS_USER_GESTURE);
+        }
+    }
+
     /** Prepares and starts the desaturate/darken animation on the bubble stack. */
     private void animateDesaturateAndDarken(View targetView, boolean desaturateAndDarken) {
         mDesaturateAndDarkenTargetView = targetView;
@@ -1912,6 +2012,63 @@
         invalidate();
     }
 
+    private void showManageMenu(boolean show) {
+        mShowingManage = show;
+
+        // This should not happen, since the manage menu is only visible when there's an expanded
+        // bubble. If we end up in this state, just hide the menu immediately.
+        if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) {
+            mManageMenu.setVisibility(View.INVISIBLE);
+            return;
+        }
+
+        // If available, update the manage menu's settings option with the expanded bubble's app
+        // name and icon.
+        if (show && mBubbleData.hasBubbleWithKey(mExpandedBubble.getKey())) {
+            final Bubble bubble = mBubbleData.getBubbleWithKey(mExpandedBubble.getKey());
+            mManageSettingsIcon.setImageDrawable(bubble.getBadgedAppIcon());
+            mManageSettingsText.setText(getResources().getString(
+                    R.string.bubbles_app_settings, bubble.getAppName()));
+        }
+
+        mExpandedBubble.getExpandedView().getManageButtonBoundsOnScreen(mTempRect);
+
+        // When the menu is open, it should be at these coordinates. This will make the menu's
+        // bottom left corner match up with the button's bottom left corner.
+        final float targetX = mTempRect.left;
+        final float targetY = mTempRect.bottom - mManageMenu.getHeight();
+
+        if (show) {
+            mManageMenu.setScaleX(0.5f);
+            mManageMenu.setScaleY(0.5f);
+            mManageMenu.setTranslationX(targetX - mManageMenu.getWidth() / 4);
+            mManageMenu.setTranslationY(targetY + mManageMenu.getHeight() / 4);
+            mManageMenu.setAlpha(0f);
+
+            PhysicsAnimator.getInstance(mManageMenu)
+                    .spring(DynamicAnimation.ALPHA, 1f)
+                    .spring(DynamicAnimation.SCALE_X, 1f)
+                    .spring(DynamicAnimation.SCALE_Y, 1f)
+                    .spring(DynamicAnimation.TRANSLATION_X, targetX)
+                    .spring(DynamicAnimation.TRANSLATION_Y, targetY)
+                    .start();
+
+            mManageMenu.setVisibility(View.VISIBLE);
+        } else {
+            PhysicsAnimator.getInstance(mManageMenu)
+                    .spring(DynamicAnimation.ALPHA, 0f)
+                    .spring(DynamicAnimation.SCALE_X, 0.5f)
+                    .spring(DynamicAnimation.SCALE_Y, 0.5f)
+                    .spring(DynamicAnimation.TRANSLATION_X, targetX - mManageMenu.getWidth() / 4)
+                    .spring(DynamicAnimation.TRANSLATION_Y, targetY + mManageMenu.getHeight() / 4)
+                    .withEndActions(() -> mManageMenu.setVisibility(View.INVISIBLE))
+                    .start();
+        }
+
+        // Update the AV's obscured touchable region for the new menu visibility state.
+        mExpandedBubble.getExpandedView().updateObscuredTouchableRegion();
+    }
+
     private void updateExpandedBubble() {
         if (DEBUG_BUBBLE_STACK_VIEW) {
             Log.d(TAG, "updateExpandedBubble()");
@@ -1921,6 +2078,7 @@
                 && mExpandedBubble.getExpandedView() != null) {
             BubbleExpandedView bev = mExpandedBubble.getExpandedView();
             mExpandedViewContainer.addView(bev);
+            bev.setManageClickListener((view) -> showManageMenu(!mShowingManage));
             bev.populateExpandedView();
             mExpandedViewContainer.setVisibility(VISIBLE);
             mExpandedViewContainer.setAlpha(1.0f);
@@ -2089,4 +2247,26 @@
         }
         return bubbles;
     }
+
+    /**
+     * Logs bubble UI click event.
+     *
+     * @param bubble the bubble notification entry that user is interacting with.
+     * @param action the user interaction enum.
+     */
+    private void logBubbleClickEvent(Bubble bubble, int action) {
+        StatusBarNotification notification = bubble.getEntry().getSbn();
+        SysUiStatsLog.write(SysUiStatsLog.BUBBLE_UI_CHANGED,
+                notification.getPackageName(),
+                notification.getNotification().getChannelId(),
+                notification.getId(),
+                getBubbleIndex(getExpandedBubble()),
+                getBubbleCount(),
+                action,
+                getNormalizedXPosition(),
+                getNormalizedYPosition(),
+                bubble.showInShade(),
+                bubble.isOngoing(),
+                false /* isAppForeground (unused) */);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java
index c96f9a4..8a57a73 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java
@@ -116,6 +116,7 @@
         ShortcutInfo shortcutInfo;
         String appName;
         Bitmap badgedBubbleImage;
+        Drawable badgedAppIcon;
         int dotColor;
         Path dotPath;
         Bubble.FlyoutMessage flyoutMessage;
@@ -176,6 +177,7 @@
             }
 
             BitmapInfo badgeBitmapInfo = iconFactory.getBadgeBitmap(badgedIcon);
+            info.badgedAppIcon = badgedIcon;
             info.badgedBubbleImage = iconFactory.getBubbleBitmap(bubbleDrawable,
                     badgeBitmapInfo).icon;
 
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewProvider.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewProvider.java
index ef84c73..ca3e2e2 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewProvider.java
@@ -20,15 +20,17 @@
 import android.graphics.Path;
 import android.view.View;
 
+import androidx.annotation.Nullable;
+
 /**
  * Interface to represent actual Bubbles and UI elements that act like bubbles, like BubbleOverflow.
  */
 interface BubbleViewProvider {
-    BubbleExpandedView getExpandedView();
+    @Nullable BubbleExpandedView getExpandedView();
 
     void setContentVisibility(boolean visible);
 
-    View getIconView();
+    @Nullable View getIconView();
 
     void logUIEvent(int bubbleCount, int action, float normalX, float normalY, int index);
 
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt
index 7e8fec7..e84f439 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.util.concurrency.DelayableExecutor
 import dagger.Lazy
+import java.util.concurrent.atomic.AtomicBoolean
 import javax.inject.Inject
 import javax.inject.Singleton
 
@@ -284,13 +285,19 @@
         val requestLimit: Long
     ) : IControlsSubscriber.Stub() {
         val loadedControls = ArrayList<Control>()
-        private var isTerminated = false
+        private var isTerminated = AtomicBoolean(false)
         private var _loadCancelInternal: (() -> Unit)? = null
         private lateinit var subscription: IControlsSubscription
 
+        /**
+         * Potentially cancel a subscriber. The subscriber may also have terminated, in which case
+         * the request is ignored.
+         */
         fun loadCancel() = Runnable {
-            Log.d(TAG, "Cancel load requested")
-            _loadCancelInternal?.invoke()
+            _loadCancelInternal?.let {
+                Log.d(TAG, "Canceling loadSubscribtion")
+                it.invoke()
+            }
         }
 
         override fun onSubscribe(token: IBinder, subs: IControlsSubscription) {
@@ -301,7 +308,7 @@
 
         override fun onNext(token: IBinder, c: Control) {
             backgroundExecutor.execute {
-                if (isTerminated) return@execute
+                if (isTerminated.get()) return@execute
 
                 loadedControls.add(c)
 
@@ -325,13 +332,15 @@
         }
 
         private fun maybeTerminateAndRun(postTerminateFn: Runnable) {
-            if (isTerminated) return
+            if (isTerminated.get()) return
 
-            isTerminated = true
             _loadCancelInternal = {}
             currentProvider?.cancelLoadTimeout()
 
-            backgroundExecutor.execute(postTerminateFn)
+            backgroundExecutor.execute {
+                isTerminated.compareAndSet(false, true)
+                postTerminateFn.run()
+            }
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
index 79a5b0a..bc97c10 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
@@ -52,19 +52,17 @@
      * Load all available [Control] for a given service.
      *
      * @param componentName the [ComponentName] of the [ControlsProviderService] to load from
-     * @param dataCallback a callback in which to retrieve the result.
+     * @param dataCallback a callback in which to retrieve the result
+     * @param cancelWrapper a callback to receive a [Runnable] that can be run to cancel the
+     *                      request
      */
     fun loadForComponent(
         componentName: ComponentName,
-        dataCallback: Consumer<LoadData>
+        dataCallback: Consumer<LoadData>,
+        cancelWrapper: Consumer<Runnable>
     )
 
     /**
-     * Cancels a pending load call
-     */
-    fun cancelLoad()
-
-    /**
      * Request to subscribe for favorited controls per structure
      *
      * @param structureInfo structure to limit the subscription to
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
index 5626a5d..8e88756 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
@@ -77,8 +77,6 @@
 
     private var userChanging: Boolean = true
 
-    private var loadCanceller: Runnable? = null
-
     private var seedingInProgress = false
     private val seedingCallbacks = mutableListOf<Consumer<Boolean>>()
 
@@ -276,28 +274,29 @@
 
     override fun loadForComponent(
         componentName: ComponentName,
-        dataCallback: Consumer<ControlsController.LoadData>
+        dataCallback: Consumer<ControlsController.LoadData>,
+        cancelWrapper: Consumer<Runnable>
     ) {
         if (!confirmAvailability()) {
             if (userChanging) {
                 // Try again later, userChanging should not last forever. If so, we have bigger
                 // problems. This will return a runnable that allows to cancel the delayed version,
                 // it will not be able to cancel the load if
-                loadCanceller = executor.executeDelayed(
-                        { loadForComponent(componentName, dataCallback) },
-                        USER_CHANGE_RETRY_DELAY,
-                        TimeUnit.MILLISECONDS
+                executor.executeDelayed(
+                    { loadForComponent(componentName, dataCallback, cancelWrapper) },
+                    USER_CHANGE_RETRY_DELAY,
+                    TimeUnit.MILLISECONDS
                 )
-            } else {
-                dataCallback.accept(createLoadDataObject(emptyList(), emptyList(), true))
             }
-            return
+
+            dataCallback.accept(createLoadDataObject(emptyList(), emptyList(), true))
         }
-        loadCanceller = bindingController.bindAndLoad(
+
+        cancelWrapper.accept(
+            bindingController.bindAndLoad(
                 componentName,
                 object : ControlsBindingController.LoadCallback {
                     override fun accept(controls: List<Control>) {
-                        loadCanceller = null
                         executor.execute {
                             val favoritesForComponentKeys = Favorites
                                 .getControlsForComponent(componentName).map { it.controlId }
@@ -333,7 +332,6 @@
                     }
 
                     override fun error(message: String) {
-                        loadCanceller = null
                         executor.execute {
                             val controls = Favorites.getStructuresForComponent(componentName)
                                     .flatMap { st ->
@@ -348,6 +346,7 @@
                         }
                     }
                 }
+            )
         )
     }
 
@@ -432,12 +431,6 @@
         seedingCallbacks.clear()
     }
 
-    override fun cancelLoad() {
-        loadCanceller?.let {
-            executor.execute(it)
-        }
-    }
-
     private fun createRemovedStatus(
         componentName: ComponentName,
         controlInfo: ControlInfo,
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsAnimations.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsAnimations.kt
new file mode 100644
index 0000000..4ca47d1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsAnimations.kt
@@ -0,0 +1,180 @@
+/*
+ * 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.controls.management
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.annotation.IdRes
+import android.content.Intent
+
+import android.transition.Transition
+import android.transition.TransitionValues
+import android.util.Log
+import android.view.View
+import android.view.ViewGroup
+import android.view.Window
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleObserver
+import androidx.lifecycle.OnLifecycleEvent
+
+import com.android.systemui.Interpolators
+import com.android.systemui.R
+
+import com.android.systemui.controls.ui.ControlsUiController
+
+object ControlsAnimations {
+
+    private const val ALPHA_EXIT_DURATION = 167L
+    private const val ALPHA_ENTER_DELAY = ALPHA_EXIT_DURATION
+    private const val ALPHA_ENTER_DURATION = 350L - ALPHA_ENTER_DELAY
+
+    private const val Y_TRANSLATION_EXIT_DURATION = 183L
+    private const val Y_TRANSLATION_ENTER_DELAY = Y_TRANSLATION_EXIT_DURATION - ALPHA_ENTER_DELAY
+    private const val Y_TRANSLATION_ENTER_DURATION = 400L - Y_TRANSLATION_EXIT_DURATION
+    private var translationY: Float = -1f
+
+    /**
+     * Setup an activity to handle enter/exit animations. [view] should be the root of the content.
+     * Fade and translate together.
+     */
+    fun observerForAnimations(view: ViewGroup, window: Window, intent: Intent): LifecycleObserver {
+        return object : LifecycleObserver {
+            var showAnimation = intent.getBooleanExtra(ControlsUiController.EXTRA_ANIMATE, false)
+
+            init {
+                // Must flag the parent group to move it all together, and set the initial
+                // transitionAlpha to 0.0f. This property is reserved for fade animations.
+                view.setTransitionGroup(true)
+                view.transitionAlpha = 0.0f
+
+                if (translationY == -1f) {
+                    translationY = view.context.resources.getDimensionPixelSize(
+                        R.dimen.global_actions_controls_y_translation).toFloat()
+                }
+            }
+
+            @OnLifecycleEvent(Lifecycle.Event.ON_START)
+            fun setup() {
+                with(window) {
+                    allowEnterTransitionOverlap = true
+                    enterTransition = enterWindowTransition(view.getId())
+                    exitTransition = exitWindowTransition(view.getId())
+                }
+            }
+
+            @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
+            fun enterAnimation() {
+                if (showAnimation) {
+                    ControlsAnimations.enterAnimation(view).start()
+                    showAnimation = false
+                }
+            }
+        }
+    }
+
+    fun enterAnimation(view: View): Animator {
+        Log.d(ControlsUiController.TAG, "Enter animation for $view")
+
+        view.transitionAlpha = 0.0f
+        view.alpha = 1.0f
+
+        view.translationY = translationY
+
+        val alphaAnimator = ObjectAnimator.ofFloat(view, "transitionAlpha", 0.0f, 1.0f).apply {
+            interpolator = Interpolators.DECELERATE_QUINT
+            startDelay = ALPHA_ENTER_DELAY
+            duration = ALPHA_ENTER_DURATION
+        }
+
+        val yAnimator = ObjectAnimator.ofFloat(view, "translationY", 0.0f).apply {
+            interpolator = Interpolators.DECELERATE_QUINT
+            startDelay = Y_TRANSLATION_ENTER_DURATION
+            duration = Y_TRANSLATION_ENTER_DURATION
+        }
+
+        return AnimatorSet().apply {
+            playTogether(alphaAnimator, yAnimator)
+        }
+    }
+
+    /**
+     * Properly handle animations originating from dialogs. Activity transitions require
+     * transitioning between two activities, so expose this method for dialogs to animate
+     * on exit.
+     */
+    @JvmStatic
+    fun exitAnimation(view: View, onEnd: Runnable? = null): Animator {
+        Log.d(ControlsUiController.TAG, "Exit animation for $view")
+
+        val alphaAnimator = ObjectAnimator.ofFloat(view, "transitionAlpha", 0.0f).apply {
+            interpolator = Interpolators.ACCELERATE
+            duration = ALPHA_EXIT_DURATION
+        }
+
+        view.translationY = 0.0f
+        val yAnimator = ObjectAnimator.ofFloat(view, "translationY", -translationY).apply {
+            interpolator = Interpolators.ACCELERATE
+            duration = Y_TRANSLATION_EXIT_DURATION
+        }
+
+        return AnimatorSet().apply {
+            playTogether(alphaAnimator, yAnimator)
+            onEnd?.let {
+                addListener(object : AnimatorListenerAdapter() {
+                    override fun onAnimationEnd(animation: Animator) {
+                        it.run()
+                    }
+                })
+            }
+        }
+    }
+
+    fun enterWindowTransition(@IdRes id: Int) =
+        WindowTransition({ view: View -> enterAnimation(view) }).apply {
+            addTarget(id)
+        }
+
+    fun exitWindowTransition(@IdRes id: Int) =
+        WindowTransition({ view: View -> exitAnimation(view) }).apply {
+            addTarget(id)
+        }
+}
+
+/**
+ * In order to animate, at least one property must be marked on each view that should move.
+ * Setting "item" is just a flag to indicate that it should move by the animator.
+ */
+class WindowTransition(
+    val animator: (view: View) -> Animator
+) : Transition() {
+    override fun captureStartValues(tv: TransitionValues) {
+        tv.values["item"] = 0.0f
+    }
+
+    override fun captureEndValues(tv: TransitionValues) {
+        tv.values["item"] = 1.0f
+    }
+
+    override fun createAnimator(
+        sceneRoot: ViewGroup,
+        startValues: TransitionValues?,
+        endValues: TransitionValues?
+    ): Animator? = animator(startValues!!.view)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt
index ee1ce7a..640c90d 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt
@@ -16,11 +16,12 @@
 
 package com.android.systemui.controls.management
 
-import android.app.Activity
+import android.app.ActivityOptions
 import android.content.ComponentName
 import android.content.Intent
 import android.os.Bundle
 import android.view.View
+import android.view.ViewGroup
 import android.view.ViewStub
 import android.widget.Button
 import android.widget.TextView
@@ -31,7 +32,9 @@
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.controls.controller.ControlsControllerImpl
 import com.android.systemui.controls.controller.StructureInfo
+import com.android.systemui.globalactions.GlobalActionsComponent
 import com.android.systemui.settings.CurrentUserTracker
+import com.android.systemui.util.LifecycleActivity
 import javax.inject.Inject
 
 /**
@@ -39,8 +42,9 @@
  */
 class ControlsEditingActivity @Inject constructor(
     private val controller: ControlsControllerImpl,
-    broadcastDispatcher: BroadcastDispatcher
-) : Activity() {
+    broadcastDispatcher: BroadcastDispatcher,
+    private val globalActionsComponent: GlobalActionsComponent
+) : LifecycleActivity() {
 
     companion object {
         private const val TAG = "ControlsEditingActivity"
@@ -84,14 +88,31 @@
         bindViews()
 
         bindButtons()
+    }
 
+    override fun onStart() {
+        super.onStart()
         setUpList()
 
         currentUserTracker.startTracking()
     }
 
+    override fun onStop() {
+        super.onStop()
+        currentUserTracker.stopTracking()
+    }
+
     private fun bindViews() {
         setContentView(R.layout.controls_management)
+
+        getLifecycle().addObserver(
+            ControlsAnimations.observerForAnimations(
+                requireViewById<ViewGroup>(R.id.controls_management_root),
+                window,
+                intent
+            )
+        )
+
         requireViewById<ViewStub>(R.id.stub).apply {
             layoutResource = R.layout.controls_management_editing
             inflate()
@@ -113,17 +134,26 @@
                     putExtras(this@ControlsEditingActivity.intent)
                     putExtra(ControlsFavoritingActivity.EXTRA_SINGLE_STRUCTURE, true)
                 }
-                startActivity(intent)
-                finish()
+                startActivity(intent, ActivityOptions
+                    .makeSceneTransitionAnimation(this@ControlsEditingActivity).toBundle())
             }
         }
 
+        val rootView = requireViewById<ViewGroup>(R.id.controls_management_root)
         saveButton = requireViewById<Button>(R.id.done).apply {
             isEnabled = false
             setText(R.string.save)
             setOnClickListener {
                 saveFavorites()
-                finishAffinity()
+                ControlsAnimations.exitAnimation(
+                    rootView,
+                    object : Runnable {
+                        override fun run() {
+                            finish()
+                        }
+                    }
+                ).start()
+                globalActionsComponent.handleShowGlobalActionsMenu()
             }
         }
     }
@@ -151,26 +181,38 @@
         val controls = controller.getFavoritesForStructure(component, structure)
         model = FavoritesModel(component, controls, favoritesModelCallback)
         val elevation = resources.getFloat(R.dimen.control_card_elevation)
-        val adapter = ControlAdapter(elevation)
-        val recycler = requireViewById<RecyclerView>(R.id.list)
+        val recyclerView = requireViewById<RecyclerView>(R.id.list)
+        recyclerView.alpha = 0.0f
+        val adapter = ControlAdapter(elevation).apply {
+            registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
+                var hasAnimated = false
+                override fun onChanged() {
+                    if (!hasAnimated) {
+                        hasAnimated = true
+                        ControlsAnimations.enterAnimation(recyclerView).start()
+                    }
+                }
+            })
+        }
+
         val margin = resources
                 .getDimensionPixelSize(R.dimen.controls_card_margin)
         val itemDecorator = MarginItemDecorator(margin, margin)
 
-        recycler.apply {
+        recyclerView.apply {
             this.adapter = adapter
-            layoutManager = GridLayoutManager(recycler.context, 2).apply {
+            layoutManager = GridLayoutManager(recyclerView.context, 2).apply {
                 spanSizeLookup = adapter.spanSizeLookup
             }
             addItemDecoration(itemDecorator)
         }
         adapter.changeModel(model)
         model.attachAdapter(adapter)
-        ItemTouchHelper(model.itemTouchHelperCallback).attachToRecyclerView(recycler)
+        ItemTouchHelper(model.itemTouchHelperCallback).attachToRecyclerView(recyclerView)
     }
 
     override fun onDestroy() {
         currentUserTracker.stopTracking()
         super.onDestroy()
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt
index 6f34dee..9a2ccb5 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt
@@ -16,11 +16,10 @@
 
 package com.android.systemui.controls.management
 
-import android.app.Activity
+import android.app.ActivityOptions
 import android.content.ComponentName
 import android.content.Intent
 import android.content.res.Configuration
-import android.graphics.drawable.Drawable
 import android.os.Bundle
 import android.text.TextUtils
 import android.view.Gravity
@@ -29,7 +28,6 @@
 import android.view.ViewStub
 import android.widget.Button
 import android.widget.FrameLayout
-import android.widget.ImageView
 import android.widget.TextView
 import androidx.viewpager2.widget.ViewPager2
 import com.android.systemui.Prefs
@@ -40,7 +38,9 @@
 import com.android.systemui.controls.controller.ControlsControllerImpl
 import com.android.systemui.controls.controller.StructureInfo
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.globalactions.GlobalActionsComponent
 import com.android.systemui.settings.CurrentUserTracker
+import com.android.systemui.util.LifecycleActivity
 import java.text.Collator
 import java.util.concurrent.Executor
 import java.util.function.Consumer
@@ -50,8 +50,9 @@
     @Main private val executor: Executor,
     private val controller: ControlsControllerImpl,
     private val listingController: ControlsListingController,
-    broadcastDispatcher: BroadcastDispatcher
-) : Activity() {
+    broadcastDispatcher: BroadcastDispatcher,
+    private val globalActionsComponent: GlobalActionsComponent
+) : LifecycleActivity() {
 
     companion object {
         private const val TAG = "ControlsFavoritingActivity"
@@ -73,14 +74,15 @@
     private lateinit var structurePager: ViewPager2
     private lateinit var statusText: TextView
     private lateinit var titleView: TextView
-    private lateinit var iconView: ImageView
-    private lateinit var iconFrame: View
     private lateinit var pageIndicator: ManagementPageIndicator
     private var mTooltipManager: TooltipManager? = null
     private lateinit var doneButton: View
+    private lateinit var otherAppsButton: View
     private var listOfStructures = emptyList<StructureContainer>()
 
     private lateinit var comparator: Comparator<StructureContainer>
+    private var cancelLoadRunnable: Runnable? = null
+    private var isPagerLoaded = false
 
     private val currentUserTracker = object : CurrentUserTracker(broadcastDispatcher) {
         private val startingUser = controller.currentUserId
@@ -94,17 +96,10 @@
     }
 
     private val listingCallback = object : ControlsListingController.ControlsListingCallback {
-        private var icon: Drawable? = null
 
         override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
-            val newIcon = serviceInfos.firstOrNull { it.componentName == component }?.loadIcon()
-            if (icon == newIcon) return
-            icon = newIcon
-            executor.execute {
-                if (icon != null) {
-                    iconView.setImageDrawable(icon)
-                }
-                iconFrame.visibility = if (icon != null) View.VISIBLE else View.GONE
+            if (serviceInfos.size > 1) {
+                otherAppsButton.visibility = View.VISIBLE
             }
         }
     }
@@ -115,6 +110,7 @@
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
+
         val collator = Collator.getInstance(resources.configuration.locales[0])
         comparator = compareBy(collator) { it.structureName }
         appName = intent.getCharSequenceExtra(EXTRA_APP)
@@ -122,14 +118,6 @@
         component = intent.getParcelableExtra<ComponentName>(Intent.EXTRA_COMPONENT_NAME)
 
         bindViews()
-
-        setUpPager()
-
-        loadControls()
-
-        listingController.addCallback(listingCallback)
-
-        currentUserTracker.startTracking()
     }
 
     private val controlsModelCallback = object : ControlsModel.ControlsModelCallback {
@@ -174,12 +162,17 @@
                     pageIndicator.setLocation(0f)
                     pageIndicator.visibility =
                         if (listOfStructures.size > 1) View.VISIBLE else View.GONE
+
+                    ControlsAnimations.enterAnimation(pageIndicator).start()
+                    ControlsAnimations.enterAnimation(structurePager).start()
                 }
-            })
+            }, Consumer { runnable -> cancelLoadRunnable = runnable })
         }
     }
 
     private fun setUpPager() {
+        structurePager.alpha = 0.0f
+        pageIndicator.alpha = 0.0f
         structurePager.apply {
             adapter = StructureAdapter(emptyList())
             registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
@@ -203,6 +196,15 @@
 
     private fun bindViews() {
         setContentView(R.layout.controls_management)
+
+        getLifecycle().addObserver(
+            ControlsAnimations.observerForAnimations(
+                requireViewById<ViewGroup>(R.id.controls_management_root),
+                window,
+                intent
+            )
+        )
+
         requireViewById<ViewStub>(R.id.stub).apply {
             layoutResource = R.layout.controls_management_favorites
             inflate()
@@ -259,8 +261,6 @@
         }
         requireViewById<TextView>(R.id.subtitle).text =
                 resources.getText(R.string.controls_favorite_subtitle)
-        iconView = requireViewById(com.android.internal.R.id.icon)
-        iconFrame = requireViewById(R.id.icon_frame)
         structurePager = requireViewById<ViewPager2>(R.id.structure_pager)
         structurePager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
             override fun onPageSelected(position: Int) {
@@ -272,16 +272,17 @@
     }
 
     private fun bindButtons() {
-        requireViewById<Button>(R.id.other_apps).apply {
-            visibility = View.VISIBLE
+        otherAppsButton = requireViewById<Button>(R.id.other_apps).apply {
             setOnClickListener {
                 val i = Intent()
                 i.setComponent(
                     ComponentName(context, ControlsProviderSelectorActivity::class.java))
-                context.startActivity(i)
+                startActivity(i, ActivityOptions
+                    .makeSceneTransitionAnimation(this@ControlsFavoritingActivity).toBundle())
             }
         }
 
+        val rootView = requireViewById<ViewGroup>(R.id.controls_management_root)
         doneButton = requireViewById<Button>(R.id.done).apply {
             isEnabled = false
             setOnClickListener {
@@ -292,7 +293,16 @@
                         StructureInfo(component!!, it.structureName, favoritesForStorage)
                     )
                 }
-                finishAffinity()
+
+                ControlsAnimations.exitAnimation(
+                    rootView,
+                    object : Runnable {
+                        override fun run() {
+                            finish()
+                        }
+                    }
+                ).start()
+                globalActionsComponent.handleShowGlobalActionsMenu()
             }
         }
     }
@@ -302,15 +312,39 @@
         mTooltipManager?.hide(false)
     }
 
+    override fun onStart() {
+        super.onStart()
+
+        listingController.addCallback(listingCallback)
+        currentUserTracker.startTracking()
+    }
+
+    override fun onResume() {
+        super.onResume()
+
+        // only do once, to make sure that any user changes do not get replaces if resume is called
+        // more than once
+        if (!isPagerLoaded) {
+            setUpPager()
+            loadControls()
+            isPagerLoaded = true
+        }
+    }
+
+    override fun onStop() {
+        super.onStop()
+
+        listingController.removeCallback(listingCallback)
+        currentUserTracker.stopTracking()
+    }
+
     override fun onConfigurationChanged(newConfig: Configuration) {
         super.onConfigurationChanged(newConfig)
         mTooltipManager?.hide(false)
     }
 
     override fun onDestroy() {
-        currentUserTracker.stopTracking()
-        listingController.removeCallback(listingCallback)
-        controller.cancelLoad()
+        cancelLoadRunnable?.run()
         super.onDestroy()
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt
index 3be5900..0044854 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt
@@ -16,15 +16,19 @@
 
 package com.android.systemui.controls.management
 
+import android.app.ActivityOptions
 import android.content.ComponentName
 import android.content.Intent
 import android.os.Bundle
 import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
 import android.view.ViewStub
 import android.widget.Button
 import android.widget.TextView
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
 import com.android.systemui.R
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.controls.controller.ControlsController
@@ -66,12 +70,42 @@
         super.onCreate(savedInstanceState)
 
         setContentView(R.layout.controls_management)
+
+        getLifecycle().addObserver(
+            ControlsAnimations.observerForAnimations(
+                requireViewById<ViewGroup>(R.id.controls_management_root),
+                window,
+                intent
+            )
+        )
+
         requireViewById<ViewStub>(R.id.stub).apply {
             layoutResource = R.layout.controls_management_apps
             inflate()
         }
 
         recyclerView = requireViewById(R.id.list)
+        recyclerView.layoutManager = LinearLayoutManager(applicationContext)
+
+        requireViewById<TextView>(R.id.title).apply {
+            text = resources.getText(R.string.controls_providers_title)
+        }
+
+        requireViewById<Button>(R.id.other_apps).apply {
+            visibility = View.VISIBLE
+            setText(com.android.internal.R.string.cancel)
+            setOnClickListener {
+                this@ControlsProviderSelectorActivity.finishAffinity()
+            }
+        }
+        requireViewById<View>(R.id.done).visibility = View.GONE
+    }
+
+    override fun onStart() {
+        super.onStart()
+        currentUserTracker.startTracking()
+
+        recyclerView.alpha = 0.0f
         recyclerView.adapter = AppAdapter(
                 backExecutor,
                 executor,
@@ -80,17 +114,22 @@
                 LayoutInflater.from(this),
                 ::launchFavoritingActivity,
                 FavoritesRenderer(resources, controlsController::countFavoritesForComponent),
-                resources)
-        recyclerView.layoutManager = LinearLayoutManager(applicationContext)
-
-        requireViewById<TextView>(R.id.title).text =
-                resources.getText(R.string.controls_providers_title)
-
-        requireViewById<Button>(R.id.done).setOnClickListener {
-            this@ControlsProviderSelectorActivity.finishAffinity()
+                resources).apply {
+            registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
+                var hasAnimated = false
+                override fun onChanged() {
+                    if (!hasAnimated) {
+                        hasAnimated = true
+                        ControlsAnimations.enterAnimation(recyclerView).start()
+                    }
+                }
+            })
         }
+    }
 
-        currentUserTracker.startTracking()
+    override fun onStop() {
+        super.onStop()
+        currentUserTracker.stopTracking()
     }
 
     /**
@@ -98,7 +137,7 @@
      * @param component a component name for a [ControlsProviderService]
      */
     fun launchFavoritingActivity(component: ComponentName?) {
-        backExecutor.execute {
+        executor.execute {
             component?.let {
                 val intent = Intent(applicationContext, ControlsFavoritingActivity::class.java)
                         .apply {
@@ -107,7 +146,7 @@
                     putExtra(Intent.EXTRA_COMPONENT_NAME, it)
                     flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
                 }
-                startActivity(intent)
+                startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this).toBundle())
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt
index 61a1a98..aed7cd3 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt
@@ -26,9 +26,10 @@
 
     companion object {
         public const val TAG = "ControlsUiController"
+        public const val EXTRA_ANIMATE = "extra_animate"
     }
 
-    fun show(parent: ViewGroup)
+    fun show(parent: ViewGroup, dismissGlobalActions: Runnable)
     fun hide()
     fun onRefreshState(componentName: ComponentName, controls: List<Control>)
     fun onActionResponse(
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
index 2adfb1b..cfd8df0 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
@@ -85,7 +85,7 @@
         private const val PREF_STRUCTURE = "controls_structure"
 
         private const val USE_PANELS = "systemui.controls_use_panel"
-        private const val FADE_IN_MILLIS = 225L
+        private const val FADE_IN_MILLIS = 200L
 
         private val EMPTY_COMPONENT = ComponentName("", "")
         private val EMPTY_STRUCTURE = StructureInfo(
@@ -104,6 +104,7 @@
     private var popup: ListPopupWindow? = null
     private var activeDialog: Dialog? = null
     private var hidden = true
+    private lateinit var dismissGlobalActions: Runnable
 
     override val available: Boolean
         get() = controlsController.get().available
@@ -134,9 +135,10 @@
         }
     }
 
-    override fun show(parent: ViewGroup) {
+    override fun show(parent: ViewGroup, dismissGlobalActions: Runnable) {
         Log.d(ControlsUiController.TAG, "show()")
         this.parent = parent
+        this.dismissGlobalActions = dismissGlobalActions
         hidden = false
 
         allStructures = controlsController.get().getFavorites()
@@ -169,7 +171,7 @@
         fadeAnim.setDuration(FADE_IN_MILLIS)
         fadeAnim.addListener(object : AnimatorListenerAdapter() {
             override fun onAnimationEnd(animation: Animator) {
-                show(parent)
+                show(parent, dismissGlobalActions)
                 val showAnim = ObjectAnimator.ofFloat(parent, "alpha", 0.0f, 1.0f)
                 showAnim.setInterpolator(DecelerateInterpolator(1.0f))
                 showAnim.setDuration(FADE_IN_MILLIS)
@@ -256,9 +258,10 @@
     }
 
     private fun startActivity(context: Context, intent: Intent) {
-        val closeDialog = Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)
-        context.sendBroadcast(closeDialog)
+        // Force animations when transitioning from a dialog to an activity
+        intent.putExtra(ControlsUiController.EXTRA_ANIMATE, true)
         context.startActivity(intent)
+        dismissGlobalActions.run()
     }
 
     private fun showControlsView(items: List<SelectionItem>) {
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java
index a7c4043..8f3dc22 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java
@@ -19,8 +19,10 @@
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.app.ActivityManager;
+import android.app.ActivityTaskManager;
 import android.app.AlarmManager;
 import android.app.IActivityManager;
+import android.app.IActivityTaskManager;
 import android.app.IWallpaperManager;
 import android.app.KeyguardManager;
 import android.app.NotificationManager;
@@ -128,6 +130,12 @@
         return ActivityManager.getService();
     }
 
+    @Singleton
+    @Provides
+    static IActivityTaskManager provideIActivityTaskManager() {
+        return ActivityTaskManager.getService();
+    }
+
     @Provides
     @Singleton
     static IBatteryStats provideIBatteryStats() {
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIDefaultModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIDefaultModule.java
index 88f96a8..8c572fe 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIDefaultModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIDefaultModule.java
@@ -24,7 +24,6 @@
 import androidx.annotation.Nullable;
 
 import com.android.keyguard.KeyguardViewController;
-import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.dock.DockManagerImpl;
 import com.android.systemui.plugins.qs.QSFactory;
@@ -34,7 +33,6 @@
 import com.android.systemui.qs.tileimpl.QSFactoryImpl;
 import com.android.systemui.recents.Recents;
 import com.android.systemui.recents.RecentsImplementation;
-import com.android.systemui.settings.CurrentUserContextTracker;
 import com.android.systemui.stackdivider.DividerModule;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
@@ -138,15 +136,4 @@
     @Binds
     abstract KeyguardViewController bindKeyguardViewController(
             StatusBarKeyguardViewManager statusBarKeyguardViewManager);
-
-    @Singleton
-    @Provides
-    static CurrentUserContextTracker provideCurrentUserContextTracker(
-            Context context,
-            BroadcastDispatcher broadcastDispatcher) {
-        CurrentUserContextTracker tracker =
-                new CurrentUserContextTracker(context, broadcastDispatcher);
-        tracker.initialize();
-        return tracker;
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 2e9ce12..90cd13f 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -29,6 +29,7 @@
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.recents.Recents;
+import com.android.systemui.settings.dagger.SettingsModule;
 import com.android.systemui.stackdivider.Divider;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinder;
@@ -61,6 +62,7 @@
             ConcurrencyModule.class,
             LogModule.class,
             PeopleHubModule.class,
+            SettingsModule.class
         },
         subcomponents = {StatusBarComponent.class,
                 NotificationRowComponent.class,
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
index ea358c7..a24fede 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
@@ -16,6 +16,8 @@
 
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_GLOBAL_ACTIONS;
+import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN;
 
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED;
@@ -110,6 +112,7 @@
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.controls.ControlsServiceInfo;
 import com.android.systemui.controls.controller.ControlsController;
+import com.android.systemui.controls.management.ControlsAnimations;
 import com.android.systemui.controls.management.ControlsListingController;
 import com.android.systemui.controls.ui.ControlsUiController;
 import com.android.systemui.dagger.qualifiers.Background;
@@ -126,7 +129,6 @@
 import com.android.systemui.util.EmergencyDialerConstants;
 import com.android.systemui.util.RingerModeTracker;
 import com.android.systemui.util.leak.RotationUtils;
-import com.android.systemui.volume.SystemUIInterpolators.LogAccelerateInterpolator;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -479,7 +481,8 @@
      */
     @VisibleForTesting
     protected int getMaxShownPowerItems() {
-        if (shouldUseControlsLayout()) {
+        // TODO: Overflow disabled on keyguard while we solve for touch blocking issues.
+        if (shouldUseControlsLayout() && !mKeyguardShowing) {
             return mResources.getInteger(com.android.systemui.R.integer.power_menu_max_columns);
         } else {
             return Integer.MAX_VALUE;
@@ -829,7 +832,8 @@
             mHandler.postDelayed(new Runnable() {
                 @Override
                 public void run() {
-                    mScreenshotHelper.takeScreenshot(1, true, true, mHandler, null);
+                    mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN, true, true,
+                            SCREENSHOT_GLOBAL_ACTIONS, mHandler, null);
                     mMetricsLogger.action(MetricsEvent.ACTION_SCREENSHOT_POWER_MENU);
                     mUiEventLogger.log(GlobalActionsEvent.GA_SCREENSHOT_PRESS);
                 }
@@ -1815,7 +1819,7 @@
                 case MESSAGE_DISMISS:
                     if (mDialog != null) {
                         if (SYSTEM_DIALOG_REASON_DREAM.equals(msg.obj)) {
-                            mDialog.dismissImmediately();
+                            mDialog.completeDismiss();
                         } else {
                             mDialog.dismiss();
                         }
@@ -1894,6 +1898,7 @@
 
         private ControlsUiController mControlsUiController;
         private ViewGroup mControlsView;
+        private ViewGroup mContainer;
 
         ActionsDialog(Context context, MyAdapter adapter, MyOverflowAdapter overflowAdapter,
                 GlobalActionsPanelPlugin.PanelViewController plugin,
@@ -2046,6 +2051,11 @@
             });
             mGlobalActionsLayout.setRotationListener(this::onRotate);
             mGlobalActionsLayout.setAdapter(mAdapter);
+            mContainer = findViewById(com.android.systemui.R.id.global_actions_container);
+            // Some legacy dialog layouts don't have the outer container
+            if (mContainer == null) {
+                mContainer = mGlobalActionsLayout;
+            }
 
             mOverflowPopup = createPowerOverflowPopup();
 
@@ -2172,10 +2182,10 @@
             mHadTopUi = mNotificationShadeWindowController.getForceHasTopUi();
             mNotificationShadeWindowController.setForceHasTopUi(true);
             mBackgroundDrawable.setAlpha(0);
-            mGlobalActionsLayout.setTranslationX(mGlobalActionsLayout.getAnimationOffsetX());
-            mGlobalActionsLayout.setTranslationY(mGlobalActionsLayout.getAnimationOffsetY());
-            mGlobalActionsLayout.setAlpha(0);
-            mGlobalActionsLayout.animate()
+            mContainer.setTranslationX(mGlobalActionsLayout.getAnimationOffsetX());
+            mContainer.setTranslationY(mGlobalActionsLayout.getAnimationOffsetY());
+            mContainer.setAlpha(0);
+            mContainer.animate()
                     .alpha(1)
                     .translationX(0)
                     .translationY(0)
@@ -2200,48 +2210,55 @@
                 return WindowInsets.CONSUMED;
             });
             if (mControlsUiController != null) {
-                mControlsUiController.show(mControlsView);
+                mControlsUiController.show(mControlsView, this::dismissForControlsActivity);
             }
         }
 
         @Override
         public void dismiss() {
+            dismissWithAnimation(() -> {
+                mContainer.setTranslationX(0);
+                mContainer.setTranslationY(0);
+                mContainer.setAlpha(1);
+                mContainer.animate()
+                        .alpha(0)
+                        .translationX(mGlobalActionsLayout.getAnimationOffsetX())
+                        .translationY(mGlobalActionsLayout.getAnimationOffsetY())
+                        .setDuration(450)
+                        .withEndAction(this::completeDismiss)
+                        .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+                        .setUpdateListener(animation -> {
+                            float animatedValue = 1f - animation.getAnimatedFraction();
+                            int alpha = (int) (animatedValue * mScrimAlpha * 255);
+                            mBackgroundDrawable.setAlpha(alpha);
+                            mDepthController.updateGlobalDialogVisibility(animatedValue,
+                                    mGlobalActionsLayout);
+                        })
+                        .start();
+            });
+        }
+
+        private void dismissForControlsActivity() {
+            dismissWithAnimation(() -> {
+                ViewGroup root = (ViewGroup) mGlobalActionsLayout.getParent();
+                ControlsAnimations.exitAnimation(root, this::completeDismiss).start();
+            });
+        }
+
+        void dismissWithAnimation(Runnable animation) {
             if (!mShowing) {
                 return;
             }
             mShowing = false;
-            if (mControlsUiController != null) mControlsUiController.hide();
-            mGlobalActionsLayout.setTranslationX(0);
-            mGlobalActionsLayout.setTranslationY(0);
-            mGlobalActionsLayout.setAlpha(1);
-            mGlobalActionsLayout.animate()
-                    .alpha(0)
-                    .translationX(mGlobalActionsLayout.getAnimationOffsetX())
-                    .translationY(mGlobalActionsLayout.getAnimationOffsetY())
-                    .setDuration(550)
-                    .withEndAction(this::completeDismiss)
-                    .setInterpolator(new LogAccelerateInterpolator())
-                    .setUpdateListener(animation -> {
-                        float animatedValue = 1f - animation.getAnimatedFraction();
-                        int alpha = (int) (animatedValue * mScrimAlpha * 255);
-                        mBackgroundDrawable.setAlpha(alpha);
-                        mDepthController.updateGlobalDialogVisibility(animatedValue,
-                                mGlobalActionsLayout);
-                    })
-                    .start();
-            dismissPanel();
-            resetOrientation();
-        }
-
-        void dismissImmediately() {
-            mShowing = false;
-            if (mControlsUiController != null) mControlsUiController.hide();
-            dismissPanel();
-            resetOrientation();
-            completeDismiss();
+            animation.run();
         }
 
         private void completeDismiss() {
+            mShowing = false;
+            resetOrientation();
+            dismissPanel();
+            dismissOverflow();
+            if (mControlsUiController != null) mControlsUiController.hide();
             mNotificationShadeWindowController.setForceHasTopUi(mHadTopUi);
             mDepthController.updateGlobalDialogVisibility(0, null /* view */);
             super.dismiss();
@@ -2253,6 +2270,12 @@
             }
         }
 
+        private void dismissOverflow() {
+            if (mOverflowPopup != null) {
+                mOverflowPopup.dismiss();
+            }
+        }
+
         private void setRotationSuggestionsEnabled(boolean enabled) {
             try {
                 final int userId = Binder.getCallingUserHandle().getIdentifier();
@@ -2296,7 +2319,7 @@
             initializeLayout();
             mGlobalActionsLayout.updateList();
             if (mControlsUiController != null) {
-                mControlsUiController.show(mControlsView);
+                mControlsUiController.show(mControlsView, this::dismissForControlsActivity);
             }
         }
 
@@ -2335,10 +2358,9 @@
                 && mControlsUiController.getAvailable()
                 && !mControlsServiceInfos.isEmpty();
     }
-
     // TODO: Remove legacy layout XML and classes.
     protected boolean shouldUseControlsLayout() {
         // always use new controls layout
         return true;
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index 14e3e93..123cf78 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -61,6 +61,18 @@
         return buffer;
     }
 
+    /** Provides a logging buffer for all logs related to the data layer of notifications. */
+    @Provides
+    @Singleton
+    @NotifInteractionLog
+    public static LogBuffer provideNotifInteractionLogBuffer(
+            LogcatEchoTracker echoTracker,
+            DumpManager dumpManager) {
+        LogBuffer buffer = new LogBuffer("NotifInteractionLog", 50, 10, echoTracker);
+        buffer.attach(dumpManager);
+        return buffer;
+    }
+
     /** Provides a logging buffer for all logs related to Quick Settings. */
     @Provides
     @Singleton
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java
new file mode 100644
index 0000000..20fc6ff
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java
@@ -0,0 +1,36 @@
+/*
+ * 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.log.dagger;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.android.systemui.log.LogBuffer;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+
+import javax.inject.Qualifier;
+
+/**
+ * A {@link LogBuffer} for messages related to the user interacting with notifications (e.g.
+ * clicking on them).
+ */
+@Qualifier
+@Documented
+@Retention(RUNTIME)
+public @interface NotifInteractionLog {
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
index 9217eb1..233d24b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
@@ -21,20 +21,25 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.res.ColorStateList;
 import android.graphics.Bitmap;
+import android.graphics.ImageDecoder;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.GradientDrawable;
 import android.graphics.drawable.Icon;
 import android.graphics.drawable.RippleDrawable;
+import android.media.MediaDescription;
 import android.media.MediaMetadata;
+import android.media.ThumbnailUtils;
 import android.media.session.MediaController;
 import android.media.session.MediaSession;
 import android.media.session.PlaybackState;
+import android.net.Uri;
+import android.service.media.MediaBrowserService;
 import android.util.Log;
-import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.View.OnAttachStateChangeListener;
@@ -55,8 +60,10 @@
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
 import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.qs.QSMediaBrowser;
 import com.android.systemui.util.Assert;
 
+import java.io.IOException;
 import java.util.List;
 import java.util.concurrent.Executor;
 
@@ -67,7 +74,7 @@
     private static final String TAG = "MediaControlPanel";
     @Nullable private final LocalMediaManager mLocalMediaManager;
     private final Executor mForegroundExecutor;
-    private final Executor mBackgroundExecutor;
+    protected final Executor mBackgroundExecutor;
 
     private Context mContext;
     protected LinearLayout mMediaNotifView;
@@ -76,13 +83,18 @@
     private MediaController mController;
     private int mForegroundColor;
     private int mBackgroundColor;
-    protected ComponentName mRecvComponent;
     private MediaDevice mDevice;
+    protected ComponentName mServiceComponent;
     private boolean mIsRegistered = false;
     private String mKey;
 
     private final int[] mActionIds;
 
+    public static final String MEDIA_PREFERENCES = "media_control_prefs";
+    public static final String MEDIA_PREFERENCE_KEY = "browser_components";
+    private SharedPreferences mSharedPrefs;
+    private boolean mCheckedForResumption = false;
+
     // Button IDs used in notifications
     protected static final int[] NOTIF_ACTION_IDS = {
             com.android.internal.R.id.action0,
@@ -92,6 +104,13 @@
             com.android.internal.R.id.action4
     };
 
+    // URI fields to try loading album art from
+    private static final String[] ART_URIS = {
+            MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
+            MediaMetadata.METADATA_KEY_ART_URI,
+            MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
+    };
+
     private final MediaController.Callback mSessionCallback = new MediaController.Callback() {
         @Override
         public void onSessionDestroyed() {
@@ -154,7 +173,6 @@
      * Initialize a new control panel
      * @param context
      * @param parent
-     * @param manager
      * @param routeManager Manager used to listen for device change events.
      * @param layoutId layout resource to use for this control panel
      * @param actionIds resource IDs for action buttons in the layout
@@ -198,47 +216,52 @@
     /**
      * Update the media panel view for the given media session
      * @param token
-     * @param icon
+     * @param iconDrawable
+     * @param largeIcon
      * @param iconColor
      * @param bgColor
      * @param contentIntent
      * @param appNameString
      * @param key
      */
-    public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor,
-            int bgColor, PendingIntent contentIntent, String appNameString, String key) {
-        mToken = token;
+    public void setMediaSession(MediaSession.Token token, Drawable iconDrawable, Icon largeIcon,
+            int iconColor, int bgColor, PendingIntent contentIntent, String appNameString,
+            String key) {
+        // Ensure that component names are updated if token has changed
+        if (mToken == null || !mToken.equals(token)) {
+            mToken = token;
+            mServiceComponent = null;
+            mCheckedForResumption = false;
+        }
+
         mForegroundColor = iconColor;
         mBackgroundColor = bgColor;
         mController = new MediaController(mContext, mToken);
         mKey = key;
 
-        MediaMetadata mediaMetadata = mController.getMetadata();
-
-        // Try to find a receiver for the media button that matches this app
-        PackageManager pm = mContext.getPackageManager();
-        Intent it = new Intent(Intent.ACTION_MEDIA_BUTTON);
-        List<ResolveInfo> info = pm.queryBroadcastReceiversAsUser(it, 0, mContext.getUser());
-        if (info != null) {
-            for (ResolveInfo inf : info) {
-                if (inf.activityInfo.packageName.equals(mController.getPackageName())) {
-                    mRecvComponent = inf.getComponentInfo().getComponentName();
+        // Try to find a browser service component for this app
+        // TODO also check for a media button receiver intended for restarting (b/154127084)
+        // Only check if we haven't tried yet or the session token changed
+        String pkgName = mController.getPackageName();
+        if (mServiceComponent == null && !mCheckedForResumption) {
+            Log.d(TAG, "Checking for service component");
+            PackageManager pm = mContext.getPackageManager();
+            Intent resumeIntent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
+            List<ResolveInfo> resumeInfo = pm.queryIntentServices(resumeIntent, 0);
+            if (resumeInfo != null) {
+                for (ResolveInfo inf : resumeInfo) {
+                    if (inf.serviceInfo.packageName.equals(mController.getPackageName())) {
+                        mBackgroundExecutor.execute(() ->
+                                tryUpdateResumptionList(inf.getComponentInfo().getComponentName()));
+                        break;
+                    }
                 }
             }
+            mCheckedForResumption = true;
         }
 
         mController.registerCallback(mSessionCallback);
 
-        if (mediaMetadata == null) {
-            Log.e(TAG, "Media metadata was null");
-            return;
-        }
-
-        ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
-        if (albumView != null) {
-            // Resize art in a background thread
-            mBackgroundExecutor.execute(() -> processAlbumArt(mediaMetadata, albumView));
-        }
         mMediaNotifView.setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor));
 
         // Click action
@@ -256,32 +279,9 @@
 
         // App icon
         ImageView appIcon = mMediaNotifView.findViewById(R.id.icon);
-        Drawable iconDrawable = icon.loadDrawable(mContext);
         iconDrawable.setTint(mForegroundColor);
         appIcon.setImageDrawable(iconDrawable);
 
-        // Song name
-        TextView titleText = mMediaNotifView.findViewById(R.id.header_title);
-        String songName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
-        titleText.setText(songName);
-        titleText.setTextColor(mForegroundColor);
-
-        // Not in mini player:
-        // App title
-        TextView appName = mMediaNotifView.findViewById(R.id.app_name);
-        if (appName != null) {
-            appName.setText(appNameString);
-            appName.setTextColor(mForegroundColor);
-        }
-
-        // Artist name
-        TextView artistText = mMediaNotifView.findViewById(R.id.header_artist);
-        if (artistText != null) {
-            String artistName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
-            artistText.setText(artistName);
-            artistText.setTextColor(mForegroundColor);
-        }
-
         // Transfer chip
         mSeamless = mMediaNotifView.findViewById(R.id.media_seamless);
         if (mSeamless != null && mLocalMediaManager != null) {
@@ -300,6 +300,39 @@
         }
 
         makeActive();
+
+        // App title (not in mini player)
+        TextView appName = mMediaNotifView.findViewById(R.id.app_name);
+        if (appName != null) {
+            appName.setText(appNameString);
+            appName.setTextColor(mForegroundColor);
+        }
+
+        MediaMetadata mediaMetadata = mController.getMetadata();
+        if (mediaMetadata == null) {
+            Log.e(TAG, "Media metadata was null");
+            return;
+        }
+
+        ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
+        if (albumView != null) {
+            // Resize art in a background thread
+            mBackgroundExecutor.execute(() -> processAlbumArt(mediaMetadata, largeIcon, albumView));
+        }
+
+        // Song name
+        TextView titleText = mMediaNotifView.findViewById(R.id.header_title);
+        String songName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
+        titleText.setText(songName);
+        titleText.setTextColor(mForegroundColor);
+
+        // Artist name (not in mini player)
+        TextView artistText = mMediaNotifView.findViewById(R.id.header_artist);
+        if (artistText != null) {
+            String artistName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
+            artistText.setText(artistName);
+            artistText.setTextColor(mForegroundColor);
+        }
     }
 
     /**
@@ -320,9 +353,12 @@
 
     /**
      * Get the name of the package associated with the current media controller
-     * @return the package name
+     * @return the package name, or null if no controller
      */
     public String getMediaPlayerPackage() {
+        if (mController == null) {
+            return null;
+        }
         return mController.getPackageName();
     }
 
@@ -370,18 +406,86 @@
 
     /**
      * Process album art for layout
-     * @param metadata media metadata
+     * @param description media description
      * @param albumView view to hold the album art
      */
-    private void processAlbumArt(MediaMetadata metadata, ImageView albumView) {
-        Bitmap albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
-        float radius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius);
+    protected void processAlbumArt(MediaDescription description, ImageView albumView) {
+        Bitmap albumArt = null;
+
+        // First try loading from URI
+        albumArt = loadBitmapFromUri(description.getIconUri());
+
+        // Then check bitmap
+        if (albumArt == null) {
+            albumArt = description.getIconBitmap();
+        }
+
+        processAlbumArtInternal(albumArt, albumView);
+    }
+
+    /**
+     * Process album art for layout
+     * @param metadata media metadata
+     * @param largeIcon from notification, checked as a fallback if metadata does not have art
+     * @param albumView view to hold the album art
+     */
+    private void processAlbumArt(MediaMetadata metadata, Icon largeIcon, ImageView albumView) {
+        Bitmap albumArt = null;
+
+        // First look in URI fields
+        for (String field : ART_URIS) {
+            String uriString = metadata.getString(field);
+            if (uriString != null) {
+                albumArt = loadBitmapFromUri(Uri.parse(uriString));
+                if (albumArt != null) {
+                    Log.d(TAG, "loaded art from " + field);
+                    break;
+                }
+            }
+        }
+
+        // Then check bitmap field
+        if (albumArt == null) {
+            albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
+        }
+
+        // Finally try the notification's largeIcon
+        if (albumArt == null && largeIcon != null) {
+            albumArt = largeIcon.getBitmap();
+        }
+
+        processAlbumArtInternal(albumArt, albumView);
+    }
+
+    /**
+     * Load a bitmap from a URI
+     * @param uri
+     * @return bitmap, or null if couldn't be loaded
+     */
+    private Bitmap loadBitmapFromUri(Uri uri) {
+        ImageDecoder.Source source = ImageDecoder.createSource(mContext.getContentResolver(), uri);
+        try {
+            return ImageDecoder.decodeBitmap(source);
+        } catch (IOException e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    /**
+     * Resize and crop the image if provided and update the control view
+     * @param albumArt Bitmap of art to display, or null to hide view
+     * @param albumView View that will hold the art
+     */
+    private void processAlbumArtInternal(@Nullable Bitmap albumArt, ImageView albumView) {
+        // Resize
         RoundedBitmapDrawable roundedDrawable = null;
         if (albumArt != null) {
+            float radius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius);
             Bitmap original = albumArt.copy(Bitmap.Config.ARGB_8888, true);
             int albumSize = (int) mContext.getResources().getDimension(
                     R.dimen.qs_media_album_size);
-            Bitmap scaled = Bitmap.createScaledBitmap(original, albumSize, albumSize, false);
+            Bitmap scaled = ThumbnailUtils.extractThumbnail(original, albumSize, albumSize);
             roundedDrawable = RoundedBitmapDrawableFactory.create(mContext.getResources(), scaled);
             roundedDrawable.setCornerRadius(radius);
         } else {
@@ -449,10 +553,24 @@
     }
 
     /**
-     * Put controls into a resumption state
+     * Puts controls into a resumption state if possible, or calls removePlayer if no component was
+     * found that could resume playback
      */
     public void clearControls() {
         Log.d(TAG, "clearControls to resumption state package=" + getMediaPlayerPackage());
+        if (mServiceComponent == null) {
+            // If we don't have a way to resume, just remove the player altogether
+            Log.d(TAG, "Removing unresumable controls");
+            removePlayer();
+            return;
+        }
+        resetButtons();
+    }
+
+    /**
+     * Hide the media buttons and show only a restart button
+     */
+    protected void resetButtons() {
         // Hide all the old buttons
         for (int i = 0; i < mActionIds.length; i++) {
             ImageButton thisBtn = mMediaNotifView.findViewById(mActionIds[i]);
@@ -465,27 +583,8 @@
         ImageButton btn = mMediaNotifView.findViewById(mActionIds[0]);
         btn.setOnClickListener(v -> {
             Log.d(TAG, "Attempting to restart session");
-            // Send a media button event to previously found receiver
-            if (mRecvComponent != null) {
-                Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
-                intent.setComponent(mRecvComponent);
-                int keyCode = KeyEvent.KEYCODE_MEDIA_PLAY;
-                intent.putExtra(
-                        Intent.EXTRA_KEY_EVENT,
-                        new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
-                mContext.sendBroadcast(intent);
-            } else {
-                // If we don't have a receiver, try relaunching the activity instead
-                if (mController.getSessionActivity() != null) {
-                    try {
-                        mController.getSessionActivity().send();
-                    } catch (PendingIntent.CanceledException e) {
-                        Log.e(TAG, "Pending intent was canceled", e);
-                    }
-                } else {
-                    Log.e(TAG, "No receiver or activity to restart");
-                }
-            }
+            QSMediaBrowser browser = new QSMediaBrowser(mContext, null, mServiceComponent);
+            browser.restart();
         });
         btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play));
         btn.setImageTintList(ColorStateList.valueOf(mForegroundColor));
@@ -514,4 +613,65 @@
         }
     }
 
+    /**
+     * Verify that we can connect to the given component with a MediaBrowser, and if so, add that
+     * component to the list of resumption components
+     */
+    private void tryUpdateResumptionList(ComponentName componentName) {
+        Log.d(TAG, "Testing if we can connect to " + componentName);
+        QSMediaBrowser.testConnection(mContext,
+                new QSMediaBrowser.Callback() {
+                    @Override
+                    public void onConnected() {
+                        Log.d(TAG, "yes we can resume with " + componentName);
+                        mServiceComponent = componentName;
+                        updateResumptionList(componentName);
+                    }
+
+                    @Override
+                    public void onError() {
+                        Log.d(TAG, "Cannot resume with " + componentName);
+                        mServiceComponent = null;
+                        clearControls();
+                        // remove
+                    }
+                },
+                componentName);
+    }
+
+    /**
+     * Add the component to the saved list of media browser services, checking for duplicates and
+     * removing older components that exceed the maximum limit
+     * @param componentName
+     */
+    private synchronized void updateResumptionList(ComponentName componentName) {
+        // Add to front of saved list
+        if (mSharedPrefs == null) {
+            mSharedPrefs = mContext.getSharedPreferences(MEDIA_PREFERENCES, 0);
+        }
+        String componentString = componentName.flattenToString();
+        String listString = mSharedPrefs.getString(MEDIA_PREFERENCE_KEY, null);
+        if (listString == null) {
+            listString = componentString;
+        } else {
+            String[] components = listString.split(QSMediaBrowser.DELIMITER);
+            StringBuilder updated = new StringBuilder(componentString);
+            int nBrowsers = 1;
+            for (int i = 0; i < components.length
+                    && nBrowsers < QSMediaBrowser.MAX_RESUMPTION_CONTROLS; i++) {
+                if (componentString.equals(components[i])) {
+                    continue;
+                }
+                updated.append(QSMediaBrowser.DELIMITER).append(components[i]);
+                nBrowsers++;
+            }
+            listString = updated.toString();
+        }
+        mSharedPrefs.edit().putString(MEDIA_PREFERENCE_KEY, listString).apply();
+    }
+
+    /**
+     * Called when a player can't be resumed to give it an opportunity to hide or remove itself
+     */
+    protected void removePlayer() { }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSMediaBrowser.java b/packages/SystemUI/src/com/android/systemui/qs/QSMediaBrowser.java
new file mode 100644
index 0000000..302b8420
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSMediaBrowser.java
@@ -0,0 +1,259 @@
+/*
+ * 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.qs;
+
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.media.MediaDescription;
+import android.media.browse.MediaBrowser;
+import android.media.session.MediaController;
+import android.media.session.MediaSession;
+import android.os.Bundle;
+import android.service.media.MediaBrowserService;
+import android.util.Log;
+
+import java.util.List;
+
+/**
+ * Media browser for managing resumption in QS media controls
+ */
+public class QSMediaBrowser {
+
+    /** Maximum number of controls to show on boot */
+    public static final int MAX_RESUMPTION_CONTROLS = 5;
+
+    /** Delimiter for saved component names */
+    public static final String DELIMITER = ":";
+
+    private static final String TAG = "QSMediaBrowser";
+    private final Context mContext;
+    private final Callback mCallback;
+    private MediaBrowser mMediaBrowser;
+    private ComponentName mComponentName;
+
+    /**
+     * Initialize a new media browser
+     * @param context the context
+     * @param callback used to report media items found
+     * @param componentName Component name of the MediaBrowserService this browser will connect to
+     */
+    public QSMediaBrowser(Context context, Callback callback, ComponentName componentName) {
+        mContext = context;
+        mCallback = callback;
+        mComponentName = componentName;
+
+        Bundle rootHints = new Bundle();
+        rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
+        mMediaBrowser = new MediaBrowser(mContext,
+                mComponentName,
+                mConnectionCallback,
+                rootHints);
+    }
+
+    /**
+     * Connects to the MediaBrowserService and looks for valid media. If a media item is returned
+     * by the service, QSMediaBrowser.Callback#addTrack will be called with its MediaDescription
+     */
+    public void findRecentMedia() {
+        Log.d(TAG, "Connecting to " + mComponentName);
+        mMediaBrowser.connect();
+    }
+
+    private final MediaBrowser.SubscriptionCallback mSubscriptionCallback =
+            new MediaBrowser.SubscriptionCallback() {
+        @Override
+        public void onChildrenLoaded(String parentId,
+                List<MediaBrowser.MediaItem> children) {
+            if (children.size() == 0) {
+                Log.e(TAG, "No children found");
+                return;
+            }
+            // We ask apps to return a playable item as the first child when sending
+            // a request with EXTRA_RECENT; if they don't, no resume controls
+            MediaBrowser.MediaItem child = children.get(0);
+            MediaDescription desc = child.getDescription();
+            if (child.isPlayable()) {
+                mCallback.addTrack(desc, mMediaBrowser.getServiceComponent(), QSMediaBrowser.this);
+            } else {
+                Log.e(TAG, "Child found but not playable for " + mComponentName);
+            }
+            mMediaBrowser.disconnect();
+        }
+
+        @Override
+        public void onError(String parentId) {
+            Log.e(TAG, "Subscribe error for " + mComponentName + ": " + parentId);
+            mMediaBrowser.disconnect();
+        }
+
+        @Override
+        public void onError(String parentId, Bundle options) {
+            Log.e(TAG, "Subscribe error for " + mComponentName + ": " + parentId
+                    + ", options: " + options);
+            mMediaBrowser.disconnect();
+        }
+    };
+
+    private final MediaBrowser.ConnectionCallback mConnectionCallback =
+            new MediaBrowser.ConnectionCallback() {
+        /**
+         * Invoked after {@link MediaBrowser#connect()} when the request has successfully completed.
+         * For resumption controls, apps are expected to return a playable media item as the first
+         * child. If there are no children or it isn't playable it will be ignored.
+         */
+        @Override
+        public void onConnected() {
+            if (mMediaBrowser.isConnected()) {
+                mCallback.onConnected();
+                Log.d(TAG, "Service connected for " + mComponentName);
+                String root = mMediaBrowser.getRoot();
+                mMediaBrowser.subscribe(root, mSubscriptionCallback);
+            }
+        }
+
+        /**
+         * Invoked when the client is disconnected from the media browser.
+         */
+        @Override
+        public void onConnectionSuspended() {
+            Log.d(TAG, "Connection suspended for " + mComponentName);
+        }
+
+        /**
+         * Invoked when the connection to the media browser failed.
+         */
+        @Override
+        public void onConnectionFailed() {
+            Log.e(TAG, "Connection failed for " + mComponentName);
+            mCallback.onError();
+        }
+    };
+
+    /**
+     * Connects to the MediaBrowserService and starts playback
+     */
+    public void restart() {
+        if (mMediaBrowser.isConnected()) {
+            mMediaBrowser.disconnect();
+        }
+        Bundle rootHints = new Bundle();
+        rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
+        mMediaBrowser = new MediaBrowser(mContext, mComponentName,
+                new MediaBrowser.ConnectionCallback() {
+                    @Override
+                    public void onConnected() {
+                        Log.d(TAG, "Connected for restart " + mMediaBrowser.isConnected());
+                        MediaSession.Token token = mMediaBrowser.getSessionToken();
+                        MediaController controller = new MediaController(mContext, token);
+                        controller.getTransportControls();
+                        controller.getTransportControls().prepare();
+                        controller.getTransportControls().play();
+                    }
+                }, rootHints);
+        mMediaBrowser.connect();
+    }
+
+    /**
+     * Get the media session token
+     * @return the token, or null if the MediaBrowser is null or disconnected
+     */
+    public MediaSession.Token getToken() {
+        if (mMediaBrowser == null || !mMediaBrowser.isConnected()) {
+            return null;
+        }
+        return mMediaBrowser.getSessionToken();
+    }
+
+    /**
+     * Get an intent to launch the app associated with this browser service
+     * @return
+     */
+    public PendingIntent getAppIntent() {
+        PackageManager pm = mContext.getPackageManager();
+        Intent launchIntent = pm.getLaunchIntentForPackage(mComponentName.getPackageName());
+        return PendingIntent.getActivity(mContext, 0, launchIntent, 0);
+    }
+
+    /**
+     * Used to test if SystemUI is allowed to connect to the given component as a MediaBrowser
+     * @param mContext the context
+     * @param callback methods onConnected or onError will be called to indicate whether the
+     *                 connection was successful or not
+     * @param mComponentName Component name of the MediaBrowserService this browser will connect to
+     */
+    public static MediaBrowser testConnection(Context mContext, Callback callback,
+            ComponentName mComponentName) {
+        final MediaBrowser.ConnectionCallback mConnectionCallback =
+                new MediaBrowser.ConnectionCallback() {
+                    @Override
+                    public void onConnected() {
+                        Log.d(TAG, "connected");
+                        callback.onConnected();
+                    }
+
+                    @Override
+                    public void onConnectionSuspended() {
+                        Log.d(TAG, "suspended");
+                        callback.onError();
+                    }
+
+                    @Override
+                    public void onConnectionFailed() {
+                        Log.d(TAG, "failed");
+                        callback.onError();
+                    }
+                };
+        Bundle rootHints = new Bundle();
+        rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
+        MediaBrowser browser = new MediaBrowser(mContext,
+                mComponentName,
+                mConnectionCallback,
+                rootHints);
+        browser.connect();
+        return browser;
+    }
+
+    /**
+     * Interface to handle results from QSMediaBrowser
+     */
+    public static class Callback {
+        /**
+         * Called when the browser has successfully connected to the service
+         */
+        public void onConnected() {
+        }
+
+        /**
+         * Called when the browser encountered an error connecting to the service
+         */
+        public void onError() {
+        }
+
+        /**
+         * Called when the browser finds a suitable track to add to the media carousel
+         * @param track media info for the item
+         * @param component component of the MediaBrowserService which returned this
+         * @param browser reference to the browser
+         */
+        public void addTrack(MediaDescription track, ComponentName component,
+                QSMediaBrowser browser) {
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java b/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java
index 89b22bc..9e574a1 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java
@@ -18,11 +18,13 @@
 
 import static com.android.systemui.util.SysuiLifecycle.viewAttachLifecycle;
 
-import android.app.Notification;
+import android.app.PendingIntent;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.content.res.ColorStateList;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
+import android.media.MediaDescription;
 import android.media.session.MediaController;
 import android.media.session.MediaSession;
 import android.util.Log;
@@ -60,9 +62,11 @@
     };
 
     private final QSPanel mParent;
+    private final Executor mForegroundExecutor;
     private final DelayableExecutor mBackgroundExecutor;
     private final SeekBarViewModel mSeekBarViewModel;
     private final SeekBarObserver mSeekBarObserver;
+    private String mPackageName;
 
     /**
      * Initialize quick shade version of player
@@ -77,6 +81,7 @@
         super(context, parent, routeManager, R.layout.qs_media_panel, QS_ACTION_IDS,
                 foregroundExecutor, backgroundExecutor);
         mParent = (QSPanel) parent;
+        mForegroundExecutor = foregroundExecutor;
         mBackgroundExecutor = backgroundExecutor;
         mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor);
         mSeekBarObserver = new SeekBarObserver(getView());
@@ -90,47 +95,103 @@
     }
 
     /**
+     * Add a media panel view based on a media description. Used for resumption
+     * @param description
+     * @param iconColor
+     * @param bgColor
+     * @param contentIntent
+     * @param pkgName
+     */
+    public void setMediaSession(MediaSession.Token token, MediaDescription description,
+            int iconColor, int bgColor, PendingIntent contentIntent, String pkgName) {
+        mPackageName = pkgName;
+        PackageManager pm = getContext().getPackageManager();
+        Drawable icon = null;
+        CharSequence appName = pkgName.substring(pkgName.lastIndexOf("."));
+        try {
+            icon = pm.getApplicationIcon(pkgName);
+            appName = pm.getApplicationLabel(pm.getApplicationInfo(pkgName, 0));
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.e(TAG, "Error getting package information", e);
+        }
+
+        // Set what we can normally
+        super.setMediaSession(token, icon, null, iconColor, bgColor, contentIntent,
+                appName.toString(), null);
+
+        // Then add info from MediaDescription
+        ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
+        if (albumView != null) {
+            // Resize art in a background thread
+            mBackgroundExecutor.execute(() -> processAlbumArt(description, albumView));
+        }
+
+        // Song name
+        TextView titleText = mMediaNotifView.findViewById(R.id.header_title);
+        CharSequence songName = description.getTitle();
+        titleText.setText(songName);
+        titleText.setTextColor(iconColor);
+
+        // Artist name (not in mini player)
+        TextView artistText = mMediaNotifView.findViewById(R.id.header_artist);
+        if (artistText != null) {
+            CharSequence artistName = description.getSubtitle();
+            artistText.setText(artistName);
+            artistText.setTextColor(iconColor);
+        }
+
+        initLongPressMenu(iconColor);
+
+        // Set buttons to resume state
+        resetButtons();
+    }
+
+    /**
      * Update media panel view for the given media session
      * @param token token for this media session
      * @param icon app notification icon
+     * @param largeIcon notification's largeIcon, used as a fallback for album art
      * @param iconColor foreground color (for text, icons)
      * @param bgColor background color
      * @param actionsContainer a LinearLayout containing the media action buttons
-     * @param notif reference to original notification
+     * @param contentIntent Intent to send when user taps on player
+     * @param appName Application title
      * @param key original notification's key
      */
-    public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor,
-            int bgColor, View actionsContainer, Notification notif, String key) {
+    public void setMediaSession(MediaSession.Token token, Drawable icon, Icon largeIcon,
+            int iconColor, int bgColor, View actionsContainer, PendingIntent contentIntent,
+            String appName, String key) {
 
-        String appName = Notification.Builder.recoverBuilder(getContext(), notif)
-                .loadHeaderAppName();
-        super.setMediaSession(token, icon, iconColor, bgColor, notif.contentIntent, appName, key);
+        super.setMediaSession(token, icon, largeIcon, iconColor, bgColor, contentIntent, appName,
+                key);
 
         // Media controls
-        LinearLayout parentActionsLayout = (LinearLayout) actionsContainer;
-        int i = 0;
-        for (; i < parentActionsLayout.getChildCount() && i < QS_ACTION_IDS.length; i++) {
-            ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]);
-            ImageButton thatBtn = parentActionsLayout.findViewById(NOTIF_ACTION_IDS[i]);
-            if (thatBtn == null || thatBtn.getDrawable() == null
-                    || thatBtn.getVisibility() != View.VISIBLE) {
-                thisBtn.setVisibility(View.GONE);
-                continue;
+        if (actionsContainer != null) {
+            LinearLayout parentActionsLayout = (LinearLayout) actionsContainer;
+            int i = 0;
+            for (; i < parentActionsLayout.getChildCount() && i < QS_ACTION_IDS.length; i++) {
+                ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]);
+                ImageButton thatBtn = parentActionsLayout.findViewById(NOTIF_ACTION_IDS[i]);
+                if (thatBtn == null || thatBtn.getDrawable() == null
+                        || thatBtn.getVisibility() != View.VISIBLE) {
+                    thisBtn.setVisibility(View.GONE);
+                    continue;
+                }
+
+                Drawable thatIcon = thatBtn.getDrawable();
+                thisBtn.setImageDrawable(thatIcon.mutate());
+                thisBtn.setVisibility(View.VISIBLE);
+                thisBtn.setOnClickListener(v -> {
+                    Log.d(TAG, "clicking on other button");
+                    thatBtn.performClick();
+                });
             }
 
-            Drawable thatIcon = thatBtn.getDrawable();
-            thisBtn.setImageDrawable(thatIcon.mutate());
-            thisBtn.setVisibility(View.VISIBLE);
-            thisBtn.setOnClickListener(v -> {
-                Log.d(TAG, "clicking on other button");
-                thatBtn.performClick();
-            });
-        }
-
-        // Hide any unused buttons
-        for (; i < QS_ACTION_IDS.length; i++) {
-            ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]);
-            thisBtn.setVisibility(View.GONE);
+            // Hide any unused buttons
+            for (; i < QS_ACTION_IDS.length; i++) {
+                ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]);
+                thisBtn.setVisibility(View.GONE);
+            }
         }
 
         // Seek Bar
@@ -138,6 +199,10 @@
         mBackgroundExecutor.execute(
                 () -> mSeekBarViewModel.updateController(controller, iconColor));
 
+        initLongPressMenu(iconColor);
+    }
+
+    private void initLongPressMenu(int iconColor) {
         // Set up long press menu
         View guts = mMediaNotifView.findViewById(R.id.media_guts);
         View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options);
@@ -145,7 +210,7 @@
 
         View clearView = options.findViewById(R.id.remove);
         clearView.setOnClickListener(b -> {
-            mParent.removeMediaPlayer(QSMediaPlayer.this);
+            removePlayer();
         });
         ImageView removeIcon = options.findViewById(R.id.remove_icon);
         removeIcon.setImageTintList(ColorStateList.valueOf(iconColor));
@@ -165,11 +230,9 @@
     }
 
     @Override
-    public void clearControls() {
-        super.clearControls();
-
+    protected void resetButtons() {
+        super.resetButtons();
         mSeekBarViewModel.clearController();
-
         View guts = mMediaNotifView.findViewById(R.id.media_guts);
         View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options);
 
@@ -192,4 +255,19 @@
     public void setListening(boolean listening) {
         mSeekBarViewModel.setListening(listening);
     }
+
+    @Override
+    public void removePlayer() {
+        Log.d(TAG, "removing player from parent: " + mParent);
+        // Ensure this happens on the main thread (could happen in QSMediaBrowser callback)
+        mForegroundExecutor.execute(() -> mParent.removeMediaPlayer(QSMediaPlayer.this));
+    }
+
+    @Override
+    public String getMediaPlayerPackage() {
+        if (getController() == null) {
+            return mPackageName;
+        }
+        return super.getMediaPlayerPackage();
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index fee0838..1252008 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -21,16 +21,26 @@
 import static com.android.systemui.util.Utils.useQsMediaPlayer;
 
 import android.annotation.Nullable;
+import android.app.Notification;
+import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
 import android.content.res.Configuration;
 import android.content.res.Resources;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
+import android.media.MediaDescription;
 import android.media.session.MediaSession;
 import android.metrics.LogMaker;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Message;
+import android.os.UserHandle;
+import android.os.UserManager;
 import android.service.notification.StatusBarNotification;
 import android.service.quicksettings.Tile;
 import android.util.AttributeSet;
@@ -54,6 +64,7 @@
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.media.MediaControlPanel;
 import com.android.systemui.plugins.qs.DetailAdapter;
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.plugins.qs.QSTileView;
@@ -90,6 +101,7 @@
 
     protected final Context mContext;
     protected final ArrayList<TileRecord> mRecords = new ArrayList<>();
+    private final BroadcastDispatcher mBroadcastDispatcher;
     private String mCachedSpecs = "";
     protected final View mBrightnessView;
     private final H mHandler = new H();
@@ -123,6 +135,19 @@
 
     private BrightnessMirrorController mBrightnessMirrorController;
     private View mDivider;
+    private boolean mHasLoadedMediaControls;
+
+    private final BroadcastReceiver mUserChangeReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            if (Intent.ACTION_USER_UNLOCKED.equals(action)) {
+                if (!mHasLoadedMediaControls) {
+                    loadMediaResumptionControls();
+                }
+            }
+        }
+    };
 
     @Inject
     public QSPanel(
@@ -142,6 +167,7 @@
         mForegroundExecutor = foregroundExecutor;
         mBackgroundExecutor = backgroundExecutor;
         mLocalBluetoothManager = localBluetoothManager;
+        mBroadcastDispatcher = broadcastDispatcher;
 
         setOrientation(VERTICAL);
 
@@ -176,7 +202,7 @@
         updateResources();
 
         mBrightnessController = new BrightnessController(getContext(),
-                findViewById(R.id.brightness_slider), broadcastDispatcher);
+                findViewById(R.id.brightness_slider), mBroadcastDispatcher);
     }
 
     @Override
@@ -200,14 +226,16 @@
      * Add or update a player for the associated media session
      * @param token
      * @param icon
+     * @param largeIcon
      * @param iconColor
      * @param bgColor
      * @param actionsContainer
      * @param notif
      * @param key
      */
-    public void addMediaSession(MediaSession.Token token, Icon icon, int iconColor, int bgColor,
-            View actionsContainer, StatusBarNotification notif, String key) {
+    public void addMediaSession(MediaSession.Token token, Drawable icon, Icon largeIcon,
+            int iconColor, int bgColor, View actionsContainer, StatusBarNotification notif,
+            String key) {
         if (!useQsMediaPlayer(mContext)) {
             // Shouldn't happen, but just in case
             Log.e(TAG, "Tried to add media session without player!");
@@ -221,7 +249,14 @@
         QSMediaPlayer player = null;
         String packageName = notif.getPackageName();
         for (QSMediaPlayer p : mMediaPlayers) {
-            if (p.getMediaSessionToken().equals(token)) {
+            if (p.getKey() == null) {
+                // No notification key = loaded via mediabrowser, so just match on package
+                if (packageName.equals(p.getMediaPlayerPackage())) {
+                    Log.d(TAG, "Found matching resume player by package: " + packageName);
+                    player = p;
+                    break;
+                }
+            } else if (p.getMediaSessionToken().equals(token)) {
                 Log.d(TAG, "Found matching player by token " + packageName);
                 player = p;
                 break;
@@ -262,8 +297,10 @@
         }
 
         Log.d(TAG, "setting player session");
-        player.setMediaSession(token, icon, iconColor, bgColor, actionsContainer,
-                notif.getNotification(), key);
+        String appName = Notification.Builder.recoverBuilder(getContext(), notif.getNotification())
+                .loadHeaderAppName();
+        player.setMediaSession(token, icon, largeIcon, iconColor, bgColor, actionsContainer,
+                notif.getNotification().contentIntent, appName, key);
 
         if (mMediaPlayers.size() > 0) {
             ((View) mMediaCarousel.getParent()).setVisibility(View.VISIBLE);
@@ -293,6 +330,74 @@
         return true;
     }
 
+    private final QSMediaBrowser.Callback mMediaBrowserCallback = new QSMediaBrowser.Callback() {
+        @Override
+        public void addTrack(MediaDescription desc, ComponentName component,
+                QSMediaBrowser browser) {
+            if (component == null) {
+                Log.e(TAG, "Component cannot be null");
+                return;
+            }
+
+            Log.d(TAG, "adding track from browser: " + desc + ", " + component);
+            QSMediaPlayer player = new QSMediaPlayer(mContext, QSPanel.this,
+                    null, mForegroundExecutor, mBackgroundExecutor);
+
+            String pkgName = component.getPackageName();
+
+            // Add controls to carousel
+            int playerWidth = (int) getResources().getDimension(R.dimen.qs_media_width);
+            int padding = (int) getResources().getDimension(R.dimen.qs_media_padding);
+            LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(playerWidth,
+                    LayoutParams.MATCH_PARENT);
+            lp.setMarginStart(padding);
+            lp.setMarginEnd(padding);
+            mMediaCarousel.addView(player.getView(), lp);
+            ((View) mMediaCarousel.getParent()).setVisibility(View.VISIBLE);
+            mMediaPlayers.add(player);
+
+            int iconColor = Color.DKGRAY;
+            int bgColor = Color.LTGRAY;
+
+            MediaSession.Token token = browser.getToken();
+            player.setMediaSession(token, desc, iconColor, bgColor, browser.getAppIntent(),
+                    pkgName);
+        }
+    };
+
+    /**
+     * Load controls for resuming media, if available
+     */
+    private void loadMediaResumptionControls() {
+        if (!useQsMediaPlayer(mContext)) {
+            return;
+        }
+        Log.d(TAG, "Loading resumption controls");
+
+        //  Look up saved components to resume
+        Context userContext = mContext.createContextAsUser(mContext.getUser(), 0);
+        SharedPreferences prefs = userContext.getSharedPreferences(
+                MediaControlPanel.MEDIA_PREFERENCES, Context.MODE_PRIVATE);
+        String listString = prefs.getString(MediaControlPanel.MEDIA_PREFERENCE_KEY, null);
+        if (listString == null) {
+            Log.d(TAG, "No saved media components");
+            return;
+        }
+
+        String[] components = listString.split(QSMediaBrowser.DELIMITER);
+        Log.d(TAG, "components are: " + listString + " count " + components.length);
+        for (int i = 0; i < components.length && i < QSMediaBrowser.MAX_RESUMPTION_CONTROLS; i++) {
+            String[] info = components[i].split("/");
+            String packageName = info[0];
+            String className = info[1];
+            ComponentName component = new ComponentName(packageName, className);
+            QSMediaBrowser browser = new QSMediaBrowser(mContext, mMediaBrowserCallback,
+                    component);
+            browser.findRecentMedia();
+        }
+        mHasLoadedMediaControls = true;
+    }
+
     protected void addDivider() {
         mDivider = LayoutInflater.from(mContext).inflate(R.layout.qs_divider, this, false);
         mDivider.setBackgroundColor(Utils.applyAlpha(mDivider.getAlpha(),
@@ -343,6 +448,22 @@
             mBrightnessMirrorController.addCallback(this);
         }
         mDumpManager.registerDumpable(getDumpableTag(), this);
+
+        if (getClass() == QSPanel.class) {
+            //TODO(ethibodeau) remove class check after media refactor in ag/11059751
+            // Only run this in QSPanel proper, not QQS
+            IntentFilter filter = new IntentFilter();
+            filter.addAction(Intent.ACTION_USER_UNLOCKED);
+            mBroadcastDispatcher.registerReceiver(mUserChangeReceiver, filter, null,
+                    UserHandle.ALL);
+            mHasLoadedMediaControls = false;
+
+            UserManager userManager = mContext.getSystemService(UserManager.class);
+            if (userManager.isUserUnlocked(mContext.getUserId())) {
+                // If it's already unlocked (like if dark theme was toggled), we can load now
+                loadMediaResumptionControls();
+            }
+        }
     }
 
     @Override
@@ -358,6 +479,7 @@
             mBrightnessMirrorController.removeCallback(this);
         }
         mDumpManager.unregisterDumpable(getDumpableTag());
+        mBroadcastDispatcher.unregisterReceiver(mUserChangeReceiver);
         super.onDetachedFromWindow();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java
index 6229672..89b36da 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java
@@ -59,6 +59,7 @@
      * Update media panel view for the given media session
      * @param token token for this media session
      * @param icon app notification icon
+     * @param largeIcon notification's largeIcon, used as a fallback for album art
      * @param iconColor foreground color (for text, icons)
      * @param bgColor background color
      * @param actionsContainer a LinearLayout containing the media action buttons
@@ -67,8 +68,9 @@
      * @param contentIntent Intent to send when user taps on the view
      * @param key original notification's key
      */
-    public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor, int bgColor,
-            View actionsContainer, int[] actionsToShow, PendingIntent contentIntent, String key) {
+    public void setMediaSession(MediaSession.Token token, Drawable icon, Icon largeIcon,
+            int iconColor, int bgColor, View actionsContainer, int[] actionsToShow,
+            PendingIntent contentIntent, String key) {
         // Only update if this is a different session and currently playing
         String oldPackage = "";
         if (getController() != null) {
@@ -83,7 +85,7 @@
             return;
         }
 
-        super.setMediaSession(token, icon, iconColor, bgColor, contentIntent, null, key);
+        super.setMediaSession(token, icon, largeIcon, iconColor, bgColor, contentIntent, null, key);
 
         LinearLayout parentActionsLayout = (LinearLayout) actionsContainer;
         int i = 0;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
index 9cee7e7..1a6a104 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
@@ -18,7 +18,9 @@
 
 import android.content.Intent;
 import android.service.quicksettings.Tile;
+import android.text.TextUtils;
 import android.util.Log;
+import android.widget.Switch;
 
 import com.android.systemui.R;
 import com.android.systemui.plugins.qs.QSTile;
@@ -88,6 +90,10 @@
             state.icon = ResourceIcon.get(R.drawable.ic_qs_screenrecord);
             state.secondaryLabel = mContext.getString(R.string.quick_settings_screen_record_start);
         }
+        state.contentDescription = TextUtils.isEmpty(state.secondaryLabel)
+                ? state.label
+                : TextUtils.concat(state.label, ", ", state.secondaryLabel);
+        state.expandedAccessibilityClassName = Switch.class.getName();
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 66bc177..fecb7b6 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -20,6 +20,7 @@
 import static android.view.MotionEvent.ACTION_CANCEL;
 import static android.view.MotionEvent.ACTION_DOWN;
 import static android.view.MotionEvent.ACTION_UP;
+import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OVERVIEW;
 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON;
 
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_INPUT_MONITOR;
@@ -380,7 +381,7 @@
         public void handleImageAsScreenshot(Bitmap screenImage, Rect locationInScreen,
                 Insets visibleInsets, int taskId) {
             mScreenshotHelper.provideScreenshot(screenImage, locationInScreen, visibleInsets,
-                    taskId, mHandler, null);
+                    taskId, SCREENSHOT_OVERVIEW, mHandler, null);
         }
 
         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java
index e1cc0b0..70454d4 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java
@@ -36,6 +36,7 @@
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.Insets;
@@ -76,6 +77,7 @@
 import android.widget.LinearLayout;
 import android.widget.Toast;
 
+import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.R;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -104,7 +106,6 @@
      */
     static class SaveImageInBackgroundData {
         public Bitmap image;
-        public Uri imageUri;
         public Consumer<Uri> finisher;
         public GlobalScreenshot.ActionsReadyListener mActionsReadyListener;
         public int errorMsgResId;
@@ -112,13 +113,33 @@
 
         void clearImage() {
             image = null;
-            imageUri = null;
+        }
+    }
+
+    /**
+     * Structure returned by the SaveImageInBackgroundTask
+     */
+    static class SavedImageData {
+        public Uri uri;
+        public Notification.Action shareAction;
+        public Notification.Action editAction;
+        public Notification.Action deleteAction;
+        public List<Notification.Action> smartActions;
+
+        /**
+         * Used to reset the return data on error
+         */
+        public void reset() {
+            uri = null;
+            shareAction = null;
+            editAction = null;
+            deleteAction = null;
+            smartActions = null;
         }
     }
 
     abstract static class ActionsReadyListener {
-        abstract void onActionsReady(Uri imageUri, List<Notification.Action> smartActions,
-                List<Notification.Action> actions);
+        abstract void onActionsReady(SavedImageData imageData);
     }
 
     // These strings are used for communicating the action invoked to
@@ -147,6 +168,7 @@
     private static final int MESSAGE_CORNER_TIMEOUT = 2;
 
     private final ScreenshotNotificationsController mNotificationsController;
+    private final UiEventLogger mUiEventLogger;
 
     private final Context mContext;
     private final WindowManager mWindowManager;
@@ -163,9 +185,11 @@
     private final LinearLayout mActionsView;
     private final ImageView mBackgroundProtection;
     private final FrameLayout mDismissButton;
+    private final ImageView mDismissImage;
 
     private Bitmap mScreenBitmap;
     private Animator mScreenshotAnimation;
+    private boolean mInDarkMode = false;
 
     private float mScreenshotOffsetXPx;
     private float mScreenshotOffsetYPx;
@@ -185,6 +209,7 @@
         public void handleMessage(Message msg) {
             switch (msg.what) {
                 case MESSAGE_CORNER_TIMEOUT:
+                    mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_INTERACTION_TIMEOUT);
                     GlobalScreenshot.this.clearScreenshot("timeout");
                     break;
                 default:
@@ -199,9 +224,11 @@
     @Inject
     public GlobalScreenshot(
             Context context, @Main Resources resources, LayoutInflater layoutInflater,
-            ScreenshotNotificationsController screenshotNotificationsController) {
+            ScreenshotNotificationsController screenshotNotificationsController,
+            UiEventLogger uiEventLogger) {
         mContext = context;
         mNotificationsController = screenshotNotificationsController;
+        mUiEventLogger = uiEventLogger;
 
         // Inflate the screenshot layout
         mScreenshotLayout = layoutInflater.inflate(R.layout.global_screenshot, null);
@@ -222,7 +249,11 @@
         mBackgroundProtection = mScreenshotLayout.findViewById(
                 R.id.global_screenshot_actions_background);
         mDismissButton = mScreenshotLayout.findViewById(R.id.global_screenshot_dismiss_button);
-        mDismissButton.setOnClickListener(view -> clearScreenshot("dismiss_button"));
+        mDismissButton.setOnClickListener(view -> {
+            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EXPLICIT_DISMISSAL);
+            clearScreenshot("dismiss_button");
+        });
+        mDismissImage = mDismissButton.findViewById(R.id.global_screenshot_dismiss_image);
 
         mScreenshotFlash = mScreenshotLayout.findViewById(R.id.global_screenshot_flash);
         mScreenshotSelectorView = mScreenshotLayout.findViewById(R.id.global_screenshot_selector);
@@ -329,6 +360,8 @@
         mScreenBitmap.setHasAlpha(false);
         mScreenBitmap.prepareToDraw();
 
+        updateDarkTheme();
+
         mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams);
         mScreenshotLayout.getViewTreeObserver().addOnComputeInternalInsetsListener(this);
 
@@ -407,7 +440,7 @@
      * Clears current screenshot
      */
     private void clearScreenshot(String reason) {
-        Log.e(TAG, "clearing screenshot: " + reason);
+        Log.v(TAG, "clearing screenshot: " + reason);
         if (mScreenshotLayout.isAttachedToWindow()) {
             mWindowManager.removeView(mScreenshotLayout);
         }
@@ -427,6 +460,41 @@
     }
 
     /**
+     * Update assets (called when the dark theme status changes). We only need to update the dismiss
+     * button and the actions container background, since the buttons are re-inflated on demand.
+     */
+    private void reloadAssets() {
+        mDismissImage.setImageDrawable(mContext.getDrawable(R.drawable.screenshot_cancel));
+        mActionsContainer.setBackground(
+                mContext.getDrawable(R.drawable.action_chip_container_background));
+
+    }
+
+    /**
+     * Checks the current dark theme status and updates if it has changed.
+     */
+    private void updateDarkTheme() {
+        int currentNightMode = mContext.getResources().getConfiguration().uiMode
+                & Configuration.UI_MODE_NIGHT_MASK;
+        switch (currentNightMode) {
+            case Configuration.UI_MODE_NIGHT_NO:
+                // Night mode is not active, we're using the light theme
+                if (mInDarkMode) {
+                    mInDarkMode = false;
+                    reloadAssets();
+                }
+                break;
+            case Configuration.UI_MODE_NIGHT_YES:
+                // Night mode is active, we're using dark theme
+                if (!mInDarkMode) {
+                    mInDarkMode = true;
+                    reloadAssets();
+                }
+                break;
+        }
+    }
+
+    /**
      * Starts the animation after taking the screenshot
      */
     private void startAnimation(final Consumer<Uri> finisher, int w, int h,
@@ -445,12 +513,14 @@
 
         saveScreenshotInWorkerThread(finisher, new ActionsReadyListener() {
             @Override
-            void onActionsReady(Uri uri, List<Notification.Action> smartActions,
-                    List<Notification.Action> actions) {
-                if (uri == null) {
+            void onActionsReady(SavedImageData imageData) {
+                finisher.accept(imageData.uri);
+                if (imageData.uri == null) {
+                    mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED);
                     mNotificationsController.notifyScreenshotError(
                             R.string.screenshot_failed_to_capture_text);
                 } else {
+                    mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED);
                     mScreenshotHandler.post(() -> {
                         if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) {
                             mScreenshotAnimation.addListener(
@@ -458,13 +528,12 @@
                                         @Override
                                         public void onAnimationEnd(Animator animation) {
                                             super.onAnimationEnd(animation);
-                                            createScreenshotActionsShadeAnimation(
-                                                    smartActions, actions).start();
+                                            createScreenshotActionsShadeAnimation(imageData)
+                                                    .start();
                                         }
                                     });
                         } else {
-                            createScreenshotActionsShadeAnimation(smartActions,
-                                    actions).start();
+                            createScreenshotActionsShadeAnimation(imageData).start();
                         }
                     });
                 }
@@ -567,8 +636,7 @@
         return dropInAnimation;
     }
 
-    private ValueAnimator createScreenshotActionsShadeAnimation(
-            List<Notification.Action> smartActions, List<Notification.Action> actions) {
+    private ValueAnimator createScreenshotActionsShadeAnimation(SavedImageData imageData) {
         LayoutInflater inflater = LayoutInflater.from(mContext);
         mActionsView.removeAllViews();
         mActionsContainer.setScrollX(0);
@@ -583,45 +651,63 @@
         } catch (RemoteException e) {
         }
 
-        for (Notification.Action smartAction : smartActions) {
+        for (Notification.Action smartAction : imageData.smartActions) {
             ScreenshotActionChip actionChip = (ScreenshotActionChip) inflater.inflate(
                     R.layout.global_screenshot_action_chip, mActionsView, false);
             actionChip.setText(smartAction.title);
             actionChip.setIcon(smartAction.getIcon(), false);
             actionChip.setPendingIntent(smartAction.actionIntent,
-                    () -> clearScreenshot("chip tapped"));
+                    () -> {
+                        mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED);
+                        clearScreenshot("chip tapped");
+                    });
             mActionsView.addView(actionChip);
         }
 
-        for (Notification.Action action : actions) {
-            ScreenshotActionChip actionChip = (ScreenshotActionChip) inflater.inflate(
-                    R.layout.global_screenshot_action_chip, mActionsView, false);
-            actionChip.setText(action.title);
-            actionChip.setIcon(action.getIcon(), true);
-            actionChip.setPendingIntent(action.actionIntent, () -> clearScreenshot("chip tapped"));
-            if (action.actionIntent.getIntent().getAction().equals(Intent.ACTION_EDIT)) {
-                mScreenshotView.setOnClickListener(v -> {
-                    try {
-                        action.actionIntent.send();
-                        clearScreenshot("screenshot preview tapped");
-                    } catch (PendingIntent.CanceledException e) {
-                        Log.e(TAG, "Intent cancelled", e);
-                    }
-                });
-                mScreenshotView.setContentDescription(action.title);
+        ScreenshotActionChip shareChip = (ScreenshotActionChip) inflater.inflate(
+                R.layout.global_screenshot_action_chip, mActionsView, false);
+        shareChip.setText(imageData.shareAction.title);
+        shareChip.setIcon(imageData.shareAction.getIcon(), true);
+        shareChip.setPendingIntent(imageData.shareAction.actionIntent, () -> {
+            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED);
+            clearScreenshot("chip tapped");
+        });
+        mActionsView.addView(shareChip);
+
+        ScreenshotActionChip editChip = (ScreenshotActionChip) inflater.inflate(
+                R.layout.global_screenshot_action_chip, mActionsView, false);
+        editChip.setText(imageData.editAction.title);
+        editChip.setIcon(imageData.editAction.getIcon(), true);
+        editChip.setPendingIntent(imageData.editAction.actionIntent, () -> {
+            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED);
+            clearScreenshot("chip tapped");
+        });
+        mActionsView.addView(editChip);
+
+        mScreenshotView.setOnClickListener(v -> {
+            try {
+                imageData.editAction.actionIntent.send();
+                mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED);
+                clearScreenshot("screenshot preview tapped");
+            } catch (PendingIntent.CanceledException e) {
+                Log.e(TAG, "Intent cancelled", e);
             }
-            mActionsView.addView(actionChip);
-        }
+        });
+        mScreenshotView.setContentDescription(imageData.editAction.title);
+
 
         if (DeviceConfig.getBoolean(NAMESPACE_SYSTEMUI, SCREENSHOT_SCROLLING_ENABLED, false)) {
             ScreenshotActionChip scrollChip = (ScreenshotActionChip) inflater.inflate(
                     R.layout.global_screenshot_action_chip, mActionsView, false);
             Toast scrollNotImplemented = Toast.makeText(
                     mContext, "Not implemented", Toast.LENGTH_SHORT);
-            scrollChip.setText("Extend"); // TODO (mkephart): add resource and translate
+            scrollChip.setText("Extend"); // TODO: add resource and translate
             scrollChip.setIcon(
                     Icon.createWithResource(mContext, R.drawable.ic_arrow_downward), true);
-            scrollChip.setOnClickListener(v -> scrollNotImplemented.show());
+            scrollChip.setOnClickListener(v -> {
+                mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SCROLL_TAPPED);
+                scrollNotImplemented.show();
+            });
             mActionsView.addView(scrollChip);
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshotLegacy.java b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshotLegacy.java
index f3614ff..095c32f 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshotLegacy.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshotLegacy.java
@@ -24,7 +24,6 @@
 import android.animation.ValueAnimator;
 import android.animation.ValueAnimator.AnimatorUpdateListener;
 import android.annotation.Nullable;
-import android.app.Notification;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.content.res.Resources;
@@ -53,7 +52,6 @@
 import com.android.systemui.R;
 import com.android.systemui.dagger.qualifiers.Main;
 
-import java.util.List;
 import java.util.function.Consumer;
 
 import javax.inject.Inject;
@@ -347,14 +345,13 @@
                 // Save the screenshot once we have a bit of time now
                 saveScreenshotInWorkerThread(finisher, new GlobalScreenshot.ActionsReadyListener() {
                     @Override
-                    void onActionsReady(Uri uri, List<Notification.Action> smartActions,
-                            List<Notification.Action> actions) {
-                        if (uri == null) {
+                    void onActionsReady(GlobalScreenshot.SavedImageData actionData) {
+                        if (actionData.uri == null) {
                             mNotificationsController.notifyScreenshotError(
                                     R.string.screenshot_failed_to_capture_text);
                         } else {
                             mNotificationsController
-                                    .showScreenshotActionsNotification(uri, smartActions, actions);
+                                    .showScreenshotActionsNotification(actionData);
                         }
                     }
                 });
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
index c828c4c..170174d 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
@@ -83,6 +83,7 @@
 
     private final Context mContext;
     private final GlobalScreenshot.SaveImageInBackgroundData mParams;
+    private final GlobalScreenshot.SavedImageData mImageData;
     private final String mImageFileName;
     private final long mImageTime;
     private final ScreenshotNotificationSmartActionsProvider mSmartActionsProvider;
@@ -93,6 +94,7 @@
 
     SaveImageInBackgroundTask(Context context, GlobalScreenshot.SaveImageInBackgroundData data) {
         mContext = context;
+        mImageData = new GlobalScreenshot.SavedImageData();
 
         // Prepare all the output metadata
         mParams = data;
@@ -145,6 +147,7 @@
             values.put(MediaColumns.IS_PENDING, 1);
 
             final Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
+
             try {
                 // First, write the actual data for our screenshot
                 try (OutputStream out = resolver.openOutputStream(uri)) {
@@ -192,8 +195,6 @@
                 throw e;
             }
 
-            List<Notification.Action> actions =
-                    populateNotificationActions(mContext, r, uri);
             List<Notification.Action> smartActions = new ArrayList<>();
             if (mSmartActionsEnabled) {
                 int timeoutMs = DeviceConfig.getInt(
@@ -206,8 +207,14 @@
                                 mSmartActionsProvider),
                         mContext));
             }
-            mParams.mActionsReadyListener.onActionsReady(uri, smartActions, actions);
-            mParams.imageUri = uri;
+
+            mImageData.uri = uri;
+            mImageData.smartActions = smartActions;
+            mImageData.shareAction = createShareAction(mContext, mContext.getResources(), uri);
+            mImageData.editAction = createEditAction(mContext, mContext.getResources(), uri);
+            mImageData.deleteAction = createDeleteAction(mContext, mContext.getResources(), uri);
+
+            mParams.mActionsReadyListener.onActionsReady(mImageData);
             mParams.image = null;
             mParams.errorMsgResId = 0;
         } catch (Exception e) {
@@ -216,29 +223,26 @@
             Slog.e(TAG, "unable to save screenshot", e);
             mParams.clearImage();
             mParams.errorMsgResId = R.string.screenshot_failed_to_save_text;
-            mParams.mActionsReadyListener.onActionsReady(null, null, null);
+            mImageData.reset();
+            mParams.mActionsReadyListener.onActionsReady(mImageData);
         }
 
         return null;
     }
 
     @Override
-    protected void onPostExecute(Void params) {
-        mParams.finisher.accept(mParams.imageUri);
-    }
-
-    @Override
     protected void onCancelled(Void params) {
         // If we are cancelled while the task is running in the background, we may get null
         // params. The finisher is expected to always be called back, so just use the baked-in
         // params from the ctor in any case.
-        mParams.mActionsReadyListener.onActionsReady(null, null, null);
+        mImageData.reset();
+        mParams.mActionsReadyListener.onActionsReady(mImageData);
         mParams.finisher.accept(null);
         mParams.clearImage();
     }
 
     @VisibleForTesting
-    List<Notification.Action> populateNotificationActions(Context context, Resources r, Uri uri) {
+    Notification.Action createShareAction(Context context, Resources r, Uri uri) {
         // Note: Both the share and edit actions are proxied through ActionProxyReceiver in
         // order to do some common work like dismissing the keyguard and sending
         // closeSystemWindows
@@ -263,8 +267,6 @@
         // by setting the (otherwise unused) request code to the current user id.
         int requestCode = context.getUserId();
 
-        ArrayList<Notification.Action> actions = new ArrayList<>();
-
         PendingIntent chooserAction = PendingIntent.getBroadcast(context, requestCode,
                 new Intent(context, GlobalScreenshot.TargetChosenReceiver.class),
                 PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT);
@@ -288,7 +290,15 @@
         Notification.Action.Builder shareActionBuilder = new Notification.Action.Builder(
                 Icon.createWithResource(r, R.drawable.ic_screenshot_share),
                 r.getString(com.android.internal.R.string.share), shareAction);
-        actions.add(shareActionBuilder.build());
+
+        return shareActionBuilder.build();
+    }
+
+    @VisibleForTesting
+    Notification.Action createEditAction(Context context, Resources r, Uri uri) {
+        // Note: Both the share and edit actions are proxied through ActionProxyReceiver in
+        // order to do some common work like dismissing the keyguard and sending
+        // closeSystemWindows
 
         // Create an edit intent, if a specific package is provided as the editor, then
         // launch that directly
@@ -302,6 +312,10 @@
         editIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
         editIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
 
+        // Make sure pending intents for the system user are still unique across users
+        // by setting the (otherwise unused) request code to the current user id.
+        int requestCode = mContext.getUserId();
+
         // Create a edit action
         PendingIntent editAction = PendingIntent.getBroadcastAsUser(context, requestCode,
                 new Intent(context, GlobalScreenshot.ActionProxyReceiver.class)
@@ -317,24 +331,30 @@
         Notification.Action.Builder editActionBuilder = new Notification.Action.Builder(
                 Icon.createWithResource(r, R.drawable.ic_screenshot_edit),
                 r.getString(com.android.internal.R.string.screenshot_edit), editAction);
-        actions.add(editActionBuilder.build());
 
-        if (mCreateDeleteAction) {
-            // Create a delete action for the notification
-            PendingIntent deleteAction = PendingIntent.getBroadcast(context, requestCode,
-                    new Intent(context, GlobalScreenshot.DeleteScreenshotReceiver.class)
-                            .putExtra(GlobalScreenshot.SCREENSHOT_URI_ID, uri.toString())
-                            .putExtra(GlobalScreenshot.EXTRA_ID, mScreenshotId)
-                            .putExtra(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED,
-                                    mSmartActionsEnabled)
-                            .addFlags(Intent.FLAG_RECEIVER_FOREGROUND),
-                    PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT);
-            Notification.Action.Builder deleteActionBuilder = new Notification.Action.Builder(
-                    Icon.createWithResource(r, R.drawable.ic_screenshot_delete),
-                    r.getString(com.android.internal.R.string.delete), deleteAction);
-            actions.add(deleteActionBuilder.build());
-        }
-        return actions;
+        return editActionBuilder.build();
+    }
+
+    @VisibleForTesting
+    Notification.Action createDeleteAction(Context context, Resources r, Uri uri) {
+        // Make sure pending intents for the system user are still unique across users
+        // by setting the (otherwise unused) request code to the current user id.
+        int requestCode = mContext.getUserId();
+
+        // Create a delete action for the notification
+        PendingIntent deleteAction = PendingIntent.getBroadcast(context, requestCode,
+                new Intent(context, GlobalScreenshot.DeleteScreenshotReceiver.class)
+                        .putExtra(GlobalScreenshot.SCREENSHOT_URI_ID, uri.toString())
+                        .putExtra(GlobalScreenshot.EXTRA_ID, mScreenshotId)
+                        .putExtra(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED,
+                                mSmartActionsEnabled)
+                        .addFlags(Intent.FLAG_RECEIVER_FOREGROUND),
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT);
+        Notification.Action.Builder deleteActionBuilder = new Notification.Action.Builder(
+                Icon.createWithResource(r, R.drawable.ic_screenshot_delete),
+                r.getString(com.android.internal.R.string.delete), deleteAction);
+
+        return deleteActionBuilder.build();
     }
 
     private int getUserHandleOfForegroundApplication(Context context) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java
new file mode 100644
index 0000000..20fa991
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java
@@ -0,0 +1,89 @@
+/*
+ * 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.screenshot;
+
+import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_ACCESSIBILITY_ACTIONS;
+import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_GLOBAL_ACTIONS;
+import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD;
+import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER;
+import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OTHER;
+import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OVERVIEW;
+
+import com.android.internal.logging.UiEvent;
+import com.android.internal.logging.UiEventLogger;
+
+public enum ScreenshotEvent implements UiEventLogger.UiEventEnum {
+    @UiEvent(doc = "screenshot requested from global actions")
+    SCREENSHOT_REQUESTED_GLOBAL_ACTIONS(302),
+    @UiEvent(doc = "screenshot requested from key chord")
+    SCREENSHOT_REQUESTED_KEY_CHORD(303),
+    @UiEvent(doc = "screenshot requested from other key press (e.g. ctrl-s)")
+    SCREENSHOT_REQUESTED_KEY_OTHER(384),
+    @UiEvent(doc = "screenshot requested from overview")
+    SCREENSHOT_REQUESTED_OVERVIEW(304),
+    @UiEvent(doc = "screenshot requested from accessibility actions")
+    SCREENSHOT_REQUESTED_ACCESSIBILITY_ACTIONS(382),
+    @UiEvent(doc = "screenshot requested (other)")
+    SCREENSHOT_REQUESTED_OTHER(305),
+    @UiEvent(doc = "screenshot was saved")
+    SCREENSHOT_SAVED(306),
+    @UiEvent(doc = "screenshot failed to save")
+    SCREENSHOT_NOT_SAVED(336),
+    @UiEvent(doc = "screenshot preview tapped")
+    SCREENSHOT_PREVIEW_TAPPED(307),
+    @UiEvent(doc = "screenshot edit button tapped")
+    SCREENSHOT_EDIT_TAPPED(308),
+    @UiEvent(doc = "screenshot share button tapped")
+    SCREENSHOT_SHARE_TAPPED(309),
+    @UiEvent(doc = "screenshot smart action chip tapped")
+    SCREENSHOT_SMART_ACTION_TAPPED(374),
+    @UiEvent(doc = "screenshot scroll tapped")
+    SCREENSHOT_SCROLL_TAPPED(373),
+    @UiEvent(doc = "screenshot interaction timed out")
+    SCREENSHOT_INTERACTION_TIMEOUT(310),
+    @UiEvent(doc = "screenshot explicitly dismissed")
+    SCREENSHOT_EXPLICIT_DISMISSAL(311);
+
+    private final int mId;
+
+    ScreenshotEvent(int id) {
+        mId = id;
+    }
+
+    @Override
+    public int getId() {
+        return mId;
+    }
+
+    static ScreenshotEvent getScreenshotSource(int source) {
+        switch (source) {
+            case SCREENSHOT_GLOBAL_ACTIONS:
+                return ScreenshotEvent.SCREENSHOT_REQUESTED_GLOBAL_ACTIONS;
+            case SCREENSHOT_KEY_CHORD:
+                return ScreenshotEvent.SCREENSHOT_REQUESTED_KEY_CHORD;
+            case SCREENSHOT_KEY_OTHER:
+                return ScreenshotEvent.SCREENSHOT_REQUESTED_KEY_OTHER;
+            case SCREENSHOT_OVERVIEW:
+                return ScreenshotEvent.SCREENSHOT_REQUESTED_OVERVIEW;
+            case SCREENSHOT_ACCESSIBILITY_ACTIONS:
+                return ScreenshotEvent.SCREENSHOT_REQUESTED_ACCESSIBILITY_ACTIONS;
+            case SCREENSHOT_OTHER:
+            default:
+                return ScreenshotEvent.SCREENSHOT_REQUESTED_OTHER;
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationsController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationsController.java
index 811a8d9..fbcd6ba 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationsController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationsController.java
@@ -32,7 +32,6 @@
 import android.graphics.Matrix;
 import android.graphics.Paint;
 import android.graphics.Picture;
-import android.net.Uri;
 import android.os.UserHandle;
 import android.util.DisplayMetrics;
 import android.view.WindowManager;
@@ -42,8 +41,6 @@
 import com.android.systemui.SystemUI;
 import com.android.systemui.util.NotificationChannels;
 
-import java.util.List;
-
 import javax.inject.Inject;
 
 /**
@@ -185,23 +182,20 @@
     /**
      * Shows a notification with the saved screenshot and actions that can be taken with it.
      *
-     * @param imageUri URI for the saved image
-     * @param actions  a list of notification actions which can be taken
+     * @param actionData SavedImageData struct with image URI and actions
      */
     public void showScreenshotActionsNotification(
-            Uri imageUri,
-            List<Notification.Action> smartActions,
-            List<Notification.Action> actions) {
-        for (Notification.Action action : actions) {
-            mNotificationBuilder.addAction(action);
-        }
-        for (Notification.Action smartAction : smartActions) {
+            GlobalScreenshot.SavedImageData actionData) {
+        mNotificationBuilder.addAction(actionData.shareAction);
+        mNotificationBuilder.addAction(actionData.editAction);
+        mNotificationBuilder.addAction(actionData.deleteAction);
+        for (Notification.Action smartAction : actionData.smartActions) {
             mNotificationBuilder.addAction(smartAction);
         }
 
         // Create the intent to show the screenshot in gallery
         Intent launchIntent = new Intent(Intent.ACTION_VIEW);
-        launchIntent.setDataAndType(imageUri, "image/png");
+        launchIntent.setDataAndType(actionData.uri, "image/png");
         launchIntent.setFlags(
                 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION);
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
new file mode 100644
index 0000000..5ced40c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
@@ -0,0 +1,61 @@
+/*
+ * 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.screenshot;
+
+import android.os.IBinder;
+import android.view.IWindowManager;
+
+import javax.inject.Inject;
+
+/**
+ * Stub
+ */
+public class ScrollCaptureController {
+
+    public static final int STATUS_A = 0;
+    public static final int STATUS_B = 1;
+
+    private final IWindowManager mWindowManagerService;
+    private StatusListener mListener;
+
+    /**
+     *
+     * @param windowManagerService
+     */
+    @Inject
+    public ScrollCaptureController(IWindowManager windowManagerService) {
+        mWindowManagerService = windowManagerService;
+    }
+
+    interface StatusListener {
+        void onScrollCaptureStatus(boolean available);
+    }
+
+    /**
+     *
+     * @param window
+     * @param listener
+     */
+    public void getStatus(IBinder window, StatusListener listener) {
+        mListener = listener;
+//        try {
+//           mWindowManagerService.requestScrollCapture(window, new ClientCallbacks());
+//        } catch (RemoteException e) {
+//        }
+    }
+
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
index 8b8b6f8..f68cb74 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
@@ -32,6 +32,9 @@
 import android.util.Log;
 import android.view.WindowManager;
 
+import com.android.internal.logging.UiEventLogger;
+import com.android.internal.util.ScreenshotHelper;
+
 import java.util.function.Consumer;
 
 import javax.inject.Inject;
@@ -42,6 +45,7 @@
     private final GlobalScreenshot mScreenshot;
     private final GlobalScreenshotLegacy mScreenshotLegacy;
     private final UserManager mUserManager;
+    private final UiEventLogger mUiEventLogger;
 
     private Handler mHandler = new Handler(Looper.myLooper()) {
         @Override
@@ -64,14 +68,22 @@
                 return;
             }
 
-            // TODO (mkephart): clean up once notifications flow is fully deprecated
+            // TODO: clean up once notifications flow is fully deprecated
             boolean useCornerFlow = true;
+
+            ScreenshotHelper.ScreenshotRequest screenshotRequest =
+                    (ScreenshotHelper.ScreenshotRequest) msg.obj;
+
+            mUiEventLogger.log(ScreenshotEvent.getScreenshotSource(screenshotRequest.getSource()));
+
             switch (msg.what) {
                 case WindowManager.TAKE_SCREENSHOT_FULLSCREEN:
                     if (useCornerFlow) {
                         mScreenshot.takeScreenshot(finisher);
                     } else {
-                        mScreenshotLegacy.takeScreenshot(finisher, msg.arg1 > 0, msg.arg2 > 0);
+                        mScreenshotLegacy.takeScreenshot(
+                                finisher, screenshotRequest.getHasStatusBar(),
+                                screenshotRequest.getHasNavBar());
                     }
                     break;
                 case WindowManager.TAKE_SCREENSHOT_SELECTED_REGION:
@@ -79,17 +91,15 @@
                         mScreenshot.takeScreenshotPartial(finisher);
                     } else {
                         mScreenshotLegacy.takeScreenshotPartial(
-                                finisher, msg.arg1 > 0, msg.arg2 > 0);
+                                finisher, screenshotRequest.getHasStatusBar(),
+                                screenshotRequest.getHasNavBar());
                     }
                     break;
                 case WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE:
-                    Bitmap screenshot = msg.getData().getParcelable(
-                            WindowManager.PARCEL_KEY_SCREENSHOT_BITMAP);
-                    Rect screenBounds = msg.getData().getParcelable(
-                            WindowManager.PARCEL_KEY_SCREENSHOT_BOUNDS);
-                    Insets insets = msg.getData().getParcelable(
-                            WindowManager.PARCEL_KEY_SCREENSHOT_INSETS);
-                    int taskId = msg.getData().getInt(WindowManager.PARCEL_KEY_SCREENSHOT_TASK_ID);
+                    Bitmap screenshot = screenshotRequest.getBitmap();
+                    Rect screenBounds = screenshotRequest.getBoundsInScreen();
+                    Insets insets = screenshotRequest.getInsets();
+                    int taskId = screenshotRequest.getTaskId();
                     if (useCornerFlow) {
                         mScreenshot.handleImageAsScreenshot(
                                 screenshot, screenBounds, insets, taskId, finisher);
@@ -106,10 +116,12 @@
 
     @Inject
     public TakeScreenshotService(GlobalScreenshot globalScreenshot,
-            GlobalScreenshotLegacy globalScreenshotLegacy, UserManager userManager) {
+            GlobalScreenshotLegacy globalScreenshotLegacy, UserManager userManager,
+            UiEventLogger uiEventLogger) {
         mScreenshot = globalScreenshot;
         mScreenshotLegacy = globalScreenshotLegacy;
         mUserManager = userManager;
+        mUiEventLogger = uiEventLogger;
     }
 
     @Override
@@ -120,7 +132,7 @@
     @Override
     public boolean onUnbind(Intent intent) {
         if (mScreenshot != null) mScreenshot.stopScreenshot();
-        // TODO (mkephart) remove once notifications flow is fully deprecated
+        // TODO remove once notifications flow is fully deprecated
         if (mScreenshotLegacy != null) mScreenshotLegacy.stopScreenshot();
         return true;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/settings/CurrentUserContextTracker.kt b/packages/SystemUI/src/com/android/systemui/settings/CurrentUserContextTracker.kt
index fa1b026..825a7f3 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/CurrentUserContextTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/CurrentUserContextTracker.kt
@@ -18,21 +18,31 @@
 
 import android.content.Context
 import android.os.UserHandle
+import androidx.annotation.VisibleForTesting
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.util.Assert
-import javax.inject.Inject
-import javax.inject.Singleton
+import java.lang.IllegalStateException
 
 /**
  * Tracks a reference to the context for the current user
+ *
+ * Constructor is injected at SettingsModule
  */
-@Singleton
-class CurrentUserContextTracker @Inject constructor(
+class CurrentUserContextTracker internal constructor(
     private val sysuiContext: Context,
     broadcastDispatcher: BroadcastDispatcher
 ) {
     private val userTracker: CurrentUserTracker
-    var currentUserContext: Context
+    private var initialized = false
+
+    private var _curUserContext: Context? = null
+    val currentUserContext: Context
+        get() {
+            if (!initialized) {
+                throw IllegalStateException("Must initialize before getting context")
+            }
+            return _curUserContext!!
+        }
 
     init {
         userTracker = object : CurrentUserTracker(broadcastDispatcher) {
@@ -40,21 +50,21 @@
                 handleUserSwitched(newUserId)
             }
         }
-
-        currentUserContext = makeUserContext(userTracker.currentUserId)
     }
 
     fun initialize() {
+        initialized = true
+        _curUserContext = makeUserContext(userTracker.currentUserId)
         userTracker.startTracking()
     }
 
-    private fun handleUserSwitched(newUserId: Int) {
-        currentUserContext = makeUserContext(newUserId)
+    @VisibleForTesting
+    fun handleUserSwitched(newUserId: Int) {
+        _curUserContext = makeUserContext(newUserId)
     }
 
     private fun makeUserContext(uid: Int): Context {
         Assert.isMainThread()
-        return sysuiContext.createContextAsUser(
-                UserHandle.getUserHandleForUid(userTracker.currentUserId), 0)
+        return sysuiContext.createContextAsUser(UserHandle.of(uid), 0)
     }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/settings/dagger/SettingsModule.java b/packages/SystemUI/src/com/android/systemui/settings/dagger/SettingsModule.java
new file mode 100644
index 0000000..2c5c3ce
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/settings/dagger/SettingsModule.java
@@ -0,0 +1,48 @@
+/*
+ * 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.settings.dagger;
+
+import android.content.Context;
+
+import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.settings.CurrentUserContextTracker;
+
+import javax.inject.Singleton;
+
+import dagger.Module;
+import dagger.Provides;
+
+/**
+ * Dagger Module for classes found within the com.android.systemui.settings package.
+ */
+@Module
+public interface SettingsModule {
+
+    /**
+     * Provides and initializes a CurrentUserContextTracker
+     */
+    @Singleton
+    @Provides
+    static CurrentUserContextTracker provideCurrentUserContextTracker(
+            Context context,
+            BroadcastDispatcher broadcastDispatcher) {
+        CurrentUserContextTracker tracker =
+                new CurrentUserContextTracker(context, broadcastDispatcher);
+        tracker.initialize();
+        return tracker;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/SplitScreenTaskOrganizer.java b/packages/SystemUI/src/com/android/systemui/stackdivider/SplitScreenTaskOrganizer.java
index a4b1310..f5d6cb6 100644
--- a/packages/SystemUI/src/com/android/systemui/stackdivider/SplitScreenTaskOrganizer.java
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/SplitScreenTaskOrganizer.java
@@ -33,10 +33,8 @@
 import android.view.SurfaceSession;
 import android.window.TaskOrganizer;
 
-import java.util.ArrayList;
-
 class SplitScreenTaskOrganizer extends TaskOrganizer {
-    private static final String TAG = "SplitScreenTaskOrganizer";
+    private static final String TAG = "SplitScreenTaskOrg";
     private static final boolean DEBUG = Divider.DEBUG;
 
     RunningTaskInfo mPrimary;
@@ -45,7 +43,6 @@
     SurfaceControl mSecondarySurface;
     SurfaceControl mPrimaryDim;
     SurfaceControl mSecondaryDim;
-    ArrayList<SurfaceControl> mHomeAndRecentsSurfaces = new ArrayList<>();
     Rect mHomeBounds = new Rect();
     final Divider mDivider;
     private boolean mSplitScreenSupported = false;
@@ -110,6 +107,15 @@
      * presentations based on the contents of the split regions.
      */
     private void handleTaskInfoChanged(RunningTaskInfo info) {
+        if (!mSplitScreenSupported) {
+            // This shouldn't happen; but apparently there is a chance that SysUI crashes without
+            // system server receiving binder-death (or maybe it receives binder-death too late?).
+            // In this situation, when sys-ui restarts, the split root-tasks will still exist so
+            // there is a small window of time during init() where WM might send messages here
+            // before init() fails. So, avoid a cycle of crashes by returning early.
+            Log.e(TAG, "Got handleTaskInfoChanged when not initialized: " + info);
+            return;
+        }
         final boolean secondaryWasHomeOrRecents = mSecondary.topActivityType == ACTIVITY_TYPE_HOME
                 || mSecondary.topActivityType == ACTIVITY_TYPE_RECENTS;
         final boolean primaryWasEmpty = mPrimary.topActivityType == ACTIVITY_TYPE_UNDEFINED;
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java b/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java
index 85dcbb6..3027bd2 100644
--- a/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java
@@ -174,18 +174,19 @@
         if (rootTasks.isEmpty()) {
             return false;
         }
-        tiles.mHomeAndRecentsSurfaces.clear();
         for (int i = rootTasks.size() - 1; i >= 0; --i) {
             final ActivityManager.RunningTaskInfo rootTask = rootTasks.get(i);
-            if (isHomeOrRecentTask(rootTask)) {
-                tiles.mHomeAndRecentsSurfaces.add(rootTask.token.getLeash());
-            }
+            // Only move resizeable task to split secondary. WM will just ignore this anyways...
+            if (!rootTask.isResizable()) continue;
+            // Only move fullscreen tasks to split secondary.
             if (rootTask.configuration.windowConfiguration.getWindowingMode()
                     != WINDOWING_MODE_FULLSCREEN) {
                 continue;
             }
             wct.reparent(rootTask.token, tiles.mSecondary.token, true /* onTop */);
         }
+        // Move the secondary split-forward.
+        wct.reorder(tiles.mSecondary.token, true /* onTop */);
         boolean isHomeResizable = applyHomeTasksMinimized(layout, null /* parent */, wct);
         WindowOrganizer.applyTransaction(wct);
         return isHomeResizable;
@@ -206,7 +207,6 @@
         // Set launch root first so that any task created after getChildContainers and
         // before reparent (pretty unlikely) are put into fullscreen.
         TaskOrganizer.setLaunchRoot(Display.DEFAULT_DISPLAY, null);
-        tiles.mHomeAndRecentsSurfaces.clear();
         // TODO(task-org): Once task-org is more complete, consider using Appeared/Vanished
         //                 plus specific APIs to clean this up.
         List<ActivityManager.RunningTaskInfo> primaryChildren =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NavigationBarController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NavigationBarController.java
index 1b752452..8c24c54 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NavigationBarController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NavigationBarController.java
@@ -44,6 +44,7 @@
 import com.android.systemui.statusbar.phone.LightBarController;
 import com.android.systemui.statusbar.phone.NavigationBarFragment;
 import com.android.systemui.statusbar.phone.NavigationBarView;
+import com.android.systemui.statusbar.phone.NavigationModeController;
 import com.android.systemui.statusbar.policy.BatteryController;
 
 import javax.inject.Inject;
@@ -139,7 +140,8 @@
                     ? Dependency.get(LightBarController.class)
                     : new LightBarController(context,
                             Dependency.get(DarkIconDispatcher.class),
-                            Dependency.get(BatteryController.class));
+                            Dependency.get(BatteryController.class),
+                            Dependency.get(NavigationModeController.class));
             navBar.setLightBarController(lightBarController);
 
             // TODO(b/118592525): to support multi-display, we start to add something which is
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
index 4e6df0a..d364689 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
@@ -23,11 +23,14 @@
 
 import com.android.systemui.DejankUtils;
 import com.android.systemui.bubbles.BubbleController;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.phone.StatusBar;
 
 import java.util.Optional;
 
+import javax.inject.Inject;
+
 /**
  * Click handler for generic clicks on notifications. Clicks on specific areas (expansion caret,
  * app ops icon, etc) are handled elsewhere.
@@ -35,15 +38,19 @@
 public final class NotificationClicker implements View.OnClickListener {
     private static final String TAG = "NotificationClicker";
 
-    private final Optional<StatusBar> mStatusBar;
     private final BubbleController mBubbleController;
+    private final NotificationClickerLogger mLogger;
+    private final Optional<StatusBar> mStatusBar;
     private final NotificationActivityStarter mNotificationActivityStarter;
 
-    public NotificationClicker(Optional<StatusBar> statusBar,
+    private NotificationClicker(
             BubbleController bubbleController,
+            NotificationClickerLogger logger,
+            Optional<StatusBar> statusBar,
             NotificationActivityStarter notificationActivityStarter) {
-        mStatusBar = statusBar;
         mBubbleController = bubbleController;
+        mLogger = logger;
+        mStatusBar = statusBar;
         mNotificationActivityStarter = notificationActivityStarter;
     }
 
@@ -58,25 +65,26 @@
                 SystemClock.uptimeMillis(), v, "NOTIFICATION_CLICK"));
 
         final ExpandableNotificationRow row = (ExpandableNotificationRow) v;
-        final StatusBarNotification sbn = row.getEntry().getSbn();
-        if (sbn == null) {
-            Log.e(TAG, "NotificationClicker called on an unclickable notification,");
-            return;
-        }
+        final NotificationEntry entry = row.getEntry();
+        mLogger.logOnClick(entry);
 
         // Check if the notification is displaying the menu, if so slide notification back
         if (isMenuVisible(row)) {
+            mLogger.logMenuVisible(entry);
             row.animateTranslateNotification(0);
             return;
         } else if (row.isChildInGroup() && isMenuVisible(row.getNotificationParent())) {
+            mLogger.logParentMenuVisible(entry);
             row.getNotificationParent().animateTranslateNotification(0);
             return;
         } else if (row.isSummaryWithChildren() && row.areChildrenExpanded()) {
             // We never want to open the app directly if the user clicks in between
             // the notifications.
+            mLogger.logChildrenExpanded(entry);
             return;
         } else if (row.areGutsExposed()) {
             // ignore click if guts are exposed
+            mLogger.logGutsExposed(entry);
             return;
         }
 
@@ -88,7 +96,7 @@
             mBubbleController.collapseStack();
         }
 
-        mNotificationActivityStarter.onNotificationClicked(sbn, row);
+        mNotificationActivityStarter.onNotificationClicked(entry.getSbn(), row);
     }
 
     private boolean isMenuVisible(ExpandableNotificationRow row) {
@@ -107,4 +115,30 @@
             row.setOnClickListener(null);
         }
     }
+
+    /** Daggerized builder for NotificationClicker. */
+    public static class Builder {
+        private final BubbleController mBubbleController;
+        private final NotificationClickerLogger mLogger;
+
+        @Inject
+        public Builder(
+                BubbleController bubbleController,
+                NotificationClickerLogger logger) {
+            mBubbleController = bubbleController;
+            mLogger = logger;
+        }
+
+        /** Builds an instance. */
+        public NotificationClicker build(
+                Optional<StatusBar> statusBar,
+                NotificationActivityStarter notificationActivityStarter
+        ) {
+            return new NotificationClicker(
+                    mBubbleController,
+                    mLogger,
+                    statusBar,
+                    notificationActivityStarter);
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt
new file mode 100644
index 0000000..fbf033b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.statusbar.notification
+
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogLevel
+import com.android.systemui.log.dagger.NotifInteractionLog
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import javax.inject.Inject
+
+class NotificationClickerLogger @Inject constructor(
+    @NotifInteractionLog private val buffer: LogBuffer
+) {
+    fun logOnClick(entry: NotificationEntry) {
+        buffer.log(TAG, LogLevel.DEBUG, {
+            str1 = entry.key
+            str2 = entry.ranking.channel.id
+        }, {
+            "CLICK $str1 (channel=$str2)"
+        })
+    }
+
+    fun logMenuVisible(entry: NotificationEntry) {
+        buffer.log(TAG, LogLevel.DEBUG, {
+            str1 = entry.key
+        }, {
+            "Ignoring click on $str1; menu is visible"
+        })
+    }
+
+    fun logParentMenuVisible(entry: NotificationEntry) {
+        buffer.log(TAG, LogLevel.DEBUG, {
+            str1 = entry.key
+        }, {
+            "Ignoring click on $str1; parent menu is visible"
+        })
+    }
+
+    fun logChildrenExpanded(entry: NotificationEntry) {
+        buffer.log(TAG, LogLevel.DEBUG, {
+            str1 = entry.key
+        }, {
+            "Ignoring click on $str1; children are expanded"
+        })
+    }
+
+    fun logGutsExposed(entry: NotificationEntry) {
+        buffer.log(TAG, LogLevel.DEBUG, {
+            str1 = entry.key
+        }, {
+            "Ignoring click on $str1; guts are exposed"
+        })
+    }
+}
+
+private const val TAG = "NotificationClicker"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index 5236385..cb0c283 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -378,6 +378,7 @@
     /**
      * Returns the data needed for a bubble for this notification, if it exists.
      */
+    @Nullable
     public Notification.BubbleMetadata getBubbleMetadata() {
         return mBubbleMetadata;
     }
@@ -385,7 +386,7 @@
     /**
      * Sets bubble metadata for this notification.
      */
-    public void setBubbleMetadata(Notification.BubbleMetadata metadata) {
+    public void setBubbleMetadata(@Nullable Notification.BubbleMetadata metadata) {
         mBubbleMetadata = metadata;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
index 7f2f898..c975404 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.statusbar.notification.init
 
 import android.service.notification.StatusBarNotification
-import com.android.systemui.bubbles.BubbleController
 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption
 import com.android.systemui.statusbar.FeatureFlags
 import com.android.systemui.statusbar.NotificationListener
@@ -62,12 +61,12 @@
     private val deviceProvisionedController: DeviceProvisionedController,
     private val notificationRowBinder: NotificationRowBinderImpl,
     private val remoteInputUriController: RemoteInputUriController,
-    private val bubbleController: BubbleController,
     private val groupManager: NotificationGroupManager,
     private val groupAlertTransferHelper: NotificationGroupAlertTransferHelper,
     private val headsUpManager: HeadsUpManager,
     private val headsUpController: HeadsUpController,
-    private val headsUpViewBinder: HeadsUpViewBinder
+    private val headsUpViewBinder: HeadsUpViewBinder,
+    private val clickerBuilder: NotificationClicker.Builder
 ) : NotificationsController {
 
     override fun initialize(
@@ -87,10 +86,7 @@
         listController.bind()
 
         notificationRowBinder.setNotificationClicker(
-                NotificationClicker(
-                        Optional.of(statusBar),
-                        bubbleController,
-                        notificationActivityStarter))
+                clickerBuilder.build(Optional.of(statusBar), notificationActivityStarter))
         notificationRowBinder.setUpWithPresenter(
                 presenter,
                 listContainer,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpController.java
index 6d14ccf..9b6ae9a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpController.java
@@ -175,7 +175,7 @@
     private OnHeadsUpChangedListener mOnHeadsUpChangedListener  = new OnHeadsUpChangedListener() {
         @Override
         public void onHeadsUpStateChanged(@NonNull NotificationEntry entry, boolean isHeadsUp) {
-            if (!isHeadsUp) {
+            if (!isHeadsUp && !entry.getRow().isRemoved()) {
                 mHeadsUpViewBinder.unbindHeadsUpView(entry);
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java
index 55a5935..bcc81a8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java
@@ -31,6 +31,7 @@
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.INotificationManager;
 import android.app.Notification;
 import android.app.NotificationChannel;
@@ -98,7 +99,7 @@
     private ShortcutInfo mShortcutInfo;
     private String mConversationId;
     private StatusBarNotification mSbn;
-    private Notification.BubbleMetadata mBubbleMetadata;
+    @Nullable private Notification.BubbleMetadata mBubbleMetadata;
     private Context mUserContext;
     private Provider<PriorityOnboardingDialogController.Builder> mBuilderProvider;
     private boolean mIsDeviceProvisioned;
@@ -203,6 +204,7 @@
             String pkg,
             NotificationChannel notificationChannel,
             NotificationEntry entry,
+            Notification.BubbleMetadata bubbleMetadata,
             OnSettingsClickListener onSettingsClick,
             OnSnoozeClickListener onSnoozeClickListener,
             ConversationIconFactory conversationIconFactory,
@@ -224,7 +226,7 @@
         mOnSnoozeClickListener = onSnoozeClickListener;
         mIconFactory = conversationIconFactory;
         mUserContext = userContext;
-        mBubbleMetadata = entry.getBubbleMetadata();
+        mBubbleMetadata = bubbleMetadata;
         mBuilderProvider = builderProvider;
 
         mShortcutManager = shortcutManager;
@@ -538,7 +540,8 @@
             Log.e(TAG, "Could not check conversation senders", e);
         }
 
-        boolean showAsBubble = mBubbleMetadata.getAutoExpandBubble()
+        boolean showAsBubble = mBubbleMetadata != null
+                && mBubbleMetadata.getAutoExpandBubble()
                 && Settings.Global.getInt(mContext.getContentResolver(),
                         NOTIFICATION_BUBBLES, 0) == 1;
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
index 624fabc..1c808cf9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
@@ -366,7 +366,8 @@
             final ExpandableNotificationRow row,
             NotificationConversationInfo notificationInfoView) throws Exception {
         NotificationGuts guts = row.getGuts();
-        StatusBarNotification sbn = row.getEntry().getSbn();
+        NotificationEntry entry = row.getEntry();
+        StatusBarNotification sbn = entry.getSbn();
         String packageName = sbn.getPackageName();
         // Settings link is only valid for notifications that specify a non-system user
         NotificationConversationInfo.OnSettingsClickListener onSettingsClick = null;
@@ -407,8 +408,9 @@
                 mNotificationManager,
                 mVisualStabilityManager,
                 packageName,
-                row.getEntry().getChannel(),
-                row.getEntry(),
+                entry.getChannel(),
+                entry,
+                entry.getBubbleMetadata(),
                 onSettingsClick,
                 onSnoozeClickListener,
                 iconFactoryLoader,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java
index 2da2724..b96cff8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java
@@ -22,6 +22,7 @@
 import android.app.Notification;
 import android.content.Context;
 import android.content.res.ColorStateList;
+import android.graphics.drawable.Drawable;
 import android.media.MediaMetadata;
 import android.media.session.MediaController;
 import android.media.session.MediaSession;
@@ -187,8 +188,10 @@
                     com.android.systemui.R.id.quick_qs_panel);
             StatusBarNotification sbn = mRow.getEntry().getSbn();
             Notification notif = sbn.getNotification();
+            Drawable iconDrawable = notif.getSmallIcon().loadDrawable(mContext);
             panel.getMediaPlayer().setMediaSession(token,
-                    notif.getSmallIcon(),
+                    iconDrawable,
+                    notif.getLargeIcon(),
                     tintColor,
                     mBackgroundColor,
                     mActions,
@@ -198,7 +201,8 @@
             QSPanel bigPanel = ctrl.getNotificationShadeView().findViewById(
                     com.android.systemui.R.id.quick_settings_panel);
             bigPanel.addMediaSession(token,
-                    notif.getSmallIcon(),
+                    iconDrawable,
+                    notif.getLargeIcon(),
                     tintColor,
                     mBackgroundColor,
                     mActions,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java
index 7d422e3..f9119c7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java
@@ -53,11 +53,13 @@
 import com.android.internal.policy.GestureNavigationSettingsObserver;
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
+import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.bubbles.BubbleController;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.plugins.NavigationEdgeBackPlugin;
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.recents.OverviewProxyService;
+import com.android.systemui.settings.CurrentUserTracker;
 import com.android.systemui.shared.plugins.PluginManager;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
@@ -74,7 +76,7 @@
 /**
  * Utility class to handle edge swipes for back gesture
  */
-public class EdgeBackGestureHandler implements DisplayListener,
+public class EdgeBackGestureHandler extends CurrentUserTracker implements DisplayListener,
         PluginListener<NavigationEdgeBackPlugin>, ProtoTraceable<SystemUiTraceProto> {
 
     private static final String TAG = "EdgeBackGestureHandler";
@@ -165,6 +167,7 @@
     private boolean mIsGesturalModeEnabled;
     private boolean mIsEnabled;
     private boolean mIsNavBarShownTransiently;
+    private boolean mIsBackGestureAllowed;
 
     private InputMonitor mInputMonitor;
     private InputEventReceiver mInputEventReceiver;
@@ -200,7 +203,7 @@
 
     public EdgeBackGestureHandler(Context context, OverviewProxyService overviewProxyService,
             SysUiState sysUiFlagContainer, PluginManager pluginManager) {
-        final Resources res = context.getResources();
+        super(Dependency.get(BroadcastDispatcher.class));
         mContext = context;
         mDisplayId = context.getDisplayId();
         mMainExecutor = context.getMainExecutor();
@@ -216,20 +219,30 @@
                 ViewConfiguration.getLongPressTimeout());
 
         mGestureNavigationSettingsObserver = new GestureNavigationSettingsObserver(
-                mContext.getMainThreadHandler(), mContext, () -> updateCurrentUserResources(res));
+                mContext.getMainThreadHandler(), mContext, this::updateCurrentUserResources);
 
-        updateCurrentUserResources(res);
+        updateCurrentUserResources();
         sysUiFlagContainer.addCallback(sysUiFlags -> mSysUiFlags = sysUiFlags);
     }
 
-    public void updateCurrentUserResources(Resources res) {
+    public void updateCurrentUserResources() {
+        Resources res = Dependency.get(NavigationModeController.class).getCurrentUserContext()
+                .getResources();
         mEdgeWidthLeft = mGestureNavigationSettingsObserver.getLeftSensitivity(res);
         mEdgeWidthRight = mGestureNavigationSettingsObserver.getRightSensitivity(res);
+        mIsBackGestureAllowed =
+                !mGestureNavigationSettingsObserver.areNavigationButtonForcedVisible();
 
         mBottomGestureHeight = res.getDimensionPixelSize(
                 com.android.internal.R.dimen.navigation_bar_gesture_height);
     }
 
+    @Override
+    public void onUserSwitched(int newUserId) {
+        updateIsEnabled();
+        updateCurrentUserResources();
+    }
+
     /**
      * @see NavigationBarView#onAttachedToWindow()
      */
@@ -243,6 +256,7 @@
                 Settings.Global.getUriFor(FIXED_ROTATION_TRANSFORM_SETTING_NAME),
                 false /* notifyForDescendants */, mFixedRotationObserver, UserHandle.USER_ALL);
         updateIsEnabled();
+        startTracking();
     }
 
     /**
@@ -255,6 +269,7 @@
         }
         mContext.getContentResolver().unregisterContentObserver(mFixedRotationObserver);
         updateIsEnabled();
+        stopTracking();
     }
 
     private void setRotationCallbacks(boolean enable) {
@@ -269,10 +284,13 @@
         }
     }
 
-    public void onNavigationModeChanged(int mode, Context currentUserContext) {
+    /**
+     * @see NavigationModeController.ModeChangedListener#onNavigationModeChanged
+     */
+    public void onNavigationModeChanged(int mode) {
         mIsGesturalModeEnabled = QuickStepContract.isGesturalMode(mode);
         updateIsEnabled();
-        updateCurrentUserResources(currentUserContext.getResources());
+        updateCurrentUserResources();
     }
 
     public void onNavBarTransientStateChanged(boolean isTransient) {
@@ -363,6 +381,10 @@
         updateDisplaySize();
     }
 
+    public boolean isHandlingGestures() {
+        return mIsEnabled && mIsBackGestureAllowed;
+    }
+
     private WindowManager.LayoutParams createLayoutParams() {
         Resources resources = mContext.getResources();
         WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
@@ -469,9 +491,9 @@
             mIsOnLeftEdge = ev.getX() <= mEdgeWidthLeft + mLeftInset;
             mLogGesture = false;
             mInRejectedExclusion = false;
-            mAllowGesture = !QuickStepContract.isBackGestureDisabled(mSysUiFlags)
-                    && isWithinTouchRegion((int) ev.getX(), (int) ev.getY())
-                    && !mDisabledForQuickstep;
+            mAllowGesture = !mDisabledForQuickstep && mIsBackGestureAllowed
+                    && !QuickStepContract.isBackGestureDisabled(mSysUiFlags)
+                    && isWithinTouchRegion((int) ev.getX(), (int) ev.getY());
             if (mAllowGesture) {
                 mEdgeBackPlugin.setIsLeftPanel(mIsOnLeftEdge);
                 mEdgeBackPlugin.onMotionEvent(ev);
@@ -599,6 +621,7 @@
     public void dump(PrintWriter pw) {
         pw.println("EdgeBackGestureHandler:");
         pw.println("  mIsEnabled=" + mIsEnabled);
+        pw.println("  mIsBackGestureAllowed=" + mIsBackGestureAllowed);
         pw.println("  mAllowGesture=" + mAllowGesture);
         pw.println("  mDisabledForQuickstep=" + mDisabledForQuickstep);
         pw.println("  mInRejectedExclusion" + mInRejectedExclusion);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
index 90bc075b..ae7867d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
@@ -85,6 +85,8 @@
 import com.android.systemui.tuner.LockscreenFragment.LockButtonFactory;
 import com.android.systemui.tuner.TunerService;
 
+import java.util.concurrent.Executor;
+
 /**
  * Implementation for the bottom area of the Keyguard, including camera/phone affordance and status
  * text.
@@ -553,7 +555,7 @@
             }
         };
         if (!mKeyguardStateController.canDismissLockScreen()) {
-            AsyncTask.execute(runnable);
+            Dependency.get(Executor.class).execute(runnable);
         } else {
             boolean dismissShade = !TextUtils.isEmpty(mRightButtonStr)
                     && Dependency.get(TunerService.class).getValue(LOCKSCREEN_RIGHT_UNLOCK, 1) != 0;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
index 82e02b4..39949c8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
@@ -400,6 +400,9 @@
             mExpansionCallback.onFullyHidden();
         } else if (fraction != EXPANSION_VISIBLE && oldExpansion == EXPANSION_VISIBLE) {
             mExpansionCallback.onStartingToHide();
+            if (mKeyguardView != null) {
+                mKeyguardView.onStartingToHide();
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
index d35e1e1..3e5eb5f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
@@ -33,6 +33,7 @@
 import com.android.systemui.Dumpable;
 import com.android.systemui.R;
 import com.android.systemui.plugins.DarkIconDispatcher;
+import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.statusbar.policy.BatteryController;
 
 import java.io.FileDescriptor;
@@ -58,6 +59,7 @@
     private AppearanceRegion[] mAppearanceRegions = new AppearanceRegion[0];
     private int mStatusBarMode;
     private int mNavigationBarMode;
+    private int mNavigationMode;
     private final Color mDarkModeColor;
 
     /**
@@ -84,11 +86,14 @@
 
     @Inject
     public LightBarController(Context ctx, DarkIconDispatcher darkIconDispatcher,
-            BatteryController batteryController) {
+            BatteryController batteryController, NavigationModeController navModeController) {
         mDarkModeColor = Color.valueOf(ctx.getColor(R.color.dark_mode_icon_color_single_tone));
         mStatusBarIconController = (SysuiDarkIconDispatcher) darkIconDispatcher;
         mBatteryController = batteryController;
         mBatteryController.addCallback(this);
+        mNavigationMode = navModeController.addListener((mode) -> {
+            mNavigationMode = mode;
+        });
     }
 
     public void setNavigationBar(LightBarTransitionsController navigationBar) {
@@ -234,7 +239,8 @@
     }
 
     private void updateNavigation() {
-        if (mNavigationBarController != null) {
+        if (mNavigationBarController != null
+                && !QuickStepContract.isGesturalMode(mNavigationMode)) {
             mNavigationBarController.setIconsDark(mNavigationLight, animateChange());
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
index 84aecd4..2978772 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
@@ -123,7 +123,7 @@
     private KeyButtonDrawable mRecentIcon;
     private KeyButtonDrawable mDockedIcon;
 
-    private final EdgeBackGestureHandler mEdgeBackGestureHandler;
+    private EdgeBackGestureHandler mEdgeBackGestureHandler;
     private final DeadZone mDeadZone;
     private boolean mDeadZoneConsuming = false;
     private final NavigationBarTransitions mBarTransitions;
@@ -244,7 +244,7 @@
     private final OnComputeInternalInsetsListener mOnComputeInternalInsetsListener = info -> {
         // When the nav bar is in 2-button or 3-button mode, or when IME is visible in fully
         // gestural mode, the entire nav bar should be touchable.
-        if (!isGesturalMode(mNavBarMode) || mImeVisible) {
+        if (!mEdgeBackGestureHandler.isHandlingGestures() || mImeVisible) {
             info.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_FRAME);
             return;
         }
@@ -296,8 +296,6 @@
                 R.style.RotateButtonCCWStart90,
                 isGesturalMode ? mFloatingRotationButton : rotateSuggestionButton);
 
-        final ContextualButton backButton = new ContextualButton(R.id.back, 0);
-
         mConfiguration = new Configuration();
         mTmpLastConfiguration = new Configuration();
         mConfiguration.updateFrom(context.getResources().getConfiguration());
@@ -305,7 +303,7 @@
         mScreenPinningNotify = new ScreenPinningNotify(mContext);
         mBarTransitions = new NavigationBarTransitions(this, Dependency.get(CommandQueue.class));
 
-        mButtonDispatchers.put(R.id.back, backButton);
+        mButtonDispatchers.put(R.id.back, new ButtonDispatcher(R.id.back));
         mButtonDispatchers.put(R.id.home, new ButtonDispatcher(R.id.home));
         mButtonDispatchers.put(R.id.home_handle, new ButtonDispatcher(R.id.home_handle));
         mButtonDispatchers.put(R.id.recent_apps, new ButtonDispatcher(R.id.recent_apps));
@@ -659,7 +657,7 @@
         boolean disableHomeHandle = disableRecent
                 && ((mDisabledFlags & View.STATUS_BAR_DISABLE_HOME) != 0);
 
-        boolean disableBack = !useAltBack && (isGesturalMode(mNavBarMode)
+        boolean disableBack = !useAltBack && (mEdgeBackGestureHandler.isHandlingGestures()
                 || ((mDisabledFlags & View.STATUS_BAR_DISABLE_BACK) != 0));
 
         // When screen pinning, don't hide back and home when connected service or back and
@@ -686,9 +684,9 @@
             }
         }
 
-        getBackButton().setVisibility(disableBack      ? View.INVISIBLE : View.VISIBLE);
-        getHomeButton().setVisibility(disableHome      ? View.INVISIBLE : View.VISIBLE);
-        getRecentsButton().setVisibility(disableRecent ? View.INVISIBLE : View.VISIBLE);
+        getBackButton().setVisibility(disableBack       ? View.INVISIBLE : View.VISIBLE);
+        getHomeButton().setVisibility(disableHome       ? View.INVISIBLE : View.VISIBLE);
+        getRecentsButton().setVisibility(disableRecent  ? View.INVISIBLE : View.VISIBLE);
         getHomeHandle().setVisibility(disableHomeHandle ? View.INVISIBLE : View.VISIBLE);
     }
 
@@ -838,10 +836,9 @@
 
     @Override
     public void onNavigationModeChanged(int mode) {
-        Context curUserCtx = Dependency.get(NavigationModeController.class).getCurrentUserContext();
         mNavBarMode = mode;
         mBarTransitions.onNavigationModeChanged(mNavBarMode);
-        mEdgeBackGestureHandler.onNavigationModeChanged(mNavBarMode, curUserCtx);
+        mEdgeBackGestureHandler.onNavigationModeChanged(mNavBarMode);
         mRecentsOnboarding.onNavigationModeChanged(mNavBarMode);
         getRotateSuggestionButton().onNavigationModeChanged(mNavBarMode);
 
@@ -864,6 +861,7 @@
 
     @Override
     public void onFinishInflate() {
+        super.onFinishInflate();
         mNavigationInflaterView = findViewById(R.id.navigation_inflater);
         mNavigationInflaterView.setButtonDispatchers(mButtonDispatchers);
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationModeController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationModeController.java
index d24ccf3..6061b1e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationModeController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationModeController.java
@@ -17,9 +17,6 @@
 package com.android.systemui.statusbar.phone;
 
 import static android.content.Intent.ACTION_OVERLAY_CHANGED;
-import static android.content.Intent.ACTION_PREFERRED_ACTIVITY_CHANGED;
-import static android.os.UserHandle.USER_CURRENT;
-import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY;
 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL;
 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY;
 
@@ -38,17 +35,14 @@
 import android.provider.Settings;
 import android.provider.Settings.Secure;
 import android.util.Log;
-import android.util.SparseBooleanArray;
 
 import com.android.systemui.Dumpable;
 import com.android.systemui.dagger.qualifiers.UiBackground;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
-import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
@@ -70,104 +64,34 @@
     private final Context mContext;
     private Context mCurrentUserContext;
     private final IOverlayManager mOverlayManager;
-    private final DeviceProvisionedController mDeviceProvisionedController;
     private final Executor mUiBgExecutor;
 
-    private SparseBooleanArray mRestoreGesturalNavBarMode = new SparseBooleanArray();
-
     private ArrayList<ModeChangedListener> mListeners = new ArrayList<>();
 
     private BroadcastReceiver mReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
-            switch (intent.getAction()) {
-                case ACTION_OVERLAY_CHANGED:
                     if (DEBUG) {
                         Log.d(TAG, "ACTION_OVERLAY_CHANGED");
                     }
                     updateCurrentInteractionMode(true /* notify */);
-                    break;
-            }
         }
     };
 
-    private final DeviceProvisionedController.DeviceProvisionedListener mDeviceProvisionedCallback =
-            new DeviceProvisionedController.DeviceProvisionedListener() {
-                @Override
-                public void onDeviceProvisionedChanged() {
-                    if (DEBUG) {
-                        Log.d(TAG, "onDeviceProvisionedChanged: "
-                                + mDeviceProvisionedController.isDeviceProvisioned());
-                    }
-                    // Once the device has been provisioned, check if we can restore gestural nav
-                    restoreGesturalNavOverlayIfNecessary();
-                }
-
-                @Override
-                public void onUserSetupChanged() {
-                    if (DEBUG) {
-                        Log.d(TAG, "onUserSetupChanged: "
-                                + mDeviceProvisionedController.isCurrentUserSetup());
-                    }
-                    // Once the user has been setup, check if we can restore gestural nav
-                    restoreGesturalNavOverlayIfNecessary();
-                }
-
-                @Override
-                public void onUserSwitched() {
-                    if (DEBUG) {
-                        Log.d(TAG, "onUserSwitched: "
-                                + ActivityManagerWrapper.getInstance().getCurrentUserId());
-                    }
-
-                    // Update the nav mode for the current user
-                    updateCurrentInteractionMode(true /* notify */);
-
-                    // When switching users, defer enabling the gestural nav overlay until the user
-                    // is all set up
-                    deferGesturalNavOverlayIfNecessary();
-                }
-            };
-
     @Inject
-    public NavigationModeController(Context context,
-            DeviceProvisionedController deviceProvisionedController,
-            @UiBackground Executor uiBgExecutor) {
+    public NavigationModeController(Context context, @UiBackground Executor uiBgExecutor) {
         mContext = context;
         mCurrentUserContext = context;
         mOverlayManager = IOverlayManager.Stub.asInterface(
                 ServiceManager.getService(Context.OVERLAY_SERVICE));
         mUiBgExecutor = uiBgExecutor;
-        mDeviceProvisionedController = deviceProvisionedController;
-        mDeviceProvisionedController.addCallback(mDeviceProvisionedCallback);
 
         IntentFilter overlayFilter = new IntentFilter(ACTION_OVERLAY_CHANGED);
         overlayFilter.addDataScheme("package");
         overlayFilter.addDataSchemeSpecificPart("android", PatternMatcher.PATTERN_LITERAL);
         mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, overlayFilter, null, null);
 
-        IntentFilter preferredActivityFilter = new IntentFilter(ACTION_PREFERRED_ACTIVITY_CHANGED);
-        mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, preferredActivityFilter, null,
-                null);
-
         updateCurrentInteractionMode(false /* notify */);
-
-        // Check if we need to defer enabling gestural nav
-        deferGesturalNavOverlayIfNecessary();
-    }
-
-    private boolean setGestureModeOverlayForMainLauncher() {
-        if (getCurrentInteractionMode(mCurrentUserContext) == NAV_BAR_MODE_GESTURAL) {
-            // Already in gesture mode
-            return true;
-        }
-
-        Log.d(TAG, "Switching system navigation to full-gesture mode:"
-                + " contextUser="
-                + mCurrentUserContext.getUserId());
-
-        setModeOverlay(NAV_BAR_MODE_GESTURAL_OVERLAY, USER_CURRENT);
-        return true;
     }
 
     public void updateCurrentInteractionMode(boolean notify) {
@@ -176,10 +100,9 @@
         if (mode == NAV_BAR_MODE_GESTURAL) {
             switchToDefaultGestureNavOverlayIfNecessary();
         }
-        mUiBgExecutor.execute(() -> {
+        mUiBgExecutor.execute(() ->
             Settings.Secure.putString(mCurrentUserContext.getContentResolver(),
-                    Secure.NAVIGATION_MODE, String.valueOf(mode));
-        });
+                    Secure.NAVIGATION_MODE, String.valueOf(mode)));
         if (DEBUG) {
             Log.e(TAG, "updateCurrentInteractionMode: mode=" + mode);
             dumpAssetPaths(mCurrentUserContext);
@@ -230,61 +153,11 @@
         }
     }
 
-    private void deferGesturalNavOverlayIfNecessary() {
-        final int userId = mDeviceProvisionedController.getCurrentUser();
-        mRestoreGesturalNavBarMode.put(userId, false);
-        if (mDeviceProvisionedController.isDeviceProvisioned()
-                && mDeviceProvisionedController.isCurrentUserSetup()) {
-            // User is already setup and device is provisioned, nothing to do
-            if (DEBUG) {
-                Log.d(TAG, "deferGesturalNavOverlayIfNecessary: device is provisioned and user is "
-                        + "setup");
-            }
-            return;
-        }
-
-        ArrayList<String> defaultOverlays = new ArrayList<>();
-        try {
-            defaultOverlays.addAll(Arrays.asList(mOverlayManager.getDefaultOverlayPackages()));
-        } catch (RemoteException e) {
-            Log.e(TAG, "deferGesturalNavOverlayIfNecessary: failed to fetch default overlays");
-        }
-        if (!defaultOverlays.contains(NAV_BAR_MODE_GESTURAL_OVERLAY)) {
-            // No default gesture nav overlay
-            if (DEBUG) {
-                Log.d(TAG, "deferGesturalNavOverlayIfNecessary: no default gestural overlay, "
-                        + "default=" + defaultOverlays);
-            }
-            return;
-        }
-
-        // If the default is gestural, force-enable three button mode until the device is
-        // provisioned
-        setModeOverlay(NAV_BAR_MODE_3BUTTON_OVERLAY, USER_CURRENT);
-        mRestoreGesturalNavBarMode.put(userId, true);
-
-        if (DEBUG) {
-            Log.d(TAG, "deferGesturalNavOverlayIfNecessary: setting to 3 button mode");
-        }
-    }
-
-    private void restoreGesturalNavOverlayIfNecessary() {
-        if (DEBUG) {
-            Log.d(TAG, "restoreGesturalNavOverlayIfNecessary: needs restore="
-                    + mRestoreGesturalNavBarMode);
-        }
-        final int userId = mDeviceProvisionedController.getCurrentUser();
-        if (mRestoreGesturalNavBarMode.get(userId)) {
-            // Restore the gestural state if necessary
-            setGestureModeOverlayForMainLauncher();
-            mRestoreGesturalNavBarMode.put(userId, false);
-        }
-    }
-
     private void switchToDefaultGestureNavOverlayIfNecessary() {
         final int userId = mCurrentUserContext.getUserId();
         try {
-            final IOverlayManager om = mOverlayManager;
+            final IOverlayManager om = IOverlayManager.Stub.asInterface(
+                    ServiceManager.getService(Context.OVERLAY_SERVICE));
             final OverlayInfo info = om.getOverlayInfo(NAV_BAR_MODE_GESTURAL_OVERLAY, userId);
             if (info != null && !info.isEnabled()) {
                 // Enable the default gesture nav overlay, and move the back gesture inset scale to
@@ -309,20 +182,6 @@
         }
     }
 
-    public void setModeOverlay(String overlayPkg, int userId) {
-        mUiBgExecutor.execute(() -> {
-            try {
-                mOverlayManager.setEnabledExclusiveInCategory(overlayPkg, userId);
-                if (DEBUG) {
-                    Log.d(TAG, "setModeOverlay: overlayPackage=" + overlayPkg
-                            + " userId=" + userId);
-                }
-            } catch (SecurityException | IllegalStateException | RemoteException e) {
-                Log.e(TAG, "Failed to enable overlay " + overlayPkg + " for user " + userId);
-            }
-        });
-    }
-
     @Override
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         pw.println("NavigationModeController:");
@@ -334,11 +193,6 @@
             defaultOverlays = "failed_to_fetch";
         }
         pw.println("  defaultOverlays=" + defaultOverlays);
-        pw.println("  restoreGesturalNavMode:");
-        for (int i = 0; i < mRestoreGesturalNavBarMode.size(); i++) {
-            pw.println("    userId=" + mRestoreGesturalNavBarMode.keyAt(i)
-                    + " shouldRestore=" + mRestoreGesturalNavBarMode.valueAt(i));
-        }
         dumpAssetPaths(mCurrentUserContext);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/RegionSamplingHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RegionSamplingHelper.java
index 1a6b415..bf52a7a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/RegionSamplingHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RegionSamplingHelper.java
@@ -148,11 +148,6 @@
         updateSamplingRect();
     }
 
-    private void postUpdateSamplingListener() {
-        mHandler.removeCallbacks(mUpdateSamplingListener);
-        mHandler.post(mUpdateSamplingListener);
-    }
-
     private void updateSamplingListener() {
         boolean isSamplingEnabled = mSamplingEnabled
                 && !mSamplingRequestBounds.isEmpty()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
index 53fa263..fbe3e9b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -40,7 +40,6 @@
 import android.service.notification.StatusBarNotification;
 import android.text.TextUtils;
 import android.util.EventLog;
-import android.util.Log;
 import android.view.RemoteAnimationAdapter;
 import android.view.View;
 
@@ -91,92 +90,119 @@
  */
 public class StatusBarNotificationActivityStarter implements NotificationActivityStarter {
 
-    private static final String TAG = "NotifActivityStarter";
-    protected static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+    private final Context mContext;
 
-    private final Lazy<AssistManager> mAssistManagerLazy;
-    private final NotificationGroupManager mGroupManager;
-    private final StatusBarRemoteInputCallback mStatusBarRemoteInputCallback;
-    private final NotificationRemoteInputManager mRemoteInputManager;
-    private final NotificationLockscreenUserManager mLockscreenUserManager;
-    private final ShadeController mShadeController;
-    private final StatusBar mStatusBar;
-    private final KeyguardStateController mKeyguardStateController;
-    private final ActivityStarter mActivityStarter;
+    private final CommandQueue mCommandQueue;
+    private final Handler mMainThreadHandler;
+    private final Handler mBackgroundHandler;
+    private final Executor mUiBgExecutor;
+
     private final NotificationEntryManager mEntryManager;
     private final NotifPipeline mNotifPipeline;
     private final NotifCollection mNotifCollection;
-    private final FeatureFlags mFeatureFlags;
-    private final StatusBarStateController mStatusBarStateController;
-    private final NotificationInterruptStateProvider mNotificationInterruptStateProvider;
-    private final MetricsLogger mMetricsLogger;
-    private final Context mContext;
-    private final NotificationPanelViewController mNotificationPanel;
-    private final NotificationPresenter mPresenter;
-    private final LockPatternUtils mLockPatternUtils;
     private final HeadsUpManagerPhone mHeadsUpManager;
+    private final ActivityStarter mActivityStarter;
+    private final IStatusBarService mBarService;
+    private final StatusBarStateController mStatusBarStateController;
     private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     private final KeyguardManager mKeyguardManager;
-    private final ActivityLaunchAnimator mActivityLaunchAnimator;
-    private final IStatusBarService mBarService;
-    private final CommandQueue mCommandQueue;
     private final IDreamManager mDreamManager;
-    private final Handler mMainThreadHandler;
-    private final Handler mBackgroundHandler;
-    private final ActivityIntentHelper mActivityIntentHelper;
     private final BubbleController mBubbleController;
-    private final Executor mUiBgExecutor;
+    private final Lazy<AssistManager> mAssistManagerLazy;
+    private final NotificationRemoteInputManager mRemoteInputManager;
+    private final NotificationGroupManager mGroupManager;
+    private final NotificationLockscreenUserManager mLockscreenUserManager;
+    private final ShadeController mShadeController;
+    private final KeyguardStateController mKeyguardStateController;
+    private final NotificationInterruptStateProvider mNotificationInterruptStateProvider;
+    private final LockPatternUtils mLockPatternUtils;
+    private final StatusBarRemoteInputCallback mStatusBarRemoteInputCallback;
+    private final ActivityIntentHelper mActivityIntentHelper;
+
+    private final FeatureFlags mFeatureFlags;
+    private final MetricsLogger mMetricsLogger;
+    private final StatusBarNotificationActivityStarterLogger mLogger;
+
+    private final StatusBar mStatusBar;
+    private final NotificationPresenter mPresenter;
+    private final NotificationPanelViewController mNotificationPanel;
+    private final ActivityLaunchAnimator mActivityLaunchAnimator;
 
     private boolean mIsCollapsingToShowActivityOverLockscreen;
 
-    private StatusBarNotificationActivityStarter(Context context, CommandQueue commandQueue,
-            Lazy<AssistManager> assistManagerLazy, NotificationPanelViewController panel,
-            NotificationPresenter presenter, NotificationEntryManager entryManager,
-            HeadsUpManagerPhone headsUpManager, ActivityStarter activityStarter,
-            ActivityLaunchAnimator activityLaunchAnimator, IStatusBarService statusBarService,
+    private StatusBarNotificationActivityStarter(
+            Context context,
+            CommandQueue commandQueue,
+            Handler mainThreadHandler,
+            Handler backgroundHandler,
+            Executor uiBgExecutor,
+            NotificationEntryManager entryManager,
+            NotifPipeline notifPipeline,
+            NotifCollection notifCollection,
+            HeadsUpManagerPhone headsUpManager,
+            ActivityStarter activityStarter,
+            IStatusBarService statusBarService,
             StatusBarStateController statusBarStateController,
             StatusBarKeyguardViewManager statusBarKeyguardViewManager,
             KeyguardManager keyguardManager,
-            IDreamManager dreamManager, NotificationRemoteInputManager remoteInputManager,
-            StatusBarRemoteInputCallback remoteInputCallback, NotificationGroupManager groupManager,
+            IDreamManager dreamManager,
+            BubbleController bubbleController,
+            Lazy<AssistManager> assistManagerLazy,
+            NotificationRemoteInputManager remoteInputManager,
+            NotificationGroupManager groupManager,
             NotificationLockscreenUserManager lockscreenUserManager,
-            ShadeController shadeController, StatusBar statusBar,
+            ShadeController shadeController,
             KeyguardStateController keyguardStateController,
             NotificationInterruptStateProvider notificationInterruptStateProvider,
-            MetricsLogger metricsLogger, LockPatternUtils lockPatternUtils,
-            Handler mainThreadHandler, Handler backgroundHandler, Executor uiBgExecutor,
-            ActivityIntentHelper activityIntentHelper, BubbleController bubbleController,
-            FeatureFlags featureFlags, NotifPipeline notifPipeline,
-            NotifCollection notifCollection) {
+            LockPatternUtils lockPatternUtils,
+            StatusBarRemoteInputCallback remoteInputCallback,
+            ActivityIntentHelper activityIntentHelper,
+
+            FeatureFlags featureFlags,
+            MetricsLogger metricsLogger,
+            StatusBarNotificationActivityStarterLogger logger,
+
+            StatusBar statusBar,
+            NotificationPresenter presenter,
+            NotificationPanelViewController panel,
+            ActivityLaunchAnimator activityLaunchAnimator) {
         mContext = context;
-        mNotificationPanel = panel;
-        mPresenter = presenter;
-        mHeadsUpManager = headsUpManager;
-        mActivityLaunchAnimator = activityLaunchAnimator;
-        mBarService = statusBarService;
         mCommandQueue = commandQueue;
+        mMainThreadHandler = mainThreadHandler;
+        mBackgroundHandler = backgroundHandler;
+        mUiBgExecutor = uiBgExecutor;
+        mEntryManager = entryManager;
+        mNotifPipeline = notifPipeline;
+        mNotifCollection = notifCollection;
+        mHeadsUpManager = headsUpManager;
+        mActivityStarter = activityStarter;
+        mBarService = statusBarService;
+        mStatusBarStateController = statusBarStateController;
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
         mKeyguardManager = keyguardManager;
         mDreamManager = dreamManager;
+        mBubbleController = bubbleController;
+        mAssistManagerLazy = assistManagerLazy;
         mRemoteInputManager = remoteInputManager;
+        mGroupManager = groupManager;
         mLockscreenUserManager = lockscreenUserManager;
         mShadeController = shadeController;
+        mKeyguardStateController = keyguardStateController;
+        mNotificationInterruptStateProvider = notificationInterruptStateProvider;
+        mLockPatternUtils = lockPatternUtils;
+        mStatusBarRemoteInputCallback = remoteInputCallback;
+        mActivityIntentHelper = activityIntentHelper;
+
+        mFeatureFlags = featureFlags;
+        mMetricsLogger = metricsLogger;
+        mLogger = logger;
+
         // TODO: use KeyguardStateController#isOccluded to remove this dependency
         mStatusBar = statusBar;
-        mKeyguardStateController = keyguardStateController;
-        mActivityStarter = activityStarter;
-        mEntryManager = entryManager;
-        mStatusBarStateController = statusBarStateController;
-        mNotificationInterruptStateProvider = notificationInterruptStateProvider;
-        mMetricsLogger = metricsLogger;
-        mAssistManagerLazy = assistManagerLazy;
-        mGroupManager = groupManager;
-        mLockPatternUtils = lockPatternUtils;
-        mBackgroundHandler = backgroundHandler;
-        mUiBgExecutor = uiBgExecutor;
-        mFeatureFlags = featureFlags;
-        mNotifPipeline = notifPipeline;
-        mNotifCollection = notifCollection;
+        mPresenter = presenter;
+        mNotificationPanel = panel;
+        mActivityLaunchAnimator = activityLaunchAnimator;
+
         if (!mFeatureFlags.isNewNotifPipelineRenderingEnabled()) {
             mEntryManager.addNotificationEntryListener(new NotificationEntryListener() {
                 @Override
@@ -192,11 +218,6 @@
                 }
             });
         }
-
-        mStatusBarRemoteInputCallback = remoteInputCallback;
-        mMainThreadHandler = mainThreadHandler;
-        mActivityIntentHelper = activityIntentHelper;
-        mBubbleController = bubbleController;
     }
 
     /**
@@ -207,6 +228,8 @@
      */
     @Override
     public void onNotificationClicked(StatusBarNotification sbn, ExpandableNotificationRow row) {
+        mLogger.logStartingActivityFromClick(sbn.getKey());
+
         RemoteInputController controller = mRemoteInputManager.getController();
         if (controller.isRemoteInputActive(row.getEntry())
                 && !TextUtils.isEmpty(row.getActiveRemoteInputText())) {
@@ -225,7 +248,7 @@
         // The only valid case is Bubble notifications. Guard against other cases
         // entering here.
         if (intent == null && !isBubble) {
-            Log.e(TAG, "onNotificationClicked called for non-clickable notification!");
+            mLogger.logNonClickableNotification(sbn.getKey());
             return;
         }
 
@@ -258,6 +281,8 @@
             boolean isActivityIntent,
             boolean wasOccluded,
             boolean showOverLockscreen) {
+        mLogger.logHandleClickAfterKeyguardDismissed(sbn.getKey());
+
         // TODO: Some of this code may be able to move to NotificationEntryManager.
         if (mHeadsUpManager != null && mHeadsUpManager.isAlerting(sbn.getKey())) {
             // Release the HUN notification to the shade.
@@ -304,6 +329,8 @@
             boolean isActivityIntent,
             boolean wasOccluded,
             NotificationEntry parentToCancelFinal) {
+        mLogger.logHandleClickAfterPanelCollapsed(sbn.getKey());
+
         String notificationKey = sbn.getKey();
         try {
             // The intent we are sending is for the application, which
@@ -343,9 +370,11 @@
                     remoteInputText.toString());
         }
         if (isBubble) {
+            mLogger.logExpandingBubble(notificationKey);
             expandBubbleStackOnMainThread(notificationKey);
         } else {
-            startNotificationIntent(intent, fillInIntent, row, wasOccluded, isActivityIntent);
+            startNotificationIntent(
+                    intent, fillInIntent, entry, row, wasOccluded, isActivityIntent);
         }
         if (isActivityIntent || isBubble) {
             mAssistManagerLazy.get().hideAssist();
@@ -392,10 +421,16 @@
         }
     }
 
-    private void startNotificationIntent(PendingIntent intent, Intent fillInIntent,
-            View row, boolean wasOccluded, boolean isActivityIntent) {
+    private void startNotificationIntent(
+            PendingIntent intent,
+            Intent fillInIntent,
+            NotificationEntry entry,
+            View row,
+            boolean wasOccluded,
+            boolean isActivityIntent) {
         RemoteAnimationAdapter adapter = mActivityLaunchAnimator.getLaunchAnimation(row,
                 wasOccluded);
+        mLogger.logStartNotificationIntent(entry.getKey(), intent);
         try {
             if (adapter != null) {
                 ActivityTaskManager.getService()
@@ -408,7 +443,7 @@
         } catch (RemoteException | PendingIntent.CanceledException e) {
             // the stack trace isn't very helpful here.
             // Just log the exception message.
-            Log.w(TAG, "Sending contentIntent failed: " + e);
+            mLogger.logSendingIntentFailed(e);
             // TODO: Dismiss Keyguard.
         }
     }
@@ -438,13 +473,9 @@
     private void handleFullScreenIntent(NotificationEntry entry) {
         if (mNotificationInterruptStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry)) {
             if (shouldSuppressFullScreenIntent(entry)) {
-                if (DEBUG) {
-                    Log.d(TAG, "No Fullscreen intent: suppressed by DND: " + entry.getKey());
-                }
+                mLogger.logFullScreenIntentSuppressedByDnD(entry.getKey());
             } else if (entry.getImportance() < NotificationManager.IMPORTANCE_HIGH) {
-                if (DEBUG) {
-                    Log.d(TAG, "No Fullscreen intent: not important enough: " + entry.getKey());
-                }
+                mLogger.logFullScreenIntentNotImportantEnough(entry.getKey());
             } else {
                 // Stop screensaver if the notification has a fullscreen intent.
                 // (like an incoming phone call)
@@ -457,13 +488,13 @@
                 });
 
                 // not immersive & a fullscreen alert should be shown
-                if (DEBUG) {
-                    Log.d(TAG, "Notification has fullScreenIntent; sending fullScreenIntent");
-                }
+                final PendingIntent fullscreenIntent =
+                        entry.getSbn().getNotification().fullScreenIntent;
+                mLogger.logSendingFullScreenIntent(entry.getKey(), fullscreenIntent);
                 try {
                     EventLog.writeEvent(EventLogTags.SYSUI_FULLSCREEN_NOTIFICATION,
                             entry.getKey());
-                    entry.getSbn().getNotification().fullScreenIntent.send();
+                    fullscreenIntent.send();
                     entry.notifyFullScreenIntentLaunched();
                     mMetricsLogger.count("note_fullscreen", 1);
                 } catch (PendingIntent.CanceledException e) {
@@ -578,9 +609,10 @@
     public static class Builder {
         private final Context mContext;
         private final CommandQueue mCommandQueue;
-        private final Lazy<AssistManager> mAssistManagerLazy;
+        private final Handler mMainThreadHandler;
+        private final Handler mBackgroundHandler;
+        private final Executor mUiBgExecutor;
         private final NotificationEntryManager mEntryManager;
-        private final FeatureFlags mFeatureFlags;
         private final NotifPipeline mNotifPipeline;
         private final NotifCollection mNotifCollection;
         private final HeadsUpManagerPhone mHeadsUpManager;
@@ -590,30 +622,37 @@
         private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
         private final KeyguardManager mKeyguardManager;
         private final IDreamManager mDreamManager;
+        private final BubbleController mBubbleController;
+        private final Lazy<AssistManager> mAssistManagerLazy;
         private final NotificationRemoteInputManager mRemoteInputManager;
-        private final StatusBarRemoteInputCallback mRemoteInputCallback;
         private final NotificationGroupManager mGroupManager;
         private final NotificationLockscreenUserManager mLockscreenUserManager;
-        private final KeyguardStateController mKeyguardStateController;
-        private final MetricsLogger mMetricsLogger;
-        private final LockPatternUtils mLockPatternUtils;
-        private final Handler mMainThreadHandler;
-        private final Handler mBackgroundHandler;
-        private final Executor mUiBgExecutor;
-        private final ActivityIntentHelper mActivityIntentHelper;
-        private final BubbleController mBubbleController;
-        private NotificationPanelViewController mNotificationPanelViewController;
-        private NotificationInterruptStateProvider mNotificationInterruptStateProvider;
         private final ShadeController mShadeController;
-        private NotificationPresenter mNotificationPresenter;
-        private ActivityLaunchAnimator mActivityLaunchAnimator;
+        private final KeyguardStateController mKeyguardStateController;
+        private final NotificationInterruptStateProvider mNotificationInterruptStateProvider;
+        private final LockPatternUtils mLockPatternUtils;
+        private final StatusBarRemoteInputCallback mRemoteInputCallback;
+        private final ActivityIntentHelper mActivityIntentHelper;
+
+        private final FeatureFlags mFeatureFlags;
+        private final MetricsLogger mMetricsLogger;
+        private final StatusBarNotificationActivityStarterLogger mLogger;
+
         private StatusBar mStatusBar;
+        private NotificationPresenter mNotificationPresenter;
+        private NotificationPanelViewController mNotificationPanelViewController;
+        private ActivityLaunchAnimator mActivityLaunchAnimator;
 
         @Inject
-        public Builder(Context context,
+        public Builder(
+                Context context,
                 CommandQueue commandQueue,
-                Lazy<AssistManager> assistManagerLazy,
+                @Main Handler mainThreadHandler,
+                @Background Handler backgroundHandler,
+                @UiBackground Executor uiBgExecutor,
                 NotificationEntryManager entryManager,
+                NotifPipeline notifPipeline,
+                NotifCollection notifCollection,
                 HeadsUpManagerPhone headsUpManager,
                 ActivityStarter activityStarter,
                 IStatusBarService statusBarService,
@@ -621,27 +660,30 @@
                 StatusBarKeyguardViewManager statusBarKeyguardViewManager,
                 KeyguardManager keyguardManager,
                 IDreamManager dreamManager,
+                BubbleController bubbleController,
+                Lazy<AssistManager> assistManagerLazy,
                 NotificationRemoteInputManager remoteInputManager,
-                StatusBarRemoteInputCallback remoteInputCallback,
                 NotificationGroupManager groupManager,
                 NotificationLockscreenUserManager lockscreenUserManager,
+                ShadeController shadeController,
                 KeyguardStateController keyguardStateController,
                 NotificationInterruptStateProvider notificationInterruptStateProvider,
-                MetricsLogger metricsLogger,
                 LockPatternUtils lockPatternUtils,
-                @Main Handler mainThreadHandler,
-                @Background Handler backgroundHandler,
-                @UiBackground Executor uiBgExecutor,
+                StatusBarRemoteInputCallback remoteInputCallback,
                 ActivityIntentHelper activityIntentHelper,
-                BubbleController bubbleController,
-                ShadeController shadeController,
+
                 FeatureFlags featureFlags,
-                NotifPipeline notifPipeline,
-                NotifCollection notifCollection) {
+                MetricsLogger metricsLogger,
+                StatusBarNotificationActivityStarterLogger logger) {
+
             mContext = context;
             mCommandQueue = commandQueue;
-            mAssistManagerLazy = assistManagerLazy;
+            mMainThreadHandler = mainThreadHandler;
+            mBackgroundHandler = backgroundHandler;
+            mUiBgExecutor = uiBgExecutor;
             mEntryManager = entryManager;
+            mNotifPipeline = notifPipeline;
+            mNotifCollection = notifCollection;
             mHeadsUpManager = headsUpManager;
             mActivityStarter = activityStarter;
             mStatusBarService = statusBarService;
@@ -649,23 +691,21 @@
             mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
             mKeyguardManager = keyguardManager;
             mDreamManager = dreamManager;
+            mBubbleController = bubbleController;
+            mAssistManagerLazy = assistManagerLazy;
             mRemoteInputManager = remoteInputManager;
-            mRemoteInputCallback = remoteInputCallback;
             mGroupManager = groupManager;
             mLockscreenUserManager = lockscreenUserManager;
+            mShadeController = shadeController;
             mKeyguardStateController = keyguardStateController;
             mNotificationInterruptStateProvider = notificationInterruptStateProvider;
-            mMetricsLogger = metricsLogger;
             mLockPatternUtils = lockPatternUtils;
-            mMainThreadHandler = mainThreadHandler;
-            mBackgroundHandler = backgroundHandler;
-            mUiBgExecutor = uiBgExecutor;
+            mRemoteInputCallback = remoteInputCallback;
             mActivityIntentHelper = activityIntentHelper;
-            mBubbleController = bubbleController;
-            mShadeController = shadeController;
+
             mFeatureFlags = featureFlags;
-            mNotifPipeline = notifPipeline;
-            mNotifCollection = notifCollection;
+            mMetricsLogger = metricsLogger;
+            mLogger = logger;
         }
 
         /** Sets the status bar to use as {@link StatusBar}. */
@@ -692,37 +732,42 @@
         }
 
         public StatusBarNotificationActivityStarter build() {
-            return new StatusBarNotificationActivityStarter(mContext,
-                    mCommandQueue, mAssistManagerLazy,
-                    mNotificationPanelViewController,
-                    mNotificationPresenter,
+            return new StatusBarNotificationActivityStarter(
+                    mContext,
+                    mCommandQueue,
+                    mMainThreadHandler,
+                    mBackgroundHandler,
+                    mUiBgExecutor,
                     mEntryManager,
+                    mNotifPipeline,
+                    mNotifCollection,
                     mHeadsUpManager,
                     mActivityStarter,
-                    mActivityLaunchAnimator,
                     mStatusBarService,
                     mStatusBarStateController,
                     mStatusBarKeyguardViewManager,
                     mKeyguardManager,
                     mDreamManager,
+                    mBubbleController,
+                    mAssistManagerLazy,
                     mRemoteInputManager,
-                    mRemoteInputCallback,
                     mGroupManager,
                     mLockscreenUserManager,
                     mShadeController,
-                    mStatusBar,
                     mKeyguardStateController,
                     mNotificationInterruptStateProvider,
-                    mMetricsLogger,
                     mLockPatternUtils,
-                    mMainThreadHandler,
-                    mBackgroundHandler,
-                    mUiBgExecutor,
+                    mRemoteInputCallback,
                     mActivityIntentHelper,
-                    mBubbleController,
+
                     mFeatureFlags,
-                    mNotifPipeline,
-                    mNotifCollection);
+                    mMetricsLogger,
+                    mLogger,
+
+                    mStatusBar,
+                    mNotificationPresenter,
+                    mNotificationPanelViewController,
+                    mActivityLaunchAnimator);
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt
new file mode 100644
index 0000000..d118747
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt
@@ -0,0 +1,114 @@
+/*
+ * 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.statusbar.phone
+
+import android.app.PendingIntent
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogLevel.DEBUG
+import com.android.systemui.log.LogLevel.ERROR
+import com.android.systemui.log.LogLevel.INFO
+import com.android.systemui.log.LogLevel.WARNING
+import com.android.systemui.log.dagger.NotifInteractionLog
+import javax.inject.Inject
+
+class StatusBarNotificationActivityStarterLogger @Inject constructor(
+    @NotifInteractionLog private val buffer: LogBuffer
+) {
+    fun logStartingActivityFromClick(key: String) {
+        buffer.log(TAG, DEBUG, {
+            str1 = key
+        }, {
+            "(1/4) onNotificationClicked: $str1"
+        })
+    }
+
+    fun logHandleClickAfterKeyguardDismissed(key: String) {
+        buffer.log(TAG, DEBUG, {
+            str1 = key
+        }, {
+            "(2/4) handleNotificationClickAfterKeyguardDismissed: $str1"
+        })
+    }
+
+    fun logHandleClickAfterPanelCollapsed(key: String) {
+        buffer.log(TAG, DEBUG, {
+            str1 = key
+        }, {
+            "(3/4) handleNotificationClickAfterPanelCollapsed: $str1"
+        })
+    }
+
+    fun logStartNotificationIntent(key: String, pendingIntent: PendingIntent) {
+        buffer.log(TAG, INFO, {
+            str1 = key
+            str2 = pendingIntent.intent.toString()
+        }, {
+            "(4/4) Starting $str2 for notification $str1"
+        })
+    }
+
+    fun logExpandingBubble(key: String) {
+        buffer.log(TAG, DEBUG, {
+            str1 = key
+        }, {
+            "Expanding bubble for $str1 (rather than firing intent)"
+        })
+    }
+
+    fun logSendingIntentFailed(e: Exception) {
+        buffer.log(TAG, WARNING, {
+            str1 = e.toString()
+        }, {
+            "Sending contentIntentFailed: $str1"
+        })
+    }
+
+    fun logNonClickableNotification(key: String) {
+        buffer.log(TAG, ERROR, {
+            str1 = key
+        }, {
+            "onNotificationClicked called for non-clickable notification! $str1"
+        })
+    }
+
+    fun logFullScreenIntentSuppressedByDnD(key: String) {
+        buffer.log(TAG, DEBUG, {
+            str1 = key
+        }, {
+            "No Fullscreen intent: suppressed by DND: $str1"
+        })
+    }
+
+    fun logFullScreenIntentNotImportantEnough(key: String) {
+        buffer.log(TAG, DEBUG, {
+            str1 = key
+        }, {
+            "No Fullscreen intent: not important enough: $str1"
+        })
+    }
+
+    fun logSendingFullScreenIntent(key: String, pendingIntent: PendingIntent) {
+        buffer.log(TAG, INFO, {
+            str1 = key
+            str2 = pendingIntent.intent.toString()
+        }, {
+            "Notification $str1 has fullScreenIntent; sending fullScreenIntent $str2"
+        })
+    }
+}
+
+private const val TAG = "NotifActivityStarter"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java
index 051bd29..a284335 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java
@@ -357,7 +357,18 @@
         mBroadcastDispatcher.registerReceiverWithHandler(this, filter, mReceiverHandler);
         mListening = true;
 
+        // Initial setup of connectivity. Handled as if we had received a sticky broadcast of
+        // ConnectivityManager.CONNECTIVITY_ACTION or ConnectivityManager.INET_CONDITION_ACTION.
+        mReceiverHandler.post(this::updateConnectivity);
+
+        // Initial setup of WifiSignalController. Handled as if we had received a sticky broadcast
+        // of WifiManager.WIFI_STATE_CHANGED_ACTION or WifiManager.NETWORK_STATE_CHANGED_ACTION
+        mReceiverHandler.post(mWifiSignalController::fetchInitialState);
         updateMobileControllers();
+
+        // Initial setup of emergency information. Handled as if we had received a sticky broadcast
+        // of TelephonyManager.ACTION_DEFAULT_VOICE_SUBSCRIPTION_CHANGED.
+        mReceiverHandler.post(this::recalculateEmergency);
     }
 
     private void unregisterListeners() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java
index b258fd4..5257ce4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java
@@ -102,6 +102,20 @@
     }
 
     /**
+     * Fetches wifi initial state replacing the initial sticky broadcast.
+     */
+    public void fetchInitialState() {
+        mWifiTracker.fetchInitialState();
+        mCurrentState.enabled = mWifiTracker.enabled;
+        mCurrentState.connected = mWifiTracker.connected;
+        mCurrentState.ssid = mWifiTracker.ssid;
+        mCurrentState.rssi = mWifiTracker.rssi;
+        mCurrentState.level = mWifiTracker.level;
+        mCurrentState.statusLabel = mWifiTracker.statusLabel;
+        notifyListenersIfNecessary();
+    }
+
+    /**
      * Extract wifi state directly from broadcasts about changes in wifi state.
      */
     public void handleBroadcast(Intent intent) {
diff --git a/packages/SystemUI/src/com/android/systemui/util/RelativeTouchListener.kt b/packages/SystemUI/src/com/android/systemui/util/RelativeTouchListener.kt
index d65b285..8880df9 100644
--- a/packages/SystemUI/src/com/android/systemui/util/RelativeTouchListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/RelativeTouchListener.kt
@@ -115,7 +115,9 @@
 
                 performedLongClick = false
                 handler.postDelayed({
-                    performedLongClick = v.performLongClick()
+                    if (v.isLongClickable) {
+                        performedLongClick = v.performLongClick()
+                    }
                 }, ViewConfiguration.getLongPressTimeout().toLong())
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimator.kt b/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimator.kt
index 8625d63..db08d64 100644
--- a/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimator.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimator.kt
@@ -61,14 +61,17 @@
 
 /**
  * Default spring configuration to use for animations where stiffness and/or damping ratio
- * were not provided.
+ * were not provided, and a default spring was not set via [PhysicsAnimator.setDefaultSpringConfig].
  */
-private val defaultSpring = PhysicsAnimator.SpringConfig(
+private val globalDefaultSpring = PhysicsAnimator.SpringConfig(
         SpringForce.STIFFNESS_MEDIUM,
         SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY)
 
-/** Default fling configuration to use for animations where friction was not provided. */
-private val defaultFling = PhysicsAnimator.FlingConfig(
+/**
+ * Default fling configuration to use for animations where friction was not provided, and a default
+ * fling config was not set via [PhysicsAnimator.setDefaultFlingConfig].
+ */
+private val globalDefaultFling = PhysicsAnimator.FlingConfig(
         friction = 1f, min = -Float.MAX_VALUE, max = Float.MAX_VALUE)
 
 /** Whether to log helpful debug information about animations. */
@@ -111,6 +114,12 @@
     /** End actions to run when all animations have completed.  */
     private val endActions = ArrayList<EndAction>()
 
+    /** SpringConfig to use by default for properties whose springs were not provided. */
+    private var defaultSpring: SpringConfig = globalDefaultSpring
+
+    /** FlingConfig to use by default for properties whose fling configs were not provided. */
+    private var defaultFling: FlingConfig = globalDefaultFling
+
     /**
      * Internal listeners that respond to DynamicAnimations updating and ending, and dispatch to
      * the listeners provided via [addUpdateListener] and [addEndListener]. This allows us to add
@@ -204,6 +213,19 @@
     }
 
     /**
+     * Springs a property to a given value using the provided configuration options, and a start
+     * velocity of 0f.
+     *
+     * @see spring
+     */
+    fun spring(
+        property: FloatPropertyCompat<in T>,
+        toPosition: Float
+    ): PhysicsAnimator<T> {
+        return spring(property, toPosition, 0f)
+    }
+
+    /**
      * Flings a property using the given start velocity, using a [FlingAnimation] configured using
      * the provided configuration settings.
      *
@@ -392,6 +414,14 @@
         return this
     }
 
+    fun setDefaultSpringConfig(defaultSpring: SpringConfig) {
+        this.defaultSpring = defaultSpring
+    }
+
+    fun setDefaultFlingConfig(defaultFling: FlingConfig) {
+        this.defaultFling = defaultFling
+    }
+
     /** Starts the animations! */
     fun start() {
         startAction()
@@ -752,7 +782,7 @@
     ) {
 
         constructor() :
-                this(defaultSpring.stiffness, defaultSpring.dampingRatio)
+                this(globalDefaultSpring.stiffness, globalDefaultSpring.dampingRatio)
 
         constructor(stiffness: Float, dampingRatio: Float) :
                 this(stiffness = stiffness, dampingRatio = dampingRatio, startVelocity = 0f)
@@ -782,10 +812,10 @@
         internal var startVelocity: Float
     ) {
 
-        constructor() : this(defaultFling.friction)
+        constructor() : this(globalDefaultFling.friction)
 
         constructor(friction: Float) :
-                this(friction, defaultFling.min, defaultFling.max)
+                this(friction, globalDefaultFling.min, globalDefaultFling.max)
 
         constructor(friction: Float, min: Float, max: Float) :
                 this(friction, min, max, startVelocity = 0f)
diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java
index cc6d607..8acfbf2 100644
--- a/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java
+++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java
@@ -137,6 +137,36 @@
     }
 
     /**
+     * Provide a Background-Thread Executor by default.
+     */
+    @Provides
+    @Singleton
+    public static RepeatableExecutor provideRepeatableExecutor(@Background DelayableExecutor exec) {
+        return new RepeatableExecutorImpl(exec);
+    }
+
+    /**
+     * Provide a Background-Thread Executor.
+     */
+    @Provides
+    @Singleton
+    @Background
+    public static RepeatableExecutor provideBackgroundRepeatableExecutor(
+            @Background DelayableExecutor exec) {
+        return new RepeatableExecutorImpl(exec);
+    }
+
+    /**
+     * Provide a Main-Thread Executor.
+     */
+    @Provides
+    @Singleton
+    @Main
+    public static RepeatableExecutor provideMainRepeatableExecutor(@Main DelayableExecutor exec) {
+        return new RepeatableExecutorImpl(exec);
+    }
+
+    /**
      * Provide an Executor specifically for running UI operations on a separate thread.
      *
      * Keep submitted runnables short and to the point, just as with any other UI code.
diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/RepeatableExecutor.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/RepeatableExecutor.java
new file mode 100644
index 0000000..aefdc99
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/RepeatableExecutor.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.concurrency;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A sub-class of {@link Executor} that allows scheduling commands to execute periodically.
+ */
+public interface RepeatableExecutor extends Executor {
+
+    /**
+     * Execute supplied Runnable on the Executors thread after initial delay, and subsequently with
+     * the given delay between the termination of one execution and the commencement of the next.
+     *
+     * Each invocation of the supplied Runnable will be scheduled after the previous invocation
+     * completes. For example, if you schedule the Runnable with a 60 second delay, and the Runnable
+     * itself takes 1 second, the effective delay will be 61 seconds between each invocation.
+     *
+     * See {@link java.util.concurrent.ScheduledExecutorService#scheduleRepeatedly(Runnable,
+     * long, long)}
+     *
+     * @return A Runnable that, when run, removes the supplied argument from the Executor queue.
+     */
+    default Runnable executeRepeatedly(Runnable r, long initialDelayMillis, long delayMillis) {
+        return executeRepeatedly(r, initialDelayMillis, delayMillis, TimeUnit.MILLISECONDS);
+    }
+
+    /**
+     * Execute supplied Runnable on the Executors thread after initial delay, and subsequently with
+     * the given delay between the termination of one execution and the commencement of the next..
+     *
+     * See {@link java.util.concurrent.ScheduledExecutorService#scheduleRepeatedly(Runnable,
+     * long, long)}
+     *
+     * @return A Runnable that, when run, removes the supplied argument from the Executor queue.
+     */
+    Runnable executeRepeatedly(Runnable r, long initialDelay, long delay, TimeUnit unit);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/RepeatableExecutorImpl.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/RepeatableExecutorImpl.java
new file mode 100644
index 0000000..c03e10e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/RepeatableExecutorImpl.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.concurrency;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Implementation of {@link RepeatableExecutor} for SystemUI.
+ */
+class RepeatableExecutorImpl implements RepeatableExecutor {
+
+    private final DelayableExecutor mExecutor;
+
+    RepeatableExecutorImpl(DelayableExecutor executor) {
+        mExecutor = executor;
+    }
+
+    @Override
+    public void execute(Runnable command) {
+        mExecutor.execute(command);
+    }
+
+    @Override
+    public Runnable executeRepeatedly(Runnable r, long initDelay, long delay, TimeUnit unit) {
+        ExecutionToken token = new ExecutionToken(r, delay, unit);
+        token.start(initDelay, unit);
+        return token::cancel;
+    }
+
+    private class ExecutionToken implements Runnable {
+        private final Runnable mCommand;
+        private final long mDelay;
+        private final TimeUnit mUnit;
+        private final Object mLock = new Object();
+        private Runnable mCancel;
+
+        ExecutionToken(Runnable r, long delay, TimeUnit unit) {
+            mCommand = r;
+            mDelay = delay;
+            mUnit = unit;
+        }
+
+        @Override
+        public void run() {
+            mCommand.run();
+            synchronized (mLock) {
+                if (mCancel != null) {
+                    mCancel = mExecutor.executeDelayed(this, mDelay, mUnit);
+                }
+            }
+        }
+
+        /** Starts execution that will repeat the command until {@link cancel}. */
+        public void start(long startDelay, TimeUnit unit) {
+            synchronized (mLock) {
+                mCancel = mExecutor.executeDelayed(this, startDelay, unit);
+            }
+        }
+
+        /** Cancel repeated execution of command. */
+        public void cancel() {
+            synchronized (mLock) {
+                if (mCancel != null) {
+                    mCancel.run();
+                    mCancel = null;
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/wm/SystemWindows.java b/packages/SystemUI/src/com/android/systemui/wm/SystemWindows.java
index e5da603..899aabb 100644
--- a/packages/SystemUI/src/com/android/systemui/wm/SystemWindows.java
+++ b/packages/SystemUI/src/com/android/systemui/wm/SystemWindows.java
@@ -32,6 +32,7 @@
 import android.view.Display;
 import android.view.DisplayCutout;
 import android.view.DragEvent;
+import android.view.IScrollCaptureController;
 import android.view.IWindow;
 import android.view.IWindowManager;
 import android.view.IWindowSession;
@@ -352,5 +353,14 @@
 
         @Override
         public void dispatchPointerCaptureChanged(boolean hasCapture) {}
+
+        @Override
+        public void requestScrollCapture(IScrollCaptureController controller) {
+            try {
+                controller.onClientUnavailable();
+            } catch (RemoteException ex) {
+                // ignore
+            }
+        }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardHostViewTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardHostViewTest.java
index 25f279b..dd5c833 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardHostViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardHostViewTest.java
@@ -17,30 +17,50 @@
 package com.android.keyguard;
 
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 
 import android.test.suitebuilder.annotation.SmallTest;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 
+import com.android.internal.widget.LockPatternUtils;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
 
 import org.junit.Assert;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
 public class KeyguardHostViewTest extends SysuiTestCase {
 
+    @Mock
+    private KeyguardSecurityContainer mSecurityContainer;
+    @Mock
+    private LockPatternUtils mLockPatternUtils;
+    @Rule
+    public MockitoRule mMockitoRule = MockitoJUnit.rule();
+
     private KeyguardHostView mKeyguardHostView;
 
     @Before
     public void setup() {
         mDependency.injectMockDependency(KeyguardUpdateMonitor.class);
-        mKeyguardHostView = new KeyguardHostView(getContext());
+        mKeyguardHostView = new KeyguardHostView(getContext()) {
+            @Override
+            protected void onFinishInflate() {
+                mSecurityContainer = KeyguardHostViewTest.this.mSecurityContainer;
+                mLockPatternUtils = KeyguardHostViewTest.this.mLockPatternUtils;
+            }
+        };
+        mKeyguardHostView.onFinishInflate();
     }
 
     @Test
@@ -50,4 +70,10 @@
                 null /* cancelAction */);
         Assert.assertTrue("Action should exist", mKeyguardHostView.hasDismissActions());
     }
+
+    @Test
+    public void testOnStartingToHide() {
+        mKeyguardHostView.onStartingToHide();
+        verify(mSecurityContainer).onStartingToHide();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index 7403a11..6c00eca 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -61,6 +61,7 @@
 import android.telephony.ServiceState;
 import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableContext;
@@ -133,20 +134,23 @@
     @Mock
     private BroadcastDispatcher mBroadcastDispatcher;
     @Mock
-    private Executor mBackgroundExecutor;
-    @Mock
     private RingerModeTracker mRingerModeTracker;
     @Mock
     private LiveData<Integer> mRingerModeLiveData;
+    @Mock
+    private TelephonyManager mTelephonyManager;
+    // Direct executor
+    private Executor mBackgroundExecutor = Runnable::run;
     private TestableLooper mTestableLooper;
     private TestableKeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    private TestableContext mSpiedContext;
 
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
-        TestableContext context = spy(mContext);
+        mSpiedContext = spy(mContext);
         when(mPackageManager.hasSystemFeature(anyString())).thenReturn(true);
-        when(context.getPackageManager()).thenReturn(mPackageManager);
+        when(mSpiedContext.getPackageManager()).thenReturn(mPackageManager);
         doAnswer(invocation -> {
             IBiometricEnabledOnKeyguardCallback callback = invocation.getArgument(0);
             callback.onChanged(BiometricSourceType.FACE, true /* enabled */,
@@ -161,19 +165,20 @@
         when(mStrongAuthTracker
                 .isUnlockingWithBiometricAllowed(anyBoolean() /* isStrongBiometric */))
                 .thenReturn(true);
-        context.addMockSystemService(TrustManager.class, mTrustManager);
-        context.addMockSystemService(FingerprintManager.class, mFingerprintManager);
-        context.addMockSystemService(BiometricManager.class, mBiometricManager);
-        context.addMockSystemService(FaceManager.class, mFaceManager);
-        context.addMockSystemService(UserManager.class, mUserManager);
-        context.addMockSystemService(DevicePolicyManager.class, mDevicePolicyManager);
-        context.addMockSystemService(SubscriptionManager.class, mSubscriptionManager);
+        mSpiedContext.addMockSystemService(TrustManager.class, mTrustManager);
+        mSpiedContext.addMockSystemService(FingerprintManager.class, mFingerprintManager);
+        mSpiedContext.addMockSystemService(BiometricManager.class, mBiometricManager);
+        mSpiedContext.addMockSystemService(FaceManager.class, mFaceManager);
+        mSpiedContext.addMockSystemService(UserManager.class, mUserManager);
+        mSpiedContext.addMockSystemService(DevicePolicyManager.class, mDevicePolicyManager);
+        mSpiedContext.addMockSystemService(SubscriptionManager.class, mSubscriptionManager);
+        mSpiedContext.addMockSystemService(TelephonyManager.class, mTelephonyManager);
 
         when(mRingerModeTracker.getRingerMode()).thenReturn(mRingerModeLiveData);
 
         mTestableLooper = TestableLooper.get(this);
         allowTestableLooperAsMainThread();
-        mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(context);
+        mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mSpiedContext);
     }
 
     @After
@@ -192,6 +197,22 @@
     }
 
     @Test
+    public void testSimStateInitialized() {
+        final int subId = 3;
+        final int state = TelephonyManager.SIM_STATE_ABSENT;
+
+        when(mTelephonyManager.getActiveModemCount()).thenReturn(1);
+        when(mTelephonyManager.getSimState(anyInt())).thenReturn(state);
+        when(mSubscriptionManager.getSubscriptionIds(anyInt())).thenReturn(new int[] { subId });
+
+        KeyguardUpdateMonitor testKUM = new TestableKeyguardUpdateMonitor(mSpiedContext);
+
+        mTestableLooper.processAllMessages();
+
+        assertThat(testKUM.getSimState(subId)).isEqualTo(state);
+    }
+
+    @Test
     public void testIgnoresSimStateCallback_rebroadcast() {
         Intent intent = new Intent(TelephonyIntents.ACTION_SIM_STATE_CHANGED);
 
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 9e18e61..3ef693a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
@@ -85,6 +85,7 @@
 import com.android.systemui.statusbar.phone.LockscreenLockIconController;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
 import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
+import com.android.systemui.statusbar.phone.NotificationShadeWindowView;
 import com.android.systemui.statusbar.phone.ShadeController;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -183,6 +184,8 @@
     private DumpManager mDumpManager;
     @Mock
     private LockscreenLockIconController mLockIconController;
+    @Mock
+    private NotificationShadeWindowView mNotificationShadeWindowView;
 
     private SuperStatusBarViewFactory mSuperStatusBarViewFactory;
     private BubbleData mBubbleData;
@@ -219,8 +222,7 @@
                 mWindowManager, mActivityManager, mDozeParameters, mStatusBarStateController,
                 mConfigurationController, mKeyguardBypassController, mColorExtractor,
                 mDumpManager);
-        mNotificationShadeWindowController.setNotificationShadeView(
-                mSuperStatusBarViewFactory.getNotificationShadeWindowView());
+        mNotificationShadeWindowController.setNotificationShadeView(mNotificationShadeWindowView);
         mNotificationShadeWindowController.attach();
 
         // Need notifications for bubbles
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 49236e0..8e6fc8a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java
@@ -79,6 +79,7 @@
 import com.android.systemui.statusbar.phone.LockscreenLockIconController;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
 import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
+import com.android.systemui.statusbar.phone.NotificationShadeWindowView;
 import com.android.systemui.statusbar.phone.ShadeController;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -134,6 +135,8 @@
     private KeyguardBypassController mKeyguardBypassController;
     @Mock
     private FloatingContentCoordinator mFloatingContentCoordinator;
+    @Mock
+    private NotificationShadeWindowView mNotificationShadeWindowView;
 
     private SysUiState mSysUiState = new SysUiState();
 
@@ -206,8 +209,7 @@
                 mWindowManager, mActivityManager, mDozeParameters, mStatusBarStateController,
                 mConfigurationController, mKeyguardBypassController, mColorExtractor,
                 mDumpManager);
-        mNotificationShadeWindowController.setNotificationShadeView(
-                mSuperStatusBarViewFactory.getNotificationShadeWindowView());
+        mNotificationShadeWindowController.setNotificationShadeView(mNotificationShadeWindowView);
         mNotificationShadeWindowController.attach();
 
         // Need notifications for bubbles
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
index f6ee46b..e3f25c6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
@@ -89,6 +89,7 @@
     @Captor
     private lateinit var controlLoadCallbackCaptor:
             ArgumentCaptor<ControlsBindingController.LoadCallback>
+
     @Captor
     private lateinit var broadcastReceiverCaptor: ArgumentCaptor<BroadcastReceiver>
     @Captor
@@ -97,6 +98,7 @@
 
     private lateinit var delayableExecutor: FakeExecutor
     private lateinit var controller: ControlsControllerImpl
+    private lateinit var canceller: DidRunRunnable
 
     companion object {
         fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
@@ -146,6 +148,9 @@
             }
         }
 
+        canceller = DidRunRunnable()
+        `when`(bindingController.bindAndLoad(any(), any())).thenReturn(canceller)
+
         controller = ControlsControllerImpl(
                 wrapper,
                 delayableExecutor,
@@ -266,7 +271,7 @@
 
             assertTrue(favorites.isEmpty())
             assertFalse(data.errorOnLoad)
-        })
+        }, Consumer {})
 
         verify(bindingController).bindAndLoad(eq(TEST_COMPONENT),
                 capture(controlLoadCallbackCaptor))
@@ -301,7 +306,7 @@
             assertEquals(1, favorites.size)
             assertEquals(TEST_CONTROL_ID, favorites[0])
             assertFalse(data.errorOnLoad)
-        })
+        }, Consumer {})
 
         verify(bindingController).bindAndLoad(eq(TEST_COMPONENT),
                 capture(controlLoadCallbackCaptor))
@@ -332,7 +337,7 @@
             assertEquals(1, favorites.size)
             assertEquals(TEST_CONTROL_ID, favorites[0])
             assertFalse(data.errorOnLoad)
-        })
+        }, Consumer {})
 
         verify(bindingController).bindAndLoad(eq(TEST_COMPONENT),
                 capture(controlLoadCallbackCaptor))
@@ -363,7 +368,7 @@
             assertEquals(1, favorites.size)
             assertEquals(TEST_CONTROL_ID, favorites[0])
             assertTrue(data.errorOnLoad)
-        })
+        }, Consumer {})
 
         verify(bindingController).bindAndLoad(eq(TEST_COMPONENT),
                 capture(controlLoadCallbackCaptor))
@@ -377,22 +382,15 @@
 
     @Test
     fun testCancelLoad() {
-        val canceller = object : Runnable {
-            var ran = false
-            override fun run() {
-                ran = true
-            }
-        }
-        `when`(bindingController.bindAndLoad(any(), any())).thenReturn(canceller)
-
         var loaded = false
+        var cancelRunnable: Runnable? = null
         controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO)
         delayableExecutor.runAllReady()
         controller.loadForComponent(TEST_COMPONENT, Consumer {
             loaded = true
-        })
+        }, Consumer { runnable -> cancelRunnable = runnable })
 
-        controller.cancelLoad()
+        cancelRunnable?.run()
         delayableExecutor.runAllReady()
 
         assertFalse(loaded)
@@ -400,61 +398,47 @@
     }
 
     @Test
-    fun testCancelLoad_noCancelAfterSuccessfulLoad() {
-        val canceller = object : Runnable {
-            var ran = false
-            override fun run() {
-                ran = true
-            }
-        }
-        `when`(bindingController.bindAndLoad(any(), any())).thenReturn(canceller)
-
+    fun testCancelLoad_afterSuccessfulLoad() {
         var loaded = false
+        var cancelRunnable: Runnable? = null
         controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO)
         delayableExecutor.runAllReady()
         controller.loadForComponent(TEST_COMPONENT, Consumer {
             loaded = true
-        })
+        }, Consumer { runnable -> cancelRunnable = runnable })
 
         verify(bindingController).bindAndLoad(eq(TEST_COMPONENT),
             capture(controlLoadCallbackCaptor))
 
         controlLoadCallbackCaptor.value.accept(emptyList())
 
-        controller.cancelLoad()
+        cancelRunnable?.run()
         delayableExecutor.runAllReady()
 
         assertTrue(loaded)
-        assertFalse(canceller.ran)
+        assertTrue(canceller.ran)
     }
 
     @Test
-    fun testCancelLoad_noCancelAfterErrorLoad() {
-        val canceller = object : Runnable {
-            var ran = false
-            override fun run() {
-                ran = true
-            }
-        }
-        `when`(bindingController.bindAndLoad(any(), any())).thenReturn(canceller)
-
+    fun testCancelLoad_afterErrorLoad() {
         var loaded = false
+        var cancelRunnable: Runnable? = null
         controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO)
         delayableExecutor.runAllReady()
         controller.loadForComponent(TEST_COMPONENT, Consumer {
             loaded = true
-        })
+        }, Consumer { runnable -> cancelRunnable = runnable })
 
         verify(bindingController).bindAndLoad(eq(TEST_COMPONENT),
             capture(controlLoadCallbackCaptor))
 
         controlLoadCallbackCaptor.value.error("")
 
-        controller.cancelLoad()
+        cancelRunnable?.run()
         delayableExecutor.runAllReady()
 
         assertTrue(loaded)
-        assertFalse(canceller.ran)
+        assertTrue(canceller.ran)
     }
 
     @Test
@@ -465,7 +449,7 @@
         val newControlInfo = TEST_CONTROL_INFO.copy(controlTitle = TEST_CONTROL_TITLE_2)
         val control = statelessBuilderFromInfo(newControlInfo).build()
 
-        controller.loadForComponent(TEST_COMPONENT, Consumer {})
+        controller.loadForComponent(TEST_COMPONENT, Consumer {}, Consumer {})
 
         verify(bindingController).bindAndLoad(eq(TEST_COMPONENT),
                 capture(controlLoadCallbackCaptor))
@@ -963,3 +947,10 @@
         assertTrue(controller.getFavoritesForStructure(TEST_COMPONENT_2, TEST_STRUCTURE).isEmpty())
     }
 }
+
+private class DidRunRunnable() : Runnable {
+    var ran = false
+    override fun run() {
+        ran = true
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java
index e8e98b4..4ac5912 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java
@@ -133,4 +133,12 @@
 
         verify(mController, times(1)).stopRecording();
     }
+
+    @Test
+    public void testContentDescriptionHasTileName() {
+        mTile.refreshState();
+        mTestableLooper.processAllMessages();
+
+        assertTrue(mTile.getState().contentDescription.toString().contains(mTile.getState().label));
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java
index 11649ca..ffe3cd5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java
@@ -16,6 +16,10 @@
 
 package com.android.systemui.screenshot;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
@@ -40,7 +44,6 @@
 import com.android.systemui.SystemUIFactory;
 import com.android.systemui.SysuiTestCase;
 
-import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -82,9 +85,9 @@
         CompletableFuture<List<Notification.Action>> smartActionsFuture =
                 ScreenshotSmartActions.getSmartActionsFuture("", "", bitmap,
                         smartActionsProvider, true, false);
-        Assert.assertNotNull(smartActionsFuture);
+        assertNotNull(smartActionsFuture);
         List<Notification.Action> smartActions = smartActionsFuture.get(5, TimeUnit.MILLISECONDS);
-        Assert.assertEquals(Collections.emptyList(), smartActions);
+        assertEquals(Collections.emptyList(), smartActions);
     }
 
     // Tests any exception thrown in waiting for smart actions future to complete does
@@ -99,7 +102,7 @@
                 RuntimeException.class);
         List<Notification.Action> actions = ScreenshotSmartActions.getSmartActions(
                 "", "", smartActionsFuture, timeoutMs, mSmartActionsProvider);
-        Assert.assertEquals(Collections.emptyList(), actions);
+        assertEquals(Collections.emptyList(), actions);
     }
 
     // Tests any exception thrown in notifying feedback does not affect regular screenshot flow.
@@ -123,9 +126,9 @@
                         mSmartActionsProvider, true, true);
         verify(mSmartActionsProvider, never()).getActions(any(), any(), any(), any(),
                 eq(false));
-        Assert.assertNotNull(smartActionsFuture);
+        assertNotNull(smartActionsFuture);
         List<Notification.Action> smartActions = smartActionsFuture.get(5, TimeUnit.MILLISECONDS);
-        Assert.assertEquals(Collections.emptyList(), smartActions);
+        assertEquals(Collections.emptyList(), smartActions);
     }
 
     // Tests for a hardware bitmap, ScreenshotNotificationSmartActionsProvider is invoked once.
@@ -152,14 +155,14 @@
                 ScreenshotSmartActions.getSmartActionsFuture("", "", bitmap,
                         actionsProvider,
                         true, true);
-        Assert.assertNotNull(smartActionsFuture);
+        assertNotNull(smartActionsFuture);
         List<Notification.Action> smartActions = smartActionsFuture.get(5, TimeUnit.MILLISECONDS);
-        Assert.assertEquals(smartActions.size(), 0);
+        assertEquals(smartActions.size(), 0);
     }
 
-    // Tests for notification action extras.
+    // Tests for share action extras
     @Test
-    public void testNotificationActionExtras() {
+    public void testShareActionExtras() {
         if (Looper.myLooper() == null) {
             Looper.prepare();
         }
@@ -169,35 +172,70 @@
         data.image = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
         data.finisher = null;
         data.mActionsReadyListener = null;
-        data.createDeleteAction = true;
         SaveImageInBackgroundTask task = new SaveImageInBackgroundTask(mContext, data);
-        List<Notification.Action> actions = task.populateNotificationActions(
-                mContext, mContext.getResources(),
+
+        Notification.Action shareAction = task.createShareAction(mContext, mContext.getResources(),
                 Uri.parse("Screenshot_123.png"));
 
-        Assert.assertEquals(actions.size(), 3);
-        boolean isShareFound = false;
-        boolean isEditFound = false;
-        boolean isDeleteFound = false;
-        for (Notification.Action action : actions) {
-            Intent intent = action.actionIntent.getIntent();
-            Assert.assertNotNull(intent);
-            Bundle bundle = intent.getExtras();
-            Assert.assertTrue(bundle.containsKey(GlobalScreenshot.EXTRA_ID));
-            Assert.assertTrue(
-                    bundle.containsKey(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED));
+        Intent intent = shareAction.actionIntent.getIntent();
+        assertNotNull(intent);
+        Bundle bundle = intent.getExtras();
+        assertTrue(bundle.containsKey(GlobalScreenshot.EXTRA_ID));
+        assertTrue(bundle.containsKey(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED));
+        assertEquals(GlobalScreenshot.ACTION_TYPE_SHARE, shareAction.title);
+        assertEquals(Intent.ACTION_SEND, intent.getAction());
+    }
 
-            if (action.title.equals(GlobalScreenshot.ACTION_TYPE_DELETE)) {
-                isDeleteFound = intent.getAction() == null;
-            } else if (action.title.equals(GlobalScreenshot.ACTION_TYPE_EDIT)) {
-                isEditFound = Intent.ACTION_EDIT.equals(intent.getAction());
-            } else if (action.title.equals(GlobalScreenshot.ACTION_TYPE_SHARE)) {
-                isShareFound = Intent.ACTION_SEND.equals(intent.getAction());
-            }
+    // Tests for edit action extras
+    @Test
+    public void testEditActionExtras() {
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
         }
 
-        Assert.assertTrue(isEditFound);
-        Assert.assertTrue(isDeleteFound);
-        Assert.assertTrue(isShareFound);
+        GlobalScreenshot.SaveImageInBackgroundData
+                data = new GlobalScreenshot.SaveImageInBackgroundData();
+        data.image = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
+        data.finisher = null;
+        data.mActionsReadyListener = null;
+        SaveImageInBackgroundTask task = new SaveImageInBackgroundTask(mContext, data);
+
+        Notification.Action editAction = task.createEditAction(mContext, mContext.getResources(),
+                Uri.parse("Screenshot_123.png"));
+
+        Intent intent = editAction.actionIntent.getIntent();
+        assertNotNull(intent);
+        Bundle bundle = intent.getExtras();
+        assertTrue(bundle.containsKey(GlobalScreenshot.EXTRA_ID));
+        assertTrue(bundle.containsKey(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED));
+        assertEquals(GlobalScreenshot.ACTION_TYPE_EDIT, editAction.title);
+        assertEquals(Intent.ACTION_EDIT, intent.getAction());
+    }
+
+    // Tests for share action extras
+    @Test
+    public void testDeleteActionExtras() {
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+
+        GlobalScreenshot.SaveImageInBackgroundData
+                data = new GlobalScreenshot.SaveImageInBackgroundData();
+        data.image = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
+        data.finisher = null;
+        data.mActionsReadyListener = null;
+        SaveImageInBackgroundTask task = new SaveImageInBackgroundTask(mContext, data);
+
+        Notification.Action deleteAction = task.createDeleteAction(mContext,
+                mContext.getResources(),
+                Uri.parse("Screenshot_123.png"));
+
+        Intent intent = deleteAction.actionIntent.getIntent();
+        assertNotNull(intent);
+        Bundle bundle = intent.getExtras();
+        assertTrue(bundle.containsKey(GlobalScreenshot.EXTRA_ID));
+        assertTrue(bundle.containsKey(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED));
+        assertEquals(deleteAction.title, GlobalScreenshot.ACTION_TYPE_DELETE);
+        assertNull(intent.getAction());
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/CurrentUserContextTrackerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/CurrentUserContextTrackerTest.kt
new file mode 100644
index 0000000..628c06a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/CurrentUserContextTrackerTest.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.settings
+
+import android.content.Context
+import android.content.ContextWrapper
+import android.os.UserHandle
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.BroadcastDispatcher
+import junit.framework.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class CurrentUserContextTrackerTest : SysuiTestCase() {
+
+    private lateinit var tracker: CurrentUserContextTracker
+    @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        allowTestableLooperAsMainThread()
+
+        // wrap Context so that tests don't throw for missing package errors
+        val wrapped = object : ContextWrapper(context) {
+            override fun createContextAsUser(user: UserHandle, flags: Int): Context {
+                val mockContext = mock(Context::class.java)
+                `when`(mockContext.user).thenReturn(user)
+                `when`(mockContext.userId).thenReturn(user.identifier)
+                return mockContext
+            }
+        }
+
+        tracker = CurrentUserContextTracker(wrapped, broadcastDispatcher)
+        tracker.initialize()
+    }
+
+    @Test
+    fun testContextExistsAfterInit_noCrash() {
+        tracker.currentUserContext
+    }
+
+    @Test
+    fun testUserContextIsCorrectAfterUserSwitch() {
+        // We always start out with system ui test
+        assertTrue("Starting userId should be 0", tracker.currentUserContext.userId == 0)
+
+        // WHEN user changes
+        tracker.handleUserSwitched(1)
+
+        // THEN user context should have the correct userId
+        assertTrue("User has changed to userId 1, the context should reflect that",
+                tracker.currentUserContext.userId == 1)
+    }
+
+    @Suppress("UNUSED_PARAMETER")
+    @Test(expected = IllegalStateException::class)
+    fun testContextTrackerThrowsExceptionWhenNotInitialized() {
+        // GIVEN an uninitialized CurrentUserContextTracker
+        val userTracker = CurrentUserContextTracker(context, broadcastDispatcher)
+
+        // WHEN client asks for a context
+        val userContext = userTracker.currentUserContext
+
+        // THEN an exception is thrown
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java
index 61388b6..6db8685 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java
@@ -48,9 +48,9 @@
 import android.app.Notification;
 import android.app.NotificationChannel;
 import android.app.NotificationChannelGroup;
+import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.app.Person;
-import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherApps;
@@ -91,6 +91,7 @@
 import org.mockito.Answers;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 import org.mockito.stubbing.Answer;
@@ -147,14 +148,15 @@
     private ShadeController mShadeController;
     @Mock
     private ConversationIconFactory mIconFactory;
-    @Mock
-    private Context mUserContext;
     @Mock(answer = Answers.RETURNS_SELF)
     private PriorityOnboardingDialogController.Builder mBuilder;
     private Provider<PriorityOnboardingDialogController.Builder> mBuilderProvider = () -> mBuilder;
+    @Mock
+    private Notification.BubbleMetadata mBubbleMetadata;
 
     @Before
     public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
         mTestableLooper = TestableLooper.get(this);
 
         mDependency.injectTestDependency(Dependency.BG_LOOPER, mTestableLooper.getLooper());
@@ -228,6 +230,11 @@
         when(mMockINotificationManager.getConversationNotificationChannel(anyString(), anyInt(),
                 anyString(), eq(TEST_CHANNEL), eq(false), eq(CONVERSATION_ID)))
                 .thenReturn(mConversationChannel);
+
+        when(mMockINotificationManager.getConsolidatedNotificationPolicy())
+                .thenReturn(mock(NotificationManager.Policy.class));
+
+        when(mBuilder.build()).thenReturn(mock(PriorityOnboardingDialogController.class));
     }
 
     @Test
@@ -240,10 +247,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
         final ImageView view = mNotificationInfo.findViewById(R.id.conversation_icon);
@@ -261,10 +269,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
         final TextView textView = mNotificationInfo.findViewById(R.id.pkg_name);
@@ -283,7 +292,8 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
-                null,
+    mBubbleMetadata,
+    null,
                 null,
                 null,
                 true);
@@ -308,10 +318,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
         final TextView textView = mNotificationInfo.findViewById(R.id.group_name);
@@ -331,10 +342,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
         final TextView textView = mNotificationInfo.findViewById(R.id.group_name);
@@ -353,10 +365,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
         final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name);
@@ -382,10 +395,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 entry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
         final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name);
@@ -404,13 +418,14 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 (View v, NotificationChannel c, int appUid) -> {
                     assertEquals(mConversationChannel, c);
                     latch.countDown();
                 },
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
 
@@ -430,10 +445,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
         final View settingsButton = mNotificationInfo.findViewById(R.id.info);
@@ -451,13 +467,14 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 (View v, NotificationChannel c, int appUid) -> {
                     assertEquals(mNotificationChannel, c);
                     latch.countDown();
                 },
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 false);
         final View settingsButton = mNotificationInfo.findViewById(R.id.info);
@@ -476,10 +493,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
         View view = mNotificationInfo.findViewById(R.id.silence);
@@ -501,10 +519,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
         View view = mNotificationInfo.findViewById(R.id.default_behavior);
@@ -529,10 +548,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
         View view = mNotificationInfo.findViewById(R.id.default_behavior);
@@ -556,10 +576,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
 
@@ -596,10 +617,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
 
@@ -635,10 +657,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
 
@@ -675,10 +698,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
 
@@ -709,10 +733,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
 
@@ -741,10 +766,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
 
@@ -774,10 +800,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
 
@@ -807,10 +834,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
 
@@ -839,10 +867,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
 
@@ -870,10 +899,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
 
@@ -892,10 +922,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
 
@@ -910,13 +941,12 @@
 
         // GIVEN the priority onboarding screen is present
         PriorityOnboardingDialogController.Builder b =
-                new PriorityOnboardingDialogController.Builder();
+                mock(PriorityOnboardingDialogController.Builder.class, Answers.RETURNS_SELF);
         PriorityOnboardingDialogController controller =
                 mock(PriorityOnboardingDialogController.class);
         when(b.build()).thenReturn(controller);
 
         // GIVEN the user is changing conversation settings
-        when(mBuilderProvider.get()).thenReturn(b);
         mNotificationInfo.bindNotification(
                 mShortcutManager,
                 mMockPackageManager,
@@ -925,11 +955,12 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
-                mBuilderProvider,
+                mContext,
+                () -> b,
                 true);
 
         // WHEN user clicks "priority"
@@ -945,12 +976,11 @@
         Prefs.putBoolean(mContext, Prefs.Key.HAS_SEEN_PRIORITY_ONBOARDING, true);
 
         PriorityOnboardingDialogController.Builder b =
-                new PriorityOnboardingDialogController.Builder();
+                mock(PriorityOnboardingDialogController.Builder.class, Answers.RETURNS_SELF);
         PriorityOnboardingDialogController controller =
                 mock(PriorityOnboardingDialogController.class);
         when(b.build()).thenReturn(controller);
 
-        when(mBuilderProvider.get()).thenReturn(b);
         mNotificationInfo.bindNotification(
                 mShortcutManager,
                 mMockPackageManager,
@@ -959,11 +989,12 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
-                mBuilderProvider,
+                mContext,
+                () -> b,
                 true);
 
         // WHEN user clicks "priority"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaTest.kt
index 2b091f2..210744e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaTest.kt
@@ -6,11 +6,18 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.assist.AssistManager
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.statusbar.policy.AccessibilityController
+import com.android.systemui.statusbar.policy.FlashlightController
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.tuner.TunerService
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.MockitoAnnotations
+import java.util.concurrent.Executor
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
@@ -24,6 +31,15 @@
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
+        // Mocked dependencies
+        mDependency.injectMockDependency(AccessibilityController::class.java)
+        mDependency.injectMockDependency(ActivityStarter::class.java)
+        mDependency.injectMockDependency(AssistManager::class.java)
+        mDependency.injectTestDependency(Executor::class.java, Executor { it.run() })
+        mDependency.injectMockDependency(FlashlightController::class.java)
+        mDependency.injectMockDependency(KeyguardStateController::class.java)
+        mDependency.injectMockDependency(TunerService::class.java)
+
         mKeyguardBottomArea = LayoutInflater.from(mContext).inflate(
                 R.layout.keyguard_bottom_area, null, false) as KeyguardBottomAreaView
         mKeyguardBottomArea.setStatusBar(mStatusBar)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java
index e052ae2..0a041e4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java
@@ -26,7 +26,6 @@
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.verifyZeroInteractions;
@@ -37,6 +36,7 @@
 import android.os.Handler;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
+import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
 import android.widget.FrameLayout;
@@ -64,6 +64,7 @@
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
+import org.mockito.stubbing.Answer;
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
@@ -94,9 +95,11 @@
     private Handler mHandler;
     @Mock
     private KeyguardSecurityModel mKeyguardSecurityModel;
+    @Mock
+    private ViewGroup mRootView;
     @Rule
     public MockitoRule mRule = MockitoJUnit.rule();
-    private ViewGroup mRootView;
+    private Integer mRootVisibility = View.INVISIBLE;
     private KeyguardBouncer mBouncer;
 
     @Before
@@ -105,6 +108,11 @@
         mDependency.injectTestDependency(KeyguardUpdateMonitor.class, mKeyguardUpdateMonitor);
         mDependency.injectTestDependency(KeyguardSecurityModel.class, mKeyguardSecurityModel);
         mDependency.injectMockDependency(KeyguardStateController.class);
+        when(mRootView.getVisibility()).thenAnswer((Answer<Integer>) invocation -> mRootVisibility);
+        doAnswer(invocation -> {
+            mRootVisibility = invocation.getArgument(0);
+            return null;
+        }).when(mRootView).setVisibility(anyInt());
         when(mKeyguardSecurityModel.getSecurityMode(anyInt()))
                 .thenReturn(KeyguardSecurityModel.SecurityMode.None);
         DejankUtils.setImmediate(true);
@@ -117,10 +125,8 @@
                 mKeyguardBypassController, mHandler) {
             @Override
             protected void inflateView() {
-                super.inflateView();
                 mKeyguardView = mKeyguardHostView;
-                mRoot = spy(mRoot);
-                mRootView = mRoot;
+                mRoot = mRootView;
             }
         };
     }
@@ -212,8 +218,10 @@
         verify(mExpansionCallback).onFullyShown();
 
         verify(mExpansionCallback, never()).onStartingToHide();
+        verify(mKeyguardHostView, never()).onStartingToHide();
         mBouncer.setExpansion(0.9f);
         verify(mExpansionCallback).onStartingToHide();
+        verify(mKeyguardHostView).onStartingToHide();
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java
index 8ab660c..3e46907 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java
@@ -56,7 +56,7 @@
         when(mStatusBarIconController.getTransitionsController()).thenReturn(
                 mLightBarTransitionsController);
         mLightBarController = new LightBarController(mContext, mStatusBarIconController,
-                mock(BatteryController.class));
+                mock(BatteryController.class), mock(NavigationModeController.class));
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarTransitionsTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarTransitionsTest.java
index 27a5002..14c6e9f9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarTransitionsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarTransitionsTest.java
@@ -17,6 +17,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
@@ -50,9 +51,11 @@
         mDependency.injectMockDependency(IWindowManager.class);
         mDependency.injectMockDependency(AssistManager.class);
         mDependency.injectMockDependency(OverviewProxyService.class);
-        mDependency.injectMockDependency(NavigationModeController.class);
         mDependency.injectMockDependency(StatusBarStateController.class);
         mDependency.injectMockDependency(KeyguardStateController.class);
+        doReturn(mContext)
+                .when(mDependency.injectMockDependency(NavigationModeController.class))
+                .getCurrentUserContext();
 
         NavigationBarView navBar = spy(new NavigationBarView(mContext, null));
         when(navBar.getCurrentView()).thenReturn(navBar);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
index dd28687..1afe132 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
@@ -176,25 +176,43 @@
         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
         when(mFeatureFlags.isNewNotifPipelineRenderingEnabled()).thenReturn(false);
 
-        mNotificationActivityStarter = (new StatusBarNotificationActivityStarter.Builder(
-                getContext(), mock(CommandQueue.class), () -> mAssistManager,
-                mEntryManager, mock(HeadsUpManagerPhone.class),
-                mActivityStarter, mStatusBarService,
-                mock(StatusBarStateController.class), mStatusBarKeyguardViewManager,
-                mock(KeyguardManager.class),
-                mock(IDreamManager.class), mRemoteInputManager,
-                mock(StatusBarRemoteInputCallback.class), mock(NotificationGroupManager.class),
-                mock(NotificationLockscreenUserManager.class),
-                mKeyguardStateController,
-                mock(NotificationInterruptStateProvider.class), mock(MetricsLogger.class),
-                mock(LockPatternUtils.class), mHandler, mHandler, mUiBgExecutor,
-                mActivityIntentHelper, mBubbleController, mShadeController, mFeatureFlags,
-                mNotifPipeline, mNotifCollection)
+        mNotificationActivityStarter =
+                new StatusBarNotificationActivityStarter.Builder(
+                        getContext(),
+                        mock(CommandQueue.class),
+                        mHandler,
+                        mHandler,
+                        mUiBgExecutor,
+                        mEntryManager,
+                        mNotifPipeline,
+                        mNotifCollection,
+                        mock(HeadsUpManagerPhone.class),
+                        mActivityStarter,
+                        mStatusBarService,
+                        mock(StatusBarStateController.class),
+                        mStatusBarKeyguardViewManager,
+                        mock(KeyguardManager.class),
+                        mock(IDreamManager.class),
+                        mBubbleController,
+                        () -> mAssistManager,
+                        mRemoteInputManager,
+                        mock(NotificationGroupManager.class),
+                        mock(NotificationLockscreenUserManager.class),
+                        mShadeController,
+                        mKeyguardStateController,
+                        mock(NotificationInterruptStateProvider.class),
+                        mock(LockPatternUtils.class),
+                        mock(StatusBarRemoteInputCallback.class),
+                        mActivityIntentHelper,
+
+                        mFeatureFlags,
+                        mock(MetricsLogger.class),
+                        mock(StatusBarNotificationActivityStarterLogger.class))
                 .setStatusBar(mStatusBar)
-                .setNotificationPanelViewController(mock(NotificationPanelViewController.class))
                 .setNotificationPresenter(mock(NotificationPresenter.class))
-                .setActivityLaunchAnimator(mock(ActivityLaunchAnimator.class)))
-        .build();
+                .setNotificationPanelViewController(mock(NotificationPanelViewController.class))
+                .setActivityLaunchAnimator(mock(ActivityLaunchAnimator.class))
+                .build();
 
         // set up dismissKeyguardThenExecute to synchronously invoke the OnDismissAction arg
         doAnswer(mCallOnDismiss).when(mActivityStarter).dismissKeyguardThenExecute(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java
index 962d773..aef454f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java
@@ -32,6 +32,7 @@
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.Instrumentation;
@@ -222,7 +223,7 @@
 
         ArgumentCaptor<ConnectivityManager.NetworkCallback> callbackArg =
             ArgumentCaptor.forClass(ConnectivityManager.NetworkCallback.class);
-        Mockito.verify(mMockCm, atLeastOnce())
+        verify(mMockCm, atLeastOnce())
             .registerDefaultNetworkCallback(callbackArg.capture(), isA(Handler.class));
         mNetworkCallback = callbackArg.getValue();
         assertNotNull(mNetworkCallback);
@@ -384,7 +385,7 @@
     }
 
     protected void verifyHasNoSims(boolean hasNoSimsVisible) {
-        Mockito.verify(mCallbackHandler, Mockito.atLeastOnce()).setNoSims(
+        verify(mCallbackHandler, Mockito.atLeastOnce()).setNoSims(
                 eq(hasNoSimsVisible), eq(false));
     }
 
@@ -395,7 +396,7 @@
         ArgumentCaptor<Boolean> dataInArg = ArgumentCaptor.forClass(Boolean.class);
         ArgumentCaptor<Boolean> dataOutArg = ArgumentCaptor.forClass(Boolean.class);
 
-        Mockito.verify(mCallbackHandler, Mockito.atLeastOnce()).setMobileDataIndicators(
+        verify(mCallbackHandler, Mockito.atLeastOnce()).setMobileDataIndicators(
                     any(),
                     iconArg.capture(),
                     anyInt(),
@@ -429,7 +430,7 @@
         ArgumentCaptor<Integer> typeIconArg = ArgumentCaptor.forClass(Integer.class);
 
         // TODO: Verify all fields.
-        Mockito.verify(mCallbackHandler, Mockito.atLeastOnce()).setMobileDataIndicators(
+        verify(mCallbackHandler, Mockito.atLeastOnce()).setMobileDataIndicators(
                 iconArg.capture(),
                 any(),
                 typeIconArg.capture(),
@@ -475,7 +476,7 @@
         ArgumentCaptor<CharSequence> typeContentDescriptionHtmlArg =
                 ArgumentCaptor.forClass(CharSequence.class);
 
-        Mockito.verify(mCallbackHandler, Mockito.atLeastOnce()).setMobileDataIndicators(
+        verify(mCallbackHandler, Mockito.atLeastOnce()).setMobileDataIndicators(
                 iconArg.capture(),
                 qsIconArg.capture(),
                 typeIconArg.capture(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerWifiTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerWifiTest.java
index 9c250c5..988e022 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerWifiTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerWifiTest.java
@@ -2,12 +2,14 @@
 
 import static junit.framework.Assert.assertEquals;
 
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyBoolean;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import android.content.Intent;
+import android.net.ConnectivityManager;
 import android.net.NetworkCapabilities;
 import android.net.NetworkInfo;
 import android.net.wifi.WifiInfo;
@@ -171,6 +173,32 @@
         verifyLastWifiIcon(true, WifiIcons.WIFI_SIGNAL_STRENGTH[1][testLevel]);
     }
 
+    @Test
+    public void testFetchInitialData() {
+        mNetworkController.mWifiSignalController.fetchInitialState();
+        Mockito.verify(mMockWm).getWifiState();
+        Mockito.verify(mMockCm).getNetworkInfo(ConnectivityManager.TYPE_WIFI);
+    }
+
+    @Test
+    public void testFetchInitialData_correctValues() {
+        String testSsid = "TEST";
+
+        when(mMockWm.getWifiState()).thenReturn(WifiManager.WIFI_STATE_ENABLED);
+        NetworkInfo networkInfo = mock(NetworkInfo.class);
+        when(networkInfo.isConnected()).thenReturn(true);
+        when(mMockCm.getNetworkInfo(ConnectivityManager.TYPE_WIFI)).thenReturn(networkInfo);
+        WifiInfo wifiInfo = mock(WifiInfo.class);
+        when(wifiInfo.getSSID()).thenReturn(testSsid);
+        when(mMockWm.getConnectionInfo()).thenReturn(wifiInfo);
+
+        mNetworkController.mWifiSignalController.fetchInitialState();
+
+        assertTrue(mNetworkController.mWifiSignalController.mCurrentState.enabled);
+        assertTrue(mNetworkController.mWifiSignalController.mCurrentState.connected);
+        assertEquals(testSsid, mNetworkController.mWifiSignalController.mCurrentState.ssid);
+    }
+
     protected void setWifiActivity(int activity) {
         // TODO: Not this, because this variable probably isn't sticking around.
         mNetworkController.mWifiSignalController.setActivity(activity);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/RepeatableExecutorTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/RepeatableExecutorTest.java
new file mode 100644
index 0000000..00f37ae
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/RepeatableExecutorTest.java
@@ -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.concurrency;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.util.time.FakeSystemClock;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class RepeatableExecutorTest extends SysuiTestCase {
+
+    private static final int DELAY = 100;
+
+    private FakeSystemClock mFakeClock;
+    private FakeExecutor mFakeExecutor;
+    private RepeatableExecutor mExecutor;
+    private CountingTask mCountingTask;
+
+    @Before
+    public void setUp() throws Exception {
+        mFakeClock = new FakeSystemClock();
+        mFakeExecutor = new FakeExecutor(mFakeClock);
+        mCountingTask = new CountingTask();
+        mExecutor = new RepeatableExecutorImpl(mFakeExecutor);
+    }
+
+    /**
+     * Test FakeExecutor that receives non-delayed items to execute.
+     */
+    @Test
+    public void testExecute() {
+        mExecutor.execute(mCountingTask);
+        mFakeExecutor.runAllReady();
+        assertThat(mCountingTask.getCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void testRepeats() {
+        // GIVEN that a command is queued to repeat
+        mExecutor.executeRepeatedly(mCountingTask, DELAY, DELAY);
+        // WHEN The clock advances and the task is run
+        mFakeExecutor.advanceClockToNext();
+        mFakeExecutor.runAllReady();
+        // THEN another task is queued
+        assertThat(mCountingTask.getCount()).isEqualTo(1);
+        assertThat(mFakeExecutor.numPending()).isEqualTo(1);
+    }
+
+    @Test
+    public void testNoExecutionBeforeStartDelay() {
+        // WHEN a command is queued with a start delay
+        mExecutor.executeRepeatedly(mCountingTask, 2 * DELAY, DELAY);
+        mFakeExecutor.runAllReady();
+        // THEN then it doesn't run immediately
+        assertThat(mCountingTask.getCount()).isEqualTo(0);
+        assertThat(mFakeExecutor.numPending()).isEqualTo(1);
+    }
+
+    @Test
+    public void testExecuteAfterStartDelay() {
+        // GIVEN that a command is queued to repeat with a longer start delay
+        mExecutor.executeRepeatedly(mCountingTask, 2 * DELAY, DELAY);
+        // WHEN the clock advances the start delay
+        mFakeClock.advanceTime(2 * DELAY);
+        mFakeExecutor.runAllReady();
+        // THEN the command has run and another task is queued
+        assertThat(mCountingTask.getCount()).isEqualTo(1);
+        assertThat(mFakeExecutor.numPending()).isEqualTo(1);
+    }
+
+    @Test
+    public void testExecuteWithZeroStartDelay() {
+        // WHEN a command is queued with no start delay
+        mExecutor.executeRepeatedly(mCountingTask, 0L, DELAY);
+        mFakeExecutor.runAllReady();
+        // THEN the command has run and another task is queued
+        assertThat(mCountingTask.getCount()).isEqualTo(1);
+        assertThat(mFakeExecutor.numPending()).isEqualTo(1);
+    }
+
+    @Test
+    public void testAdvanceTimeTwice() {
+        // GIVEN that a command is queued to repeat
+        mExecutor.executeRepeatedly(mCountingTask, DELAY, DELAY);
+        // WHEN the clock advances the time DELAY twice
+        mFakeClock.advanceTime(DELAY);
+        mFakeExecutor.runAllReady();
+        mFakeClock.advanceTime(DELAY);
+        mFakeExecutor.runAllReady();
+        // THEN the command has run twice and another task is queued
+        assertThat(mCountingTask.getCount()).isEqualTo(2);
+        assertThat(mFakeExecutor.numPending()).isEqualTo(1);
+    }
+
+    @Test
+    public void testCancel() {
+        // GIVEN that a scheduled command has been cancelled
+        Runnable cancel = mExecutor.executeRepeatedly(mCountingTask, DELAY, DELAY);
+        cancel.run();
+        // WHEN the clock advances the time DELAY
+        mFakeClock.advanceTime(DELAY);
+        mFakeExecutor.runAllReady();
+        // THEN the comamnd has not run and no further tasks are queued
+        assertThat(mCountingTask.getCount()).isEqualTo(0);
+        assertThat(mFakeExecutor.numPending()).isEqualTo(0);
+    }
+
+    @Test
+    public void testCancelAfterStart() {
+        // GIVEN that a command has reapeated a few times
+        Runnable cancel = mExecutor.executeRepeatedly(mCountingTask, DELAY, DELAY);
+        mFakeClock.advanceTime(DELAY);
+        mFakeExecutor.runAllReady();
+        // WHEN cancelled and time advances
+        cancel.run();
+        // THEN the command has only run the first time
+        assertThat(mCountingTask.getCount()).isEqualTo(1);
+        assertThat(mFakeExecutor.numPending()).isEqualTo(0);
+    }
+
+    /**
+     * Runnable used for testing that counts the number of times run() is invoked.
+     */
+    private static class CountingTask implements Runnable {
+
+        private int mRunCount;
+
+        @Override
+        public void run() {
+            mRunCount++;
+        }
+
+        /** Gets the run count. */
+        public int getCount() {
+            return mRunCount;
+        }
+    }
+}
diff --git a/packages/Tethering/res/values-mcc310-mnc004/strings.xml b/packages/Tethering/res/values-mcc310-mnc004/strings.xml
index 9dadd49..ce9ff60 100644
--- a/packages/Tethering/res/values-mcc310-mnc004/strings.xml
+++ b/packages/Tethering/res/values-mcc310-mnc004/strings.xml
@@ -25,6 +25,4 @@
     <string name="upstream_roaming_notification_title">Hotspot or tethering is on</string>
     <!-- String for cellular roaming notification message [CHAR LIMIT=500] -->
     <string name="upstream_roaming_notification_message">Additional charges may apply while roaming</string>
-    <!-- String for cellular roaming notification continue button [CHAR LIMIT=200] -->
-    <string name="upstream_roaming_notification_continue_button">Continue</string>
 </resources>
diff --git a/packages/Tethering/res/values-mcc310-mnc120/strings.xml b/packages/Tethering/res/values-mcc310-mnc120/strings.xml
deleted file mode 100644
index 618df90..0000000
--- a/packages/Tethering/res/values-mcc310-mnc120/strings.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2020 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <!-- String for tethered notification title with client number info. -->
-    <plurals name="tethered_notification_title_with_client_number">
-        <item quantity="one"><xliff:g>%1$d</xliff:g> device connected.</item>
-        <item quantity="other"><xliff:g>%1$d</xliff:g> devices connected.</item>
-    </plurals>
-</resources>
\ No newline at end of file
diff --git a/packages/Tethering/res/values-mcc311-mnc480/strings.xml b/packages/Tethering/res/values-mcc311-mnc480/strings.xml
index 9dadd49..ce9ff60 100644
--- a/packages/Tethering/res/values-mcc311-mnc480/strings.xml
+++ b/packages/Tethering/res/values-mcc311-mnc480/strings.xml
@@ -25,6 +25,4 @@
     <string name="upstream_roaming_notification_title">Hotspot or tethering is on</string>
     <!-- String for cellular roaming notification message [CHAR LIMIT=500] -->
     <string name="upstream_roaming_notification_message">Additional charges may apply while roaming</string>
-    <!-- String for cellular roaming notification continue button [CHAR LIMIT=200] -->
-    <string name="upstream_roaming_notification_continue_button">Continue</string>
 </resources>
diff --git a/packages/Tethering/res/values-mcc311-mnc490/strings.xml b/packages/Tethering/res/values-mcc311-mnc490/strings.xml
deleted file mode 100644
index 618df90..0000000
--- a/packages/Tethering/res/values-mcc311-mnc490/strings.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2020 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <!-- String for tethered notification title with client number info. -->
-    <plurals name="tethered_notification_title_with_client_number">
-        <item quantity="one"><xliff:g>%1$d</xliff:g> device connected.</item>
-        <item quantity="other"><xliff:g>%1$d</xliff:g> devices connected.</item>
-    </plurals>
-</resources>
\ No newline at end of file
diff --git a/packages/Tethering/res/values-mcc312-mnc530/strings.xml b/packages/Tethering/res/values-mcc312-mnc530/strings.xml
deleted file mode 100644
index 618df90..0000000
--- a/packages/Tethering/res/values-mcc312-mnc530/strings.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2020 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <!-- String for tethered notification title with client number info. -->
-    <plurals name="tethered_notification_title_with_client_number">
-        <item quantity="one"><xliff:g>%1$d</xliff:g> device connected.</item>
-        <item quantity="other"><xliff:g>%1$d</xliff:g> devices connected.</item>
-    </plurals>
-</resources>
\ No newline at end of file
diff --git a/packages/Tethering/res/values/config.xml b/packages/Tethering/res/values/config.xml
index 66fbefc..83c99d2 100644
--- a/packages/Tethering/res/values/config.xml
+++ b/packages/Tethering/res/values/config.xml
@@ -158,51 +158,6 @@
     <!-- ComponentName of the service used to run no ui tether provisioning. -->
     <string translatable="false" name="config_wifi_tether_enable">com.android.settings/.wifi.tether.TetherService</string>
 
-    <!-- Enable tethering notification -->
-    <!-- Icons for showing tether enable notification.
-         Each item should have two elements and be separated with ";".
-
-         The first element is downstream types which is one of tethering. This element has to be
-         made by WIFI, USB, BT, and OR'd with the others. Use "|" to combine multiple downstream
-         types and use "," to separate each combinations. Such as
-
-             USB|BT,WIFI|USB|BT
-
-         The second element is icon for the item. This element has to be composed by
-         <package name>:drawable/<resource name>. Such as
-
-             1. com.android.networkstack.tethering:drawable/stat_sys_tether_general
-             2. android:drawable/xxx
-
-         So the entire string of each item would be
-
-             USB|BT,WIFI|USB|BT;com.android.networkstack.tethering:drawable/stat_sys_tether_general
-
-         NOTE: One config can be separated into two or more for readability. Such as
-
-               WIFI|USB,WIFI|BT,USB|BT,WIFI|USB|BT;android:drawable/xxx
-
-               can be separated into
-
-               WIFI|USB;android:drawable/xxx
-               WIFI|BT;android:drawable/xxx
-               USB|BT;android:drawable/xxx
-               WIFI|USB|BT;android:drawable/xxx
-
-         Notification will not show if the downstream type isn't listed in array.
-         Empty array means disable notifications. -->
-    <!-- In AOSP, hotspot is configured to no notification by default. Because status bar has showed
-         an icon on the right side already -->
-    <string-array translatable="false" name="tethering_notification_icons">
-        <item>USB;com.android.networkstack.tethering:drawable/stat_sys_tether_usb</item>
-        <item>BT;com.android.networkstack.tethering:drawable/stat_sys_tether_bluetooth</item>
-        <item>WIFI|USB,WIFI|BT,USB|BT,WIFI|USB|BT;com.android.networkstack.tethering:drawable/stat_sys_tether_general</item>
-    </string-array>
-    <!-- String for tether enable notification title. -->
-    <string name="tethering_notification_title">@string/tethered_notification_title</string>
-    <!-- String for tether enable notification message. -->
-    <string name="tethering_notification_message">@string/tethered_notification_message</string>
-
     <!-- No upstream notification is shown when there is a downstream but no upstream that is able
          to do the tethering. -->
     <!-- Delay(millisecond) to show no upstream notification after there's no Backhaul. Set delay to
diff --git a/packages/Tethering/res/values/overlayable.xml b/packages/Tethering/res/values/overlayable.xml
index bbba3f3..16ae8ad 100644
--- a/packages/Tethering/res/values/overlayable.xml
+++ b/packages/Tethering/res/values/overlayable.xml
@@ -32,44 +32,6 @@
             <item type="string" name="config_mobile_hotspot_provision_response"/>
             <item type="integer" name="config_mobile_hotspot_provision_check_period"/>
             <item type="string" name="config_wifi_tether_enable"/>
-            <!-- Configuration values for TetheringNotificationUpdater -->
-            <!-- Icons for showing tether enable notification.
-            Each item should have two elements and be separated with ";".
-
-            The first element is downstream types which is one of tethering. This element has to be
-            made by WIFI, USB, BT, and OR'd with the others. Use "|" to combine multiple downstream
-            types and use "," to separate each combinations. Such as
-
-            USB|BT,WIFI|USB|BT
-
-            The second element is icon for the item. This element has to be composed by
-            <package name>:drawable/<resource name>. Such as
-
-            1. com.android.networkstack.tethering:drawable/stat_sys_tether_general
-            2. android:drawable/xxx
-
-            So the entire string of each item would be
-
-            USB|BT,WIFI|USB|BT;com.android.networkstack.tethering:drawable/stat_sys_tether_general
-
-            NOTE: One config can be separated into two or more for readability. Such as
-
-                  WIFI|USB,WIFI|BT,USB|BT,WIFI|USB|BT;android:drawable/xxx
-
-                  can be separated into
-
-                  WIFI|USB;android:drawable/xxx
-                  WIFI|BT;android:drawable/xxx
-                  USB|BT;android:drawable/xxx
-                  WIFI|USB|BT;android:drawable/xxx
-
-            Notification will not show if the downstream type isn't listed in array.
-            Empty array means disable notifications. -->
-            <item type="array" name="tethering_notification_icons"/>
-            <!-- String for tether enable notification title. -->
-            <item type="string" name="tethering_notification_title"/>
-            <!-- String for tether enable notification message. -->
-            <item type="string" name="tethering_notification_message"/>
             <!-- Params from config.xml that can be overlaid -->
         </policy>
     </overlayable>
diff --git a/packages/Tethering/res/values/strings.xml b/packages/Tethering/res/values/strings.xml
index 4fa60d4..d63c7c5 100644
--- a/packages/Tethering/res/values/strings.xml
+++ b/packages/Tethering/res/values/strings.xml
@@ -19,9 +19,6 @@
     <string name="tethered_notification_title">Tethering or hotspot active</string>
     <!-- String for tethered notification message [CHAR LIMIT=200] -->
     <string name="tethered_notification_message">Tap to set up.</string>
-    <!-- String for tethered notification title with client number info. -->
-    <plurals name="tethered_notification_title_with_client_number">
-    </plurals>
 
     <!-- This notification is shown when tethering has been disabled on a user's device.
     The device is managed by the user's employer. Tethering can't be turned on unless the
@@ -47,6 +44,4 @@
     <string name="upstream_roaming_notification_title"></string>
     <!-- String for cellular roaming notification message [CHAR LIMIT=500] -->
     <string name="upstream_roaming_notification_message"></string>
-    <!-- String for cellular roaming notification continue button [CHAR LIMIT=200] -->
-    <string name="upstream_roaming_notification_continue_button"></string>
 </resources>
diff --git a/packages/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java b/packages/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
index 85a23fb..55344fc 100644
--- a/packages/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
+++ b/packages/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
@@ -142,7 +142,7 @@
     public boolean initOffloadConfig() {
         IOffloadConfig offloadConfig;
         try {
-            offloadConfig = IOffloadConfig.getService();
+            offloadConfig = IOffloadConfig.getService(true /*retry*/);
         } catch (RemoteException e) {
             mLog.e("getIOffloadConfig error " + e);
             return false;
diff --git a/packages/Tethering/src/com/android/networkstack/tethering/TetheringNotificationUpdater.java b/packages/Tethering/src/com/android/networkstack/tethering/TetheringNotificationUpdater.java
index f490cc4..7fd6b61 100644
--- a/packages/Tethering/src/com/android/networkstack/tethering/TetheringNotificationUpdater.java
+++ b/packages/Tethering/src/com/android/networkstack/tethering/TetheringNotificationUpdater.java
@@ -17,9 +17,6 @@
 package com.android.networkstack.tethering;
 
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
-import static android.net.TetheringManager.TETHERING_BLUETOOTH;
-import static android.net.TetheringManager.TETHERING_USB;
-import static android.net.TetheringManager.TETHERING_WIFI;
 import static android.text.TextUtils.isEmpty;
 
 import android.app.Notification;
@@ -39,10 +36,8 @@
 import android.provider.Settings;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
-import android.util.Log;
 import android.util.SparseArray;
 
-import androidx.annotation.ArrayRes;
 import androidx.annotation.DrawableRes;
 import androidx.annotation.IntDef;
 import androidx.annotation.IntRange;
@@ -77,9 +72,6 @@
     private static final boolean NO_NOTIFY = false;
     @VisibleForTesting
     static final int EVENT_SHOW_NO_UPSTREAM = 1;
-    // Id to update and cancel enable notification. Must be unique within the tethering app.
-    @VisibleForTesting
-    static final int ENABLE_NOTIFICATION_ID = 1000;
     // Id to update and cancel restricted notification. Must be unique within the tethering app.
     @VisibleForTesting
     static final int RESTRICTED_NOTIFICATION_ID = 1001;
@@ -120,7 +112,6 @@
 
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(value = {
-            ENABLE_NOTIFICATION_ID,
             RESTRICTED_NOTIFICATION_ID,
             NO_UPSTREAM_NOTIFICATION_ID,
             ROAMING_NOTIFICATION_ID
@@ -223,7 +214,6 @@
         final boolean tetheringActiveChanged =
                 (downstreamTypes == DOWNSTREAM_NONE) != (mDownstreamTypesMask == DOWNSTREAM_NONE);
         final boolean subIdChanged = subId != mActiveDataSubId;
-        final boolean downstreamChanged = downstreamTypes != mDownstreamTypesMask;
         final boolean upstreamChanged = noUpstream != mNoUpstream;
         final boolean roamingChanged = isRoaming != mRoaming;
         final boolean updateAll = tetheringActiveChanged || subIdChanged;
@@ -232,19 +222,10 @@
         mNoUpstream = noUpstream;
         mRoaming = isRoaming;
 
-        if (updateAll || downstreamChanged) updateEnableNotification();
         if (updateAll || upstreamChanged) updateNoUpstreamNotification();
         if (updateAll || roamingChanged) updateRoamingNotification();
     }
 
-    private void updateEnableNotification() {
-        final boolean tetheringInactive = mDownstreamTypesMask == DOWNSTREAM_NONE;
-
-        if (tetheringInactive || setupNotification() == NO_NOTIFY) {
-            clearNotification(ENABLE_NOTIFICATION_ID);
-        }
-    }
-
     private void updateNoUpstreamNotification() {
         final boolean tetheringInactive = mDownstreamTypesMask == DOWNSTREAM_NONE;
 
@@ -310,64 +291,6 @@
                 NO_UPSTREAM_NOTIFICATION_ID, null /* pendingIntent */, action);
     }
 
-    /**
-     * Returns the downstream types mask which convert from given string.
-     *
-     * @param types This string has to be made by "WIFI", "USB", "BT", and OR'd with the others.
-     *
-     * @return downstream types mask value.
-     */
-    @VisibleForTesting
-    @IntRange(from = 0, to = 7)
-    int getDownstreamTypesMask(@NonNull final String types) {
-        int downstreamTypesMask = DOWNSTREAM_NONE;
-        final String[] downstreams = types.split("\\|");
-        for (String downstream : downstreams) {
-            if (USB_DOWNSTREAM.equals(downstream.trim())) {
-                downstreamTypesMask |= (1 << TETHERING_USB);
-            } else if (WIFI_DOWNSTREAM.equals(downstream.trim())) {
-                downstreamTypesMask |= (1 << TETHERING_WIFI);
-            } else if (BLUETOOTH_DOWNSTREAM.equals(downstream.trim())) {
-                downstreamTypesMask |= (1 << TETHERING_BLUETOOTH);
-            }
-        }
-        return downstreamTypesMask;
-    }
-
-    /**
-     * Returns the icons {@link android.util.SparseArray} which get from given string-array resource
-     * id.
-     *
-     * @param id String-array resource id
-     *
-     * @return {@link android.util.SparseArray} with downstream types and icon id info.
-     */
-    @NonNull
-    @VisibleForTesting
-    SparseArray<Integer> getIcons(@ArrayRes int id, @NonNull Resources res) {
-        final String[] array = res.getStringArray(id);
-        final SparseArray<Integer> icons = new SparseArray<>();
-        for (String config : array) {
-            if (isEmpty(config)) continue;
-
-            final String[] elements = config.split(";");
-            if (elements.length != 2) {
-                Log.wtf(TAG,
-                        "Unexpected format in Tethering notification configuration : " + config);
-                continue;
-            }
-
-            final String[] types = elements[0].split(",");
-            for (String type : types) {
-                int mask = getDownstreamTypesMask(type);
-                if (mask == DOWNSTREAM_NONE) continue;
-                icons.put(mask, res.getIdentifier(
-                        elements[1].trim(), null /* defType */, null /* defPackage */));
-            }
-        }
-        return icons;
-    }
-
     private boolean setupRoamingNotification() {
         final Resources res = getResourcesForSubId(mContext, mActiveDataSubId);
         final boolean upstreamRoamingNotification =
@@ -403,29 +326,6 @@
         return NOTIFY_DONE;
     }
 
-    private boolean setupNotification() {
-        final Resources res = getResourcesForSubId(mContext, mActiveDataSubId);
-        final SparseArray<Integer> downstreamIcons =
-                getIcons(R.array.tethering_notification_icons, res);
-
-        final int iconId = downstreamIcons.get(mDownstreamTypesMask, NO_ICON_ID);
-        if (iconId == NO_ICON_ID) return NO_NOTIFY;
-
-        final String title = res.getString(R.string.tethering_notification_title);
-        final String message = res.getString(R.string.tethering_notification_message);
-        if (isEmpty(title) || isEmpty(message)) return NO_NOTIFY;
-
-        final PendingIntent pi = PendingIntent.getActivity(
-                mContext.createContextAsUser(UserHandle.CURRENT, 0 /* flags */),
-                0 /* requestCode */,
-                new Intent(Settings.ACTION_TETHER_SETTINGS),
-                Intent.FLAG_ACTIVITY_NEW_TASK,
-                null /* options */);
-
-        showNotification(iconId, title, message, ENABLE_NOTIFICATION_ID, pi, new Action[0]);
-        return NOTIFY_DONE;
-    }
-
     private void showNotification(@DrawableRes final int iconId, @NonNull final String title,
             @NonNull final String message, @NotificationId final int id, @Nullable PendingIntent pi,
             @NonNull final Action... actions) {
diff --git a/packages/Tethering/tests/integration/Android.bp b/packages/Tethering/tests/integration/Android.bp
index 6b751af..3305ed0 100644
--- a/packages/Tethering/tests/integration/Android.bp
+++ b/packages/Tethering/tests/integration/Android.bp
@@ -69,6 +69,7 @@
     test_config: "AndroidTest_Coverage.xml",
     defaults: ["libnetworkstackutilsjni_deps"],
     static_libs: [
+        "NetworkStaticLibTestsLib",
         "NetworkStackTestsLib",
         "TetheringTestsLib",
         "TetheringIntegrationTestsLib",
diff --git a/packages/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringNotificationUpdaterTest.kt b/packages/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringNotificationUpdaterTest.kt
index 04f31a7..745468f 100644
--- a/packages/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringNotificationUpdaterTest.kt
+++ b/packages/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringNotificationUpdaterTest.kt
@@ -20,8 +20,6 @@
 import android.app.NotificationManager
 import android.content.Context
 import android.content.res.Resources
-import android.net.ConnectivityManager.TETHERING_BLUETOOTH
-import android.net.ConnectivityManager.TETHERING_USB
 import android.net.ConnectivityManager.TETHERING_WIFI
 import android.os.Handler
 import android.os.HandlerThread
@@ -29,14 +27,12 @@
 import android.net.NetworkCapabilities
 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
 import android.os.UserHandle
-import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
 import android.telephony.TelephonyManager
 import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.runner.AndroidJUnit4
 import com.android.internal.util.test.BroadcastInterceptingContext
 import com.android.networkstack.tethering.TetheringNotificationUpdater.DOWNSTREAM_NONE
-import com.android.networkstack.tethering.TetheringNotificationUpdater.ENABLE_NOTIFICATION_ID
 import com.android.networkstack.tethering.TetheringNotificationUpdater.EVENT_SHOW_NO_UPSTREAM
 import com.android.networkstack.tethering.TetheringNotificationUpdater.NO_UPSTREAM_NOTIFICATION_ID
 import com.android.networkstack.tethering.TetheringNotificationUpdater.RESTRICTED_NOTIFICATION_ID
@@ -63,17 +59,9 @@
 import org.mockito.MockitoAnnotations
 
 const val TEST_SUBID = 1
-const val WIFI_ICON_ID = 1
-const val USB_ICON_ID = 2
-const val BT_ICON_ID = 3
-const val GENERAL_ICON_ID = 4
 const val WIFI_MASK = 1 shl TETHERING_WIFI
-const val USB_MASK = 1 shl TETHERING_USB
-const val BT_MASK = 1 shl TETHERING_BLUETOOTH
-const val TITLE = "Tethering active"
-const val MESSAGE = "Tap here to set up."
-const val TEST_TITLE = "Hotspot active"
-const val TEST_MESSAGE = "Tap to set up hotspot."
+const val TEST_DISALLOW_TITLE = "Tether function is disallowed"
+const val TEST_DISALLOW_MESSAGE = "Please contact your admin"
 const val TEST_NO_UPSTREAM_TITLE = "Hotspot has no internet access"
 const val TEST_NO_UPSTREAM_MESSAGE = "Device cannot connect to internet."
 const val TEST_NO_UPSTREAM_BUTTON = "Turn off hotspot"
@@ -88,7 +76,6 @@
     @Mock private lateinit var mockContext: Context
     @Mock private lateinit var notificationManager: NotificationManager
     @Mock private lateinit var telephonyManager: TelephonyManager
-    @Mock private lateinit var defaultResources: Resources
     @Mock private lateinit var testResources: Resources
 
     // lateinit for these classes under test, as they should be reset to a different instance for
@@ -97,11 +84,6 @@
     private lateinit var notificationUpdater: TetheringNotificationUpdater
     private lateinit var fakeTetheringThread: HandlerThread
 
-    private val ENABLE_ICON_CONFIGS = arrayOf(
-            "USB;android.test:drawable/usb", "BT;android.test:drawable/bluetooth",
-            "WIFI|BT;android.test:drawable/general", "WIFI|USB;android.test:drawable/general",
-            "USB|BT;android.test:drawable/general", "WIFI|USB|BT;android.test:drawable/general")
-
     private val ROAMING_CAPABILITIES = NetworkCapabilities()
     private val HOME_CAPABILITIES = NetworkCapabilities().addCapability(NET_CAPABILITY_NOT_ROAMING)
     private val NOTIFICATION_ICON_ID = R.drawable.stat_sys_tether_general
@@ -117,29 +99,19 @@
 
     private inner class WrappedNotificationUpdater(c: Context, looper: Looper)
         : TetheringNotificationUpdater(c, looper) {
-        override fun getResourcesForSubId(context: Context, subId: Int) =
-                when (subId) {
-                    TEST_SUBID -> testResources
-                    INVALID_SUBSCRIPTION_ID -> defaultResources
-                    else -> super.getResourcesForSubId(context, subId)
-                }
+        override fun getResourcesForSubId(c: Context, subId: Int) =
+                if (subId == TEST_SUBID) testResources else super.getResourcesForSubId(c, subId)
     }
 
     private fun setupResources() {
-        doReturn(ENABLE_ICON_CONFIGS).`when`(defaultResources)
-                .getStringArray(R.array.tethering_notification_icons)
-        doReturn(arrayOf("WIFI;android.test:drawable/wifi")).`when`(testResources)
-                .getStringArray(R.array.tethering_notification_icons)
         doReturn(5).`when`(testResources)
                 .getInteger(R.integer.delay_to_show_no_upstream_after_no_backhaul)
         doReturn(true).`when`(testResources)
                 .getBoolean(R.bool.config_upstream_roaming_notification)
-        doReturn(TITLE).`when`(defaultResources).getString(R.string.tethering_notification_title)
-        doReturn(MESSAGE).`when`(defaultResources)
-                .getString(R.string.tethering_notification_message)
-        doReturn(TEST_TITLE).`when`(testResources).getString(R.string.tethering_notification_title)
-        doReturn(TEST_MESSAGE).`when`(testResources)
-                .getString(R.string.tethering_notification_message)
+        doReturn(TEST_DISALLOW_TITLE).`when`(testResources)
+                .getString(R.string.disable_tether_notification_title)
+        doReturn(TEST_DISALLOW_MESSAGE).`when`(testResources)
+                .getString(R.string.disable_tether_notification_message)
         doReturn(TEST_NO_UPSTREAM_TITLE).`when`(testResources)
                 .getString(R.string.no_upstream_notification_title)
         doReturn(TEST_NO_UPSTREAM_MESSAGE).`when`(testResources)
@@ -150,14 +122,6 @@
                 .getString(R.string.upstream_roaming_notification_title)
         doReturn(TEST_ROAMING_MESSAGE).`when`(testResources)
                 .getString(R.string.upstream_roaming_notification_message)
-        doReturn(USB_ICON_ID).`when`(defaultResources)
-                .getIdentifier(eq("android.test:drawable/usb"), any(), any())
-        doReturn(BT_ICON_ID).`when`(defaultResources)
-                .getIdentifier(eq("android.test:drawable/bluetooth"), any(), any())
-        doReturn(GENERAL_ICON_ID).`when`(defaultResources)
-                .getIdentifier(eq("android.test:drawable/general"), any(), any())
-        doReturn(WIFI_ICON_ID).`when`(testResources)
-                .getIdentifier(eq("android.test:drawable/wifi"), any(), any())
     }
 
     @Before
@@ -206,119 +170,27 @@
     }
 
     @Test
-    fun testNotificationWithDownstreamChanged() {
-        // Wifi downstream. No notification.
-        notificationUpdater.onDownstreamChanged(WIFI_MASK)
-        verifyNotificationCancelled(listOf(ENABLE_NOTIFICATION_ID))
-
-        // Same downstream changed. Nothing happened.
-        notificationUpdater.onDownstreamChanged(WIFI_MASK)
-        verifyZeroInteractions(notificationManager)
-
-        // Wifi and usb downstreams. Show enable notification
-        notificationUpdater.onDownstreamChanged(WIFI_MASK or USB_MASK)
-        verifyNotification(GENERAL_ICON_ID, TITLE, MESSAGE, ENABLE_NOTIFICATION_ID)
-
-        // Usb downstream. Still show enable notification.
-        notificationUpdater.onDownstreamChanged(USB_MASK)
-        verifyNotification(USB_ICON_ID, TITLE, MESSAGE, ENABLE_NOTIFICATION_ID)
-
-        // No downstream. No notification.
-        notificationUpdater.onDownstreamChanged(DOWNSTREAM_NONE)
-        verifyNotificationCancelled(listOf(ENABLE_NOTIFICATION_ID, NO_UPSTREAM_NOTIFICATION_ID,
-                ROAMING_NOTIFICATION_ID))
-    }
-
-    @Test
-    fun testNotificationWithActiveDataSubscriptionIdChanged() {
-        // Usb downstream. Showed enable notification with default resource.
-        notificationUpdater.onDownstreamChanged(USB_MASK)
-        verifyNotification(USB_ICON_ID, TITLE, MESSAGE, ENABLE_NOTIFICATION_ID)
-
-        // Same subId changed. Nothing happened.
-        notificationUpdater.onActiveDataSubscriptionIdChanged(INVALID_SUBSCRIPTION_ID)
-        verifyZeroInteractions(notificationManager)
-
-        // Set test sub id. Clear notification with test resource.
+    fun testRestrictedNotification() {
+        // Set test sub id.
         notificationUpdater.onActiveDataSubscriptionIdChanged(TEST_SUBID)
-        verifyNotificationCancelled(listOf(ENABLE_NOTIFICATION_ID, NO_UPSTREAM_NOTIFICATION_ID,
-                ROAMING_NOTIFICATION_ID))
-
-        // Wifi downstream. Show enable notification with test resource.
-        notificationUpdater.onDownstreamChanged(WIFI_MASK)
-        verifyNotification(WIFI_ICON_ID, TEST_TITLE, TEST_MESSAGE, ENABLE_NOTIFICATION_ID)
-
-        // No downstream. No notification.
-        notificationUpdater.onDownstreamChanged(DOWNSTREAM_NONE)
-        verifyNotificationCancelled(listOf(ENABLE_NOTIFICATION_ID, NO_UPSTREAM_NOTIFICATION_ID,
-                ROAMING_NOTIFICATION_ID))
-    }
-
-    private fun assertIconNumbers(number: Int, configs: Array<String?>) {
-        doReturn(configs).`when`(defaultResources)
-                .getStringArray(R.array.tethering_notification_icons)
-        assertEquals(number, notificationUpdater.getIcons(
-                R.array.tethering_notification_icons, defaultResources).size())
-    }
-
-    @Test
-    fun testGetIcons() {
-        assertIconNumbers(0, arrayOfNulls<String>(0))
-        assertIconNumbers(0, arrayOf(null, ""))
-        assertIconNumbers(3, arrayOf(
-                // These configurations are invalid with wrong strings or symbols.
-                ";", ",", "|", "|,;", "WIFI", "1;2", " U SB; ", "bt;", "WIFI;USB;BT", "WIFI|USB|BT",
-                "WIFI,BT,USB", " WIFI| |  | USB, test:drawable/test",
-                // This configuration is valid with two downstream types (USB, BT).
-                "USB|,,,,,|BT;drawable/test ",
-                // This configuration is valid with one downstream types (WIFI).
-                "     WIFI     ; android.test:drawable/xxx "))
-    }
-
-    @Test
-    fun testGetDownstreamTypesMask() {
-        assertEquals(DOWNSTREAM_NONE, notificationUpdater.getDownstreamTypesMask(""))
-        assertEquals(DOWNSTREAM_NONE, notificationUpdater.getDownstreamTypesMask("1"))
-        assertEquals(DOWNSTREAM_NONE, notificationUpdater.getDownstreamTypesMask("WIFI_P2P"))
-        assertEquals(DOWNSTREAM_NONE, notificationUpdater.getDownstreamTypesMask("usb"))
-        assertEquals(WIFI_MASK, notificationUpdater.getDownstreamTypesMask(" WIFI "))
-        assertEquals(USB_MASK, notificationUpdater.getDownstreamTypesMask("USB | B T"))
-        assertEquals(BT_MASK, notificationUpdater.getDownstreamTypesMask(" WIFI: | BT"))
-        assertEquals(WIFI_MASK or USB_MASK,
-                notificationUpdater.getDownstreamTypesMask("1|2|USB|WIFI|BLUETOOTH||"))
-    }
-
-    @Test
-    fun testSetupRestrictedNotification() {
-        val title = context.resources.getString(R.string.disable_tether_notification_title)
-        val message = context.resources.getString(R.string.disable_tether_notification_message)
-        val disallowTitle = "Tether function is disallowed"
-        val disallowMessage = "Please contact your admin"
-        doReturn(title).`when`(defaultResources)
-                .getString(R.string.disable_tether_notification_title)
-        doReturn(message).`when`(defaultResources)
-                .getString(R.string.disable_tether_notification_message)
-        doReturn(disallowTitle).`when`(testResources)
-                .getString(R.string.disable_tether_notification_title)
-        doReturn(disallowMessage).`when`(testResources)
-                .getString(R.string.disable_tether_notification_message)
+        verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID, ROAMING_NOTIFICATION_ID))
 
         // User restrictions on. Show restricted notification.
         notificationUpdater.notifyTetheringDisabledByRestriction()
-        verifyNotification(NOTIFICATION_ICON_ID, title, message, RESTRICTED_NOTIFICATION_ID)
+        verifyNotification(NOTIFICATION_ICON_ID, TEST_DISALLOW_TITLE, TEST_DISALLOW_MESSAGE,
+                RESTRICTED_NOTIFICATION_ID)
 
         // User restrictions off. Clear notification.
         notificationUpdater.tetheringRestrictionLifted()
         verifyNotificationCancelled(listOf(RESTRICTED_NOTIFICATION_ID))
 
-        // Set test sub id. No notification.
-        notificationUpdater.onActiveDataSubscriptionIdChanged(TEST_SUBID)
-        verifyNotificationCancelled(listOf(ENABLE_NOTIFICATION_ID, NO_UPSTREAM_NOTIFICATION_ID,
-                ROAMING_NOTIFICATION_ID))
+        // No downstream.
+        notificationUpdater.onDownstreamChanged(DOWNSTREAM_NONE)
+        verifyZeroInteractions(notificationManager)
 
-        // User restrictions on again. Show restricted notification with test resource.
+        // User restrictions on again. Show restricted notification.
         notificationUpdater.notifyTetheringDisabledByRestriction()
-        verifyNotification(NOTIFICATION_ICON_ID, disallowTitle, disallowMessage,
+        verifyNotification(NOTIFICATION_ICON_ID, TEST_DISALLOW_TITLE, TEST_DISALLOW_MESSAGE,
                 RESTRICTED_NOTIFICATION_ID)
     }
 
@@ -356,15 +228,14 @@
     }
 
     @Test
-    fun testNotificationWithUpstreamCapabilitiesChanged_NoUpstream() {
-        // Set test sub id. No notification.
+    fun testNoUpstreamNotification() {
+        // Set test sub id.
         notificationUpdater.onActiveDataSubscriptionIdChanged(TEST_SUBID)
-        verifyNotificationCancelled(listOf(ENABLE_NOTIFICATION_ID, NO_UPSTREAM_NOTIFICATION_ID,
-                ROAMING_NOTIFICATION_ID))
+        verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID, ROAMING_NOTIFICATION_ID))
 
-        // Wifi downstream. Show enable notification with test resource.
+        // Wifi downstream.
         notificationUpdater.onDownstreamChanged(WIFI_MASK)
-        verifyNotification(WIFI_ICON_ID, TEST_TITLE, TEST_MESSAGE, ENABLE_NOTIFICATION_ID)
+        verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID, ROAMING_NOTIFICATION_ID))
 
         // There is no upstream. Show no upstream notification.
         notificationUpdater.onUpstreamCapabilitiesChanged(null)
@@ -386,15 +257,14 @@
         verifyNotification(NOTIFICATION_ICON_ID, TEST_NO_UPSTREAM_TITLE, TEST_NO_UPSTREAM_MESSAGE,
                 NO_UPSTREAM_NOTIFICATION_ID)
 
-        // No downstream. No notification.
+        // No downstream.
         notificationUpdater.onDownstreamChanged(DOWNSTREAM_NONE)
-        verifyNotificationCancelled(listOf(ENABLE_NOTIFICATION_ID, NO_UPSTREAM_NOTIFICATION_ID,
-                ROAMING_NOTIFICATION_ID))
+        verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID, ROAMING_NOTIFICATION_ID))
 
-        // Put up enable notification with wifi downstream and home capabilities.
+        // Wifi downstream and home capabilities.
         notificationUpdater.onDownstreamChanged(WIFI_MASK)
         notificationUpdater.onUpstreamCapabilitiesChanged(HOME_CAPABILITIES)
-        verifyNotification(WIFI_ICON_ID, TEST_TITLE, TEST_MESSAGE, ENABLE_NOTIFICATION_ID)
+        verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID, ROAMING_NOTIFICATION_ID))
 
         // Set R.integer.delay_to_show_no_upstream_after_no_backhaul to -1 and change to no upstream
         // again. Don't put up no upstream notification.
@@ -429,15 +299,14 @@
     }
 
     @Test
-    fun testNotificationWithUpstreamCapabilitiesChanged_Roaming() {
-        // Set test sub id. Clear notification.
+    fun testRoamingNotification() {
+        // Set test sub id.
         notificationUpdater.onActiveDataSubscriptionIdChanged(TEST_SUBID)
-        verifyNotificationCancelled(listOf(ENABLE_NOTIFICATION_ID, NO_UPSTREAM_NOTIFICATION_ID,
-                ROAMING_NOTIFICATION_ID))
+        verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID, ROAMING_NOTIFICATION_ID))
 
-        // Wifi downstream. Show enable notification with test resource.
+        // Wifi downstream.
         notificationUpdater.onDownstreamChanged(WIFI_MASK)
-        verifyNotification(WIFI_ICON_ID, TEST_TITLE, TEST_MESSAGE, ENABLE_NOTIFICATION_ID)
+        verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID, ROAMING_NOTIFICATION_ID))
 
         // Upstream capabilities changed to roaming state. Show roaming notification.
         notificationUpdater.onUpstreamCapabilitiesChanged(ROAMING_CAPABILITIES)
@@ -464,14 +333,16 @@
         verifyNotification(NOTIFICATION_ICON_ID, TEST_NO_UPSTREAM_TITLE, TEST_NO_UPSTREAM_MESSAGE,
                 NO_UPSTREAM_NOTIFICATION_ID)
 
-        // No downstream. No notification.
+        // No downstream.
         notificationUpdater.onDownstreamChanged(DOWNSTREAM_NONE)
-        verifyNotificationCancelled(listOf(ENABLE_NOTIFICATION_ID, NO_UPSTREAM_NOTIFICATION_ID,
-                ROAMING_NOTIFICATION_ID))
+        verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID, ROAMING_NOTIFICATION_ID))
 
-        // Wifi downstream again. Show enable notification with test resource.
+        // Wifi downstream again.
         notificationUpdater.onDownstreamChanged(WIFI_MASK)
-        verifyNotification(WIFI_ICON_ID, TEST_TITLE, TEST_MESSAGE, ENABLE_NOTIFICATION_ID)
+        notificationUpdater.handler.waitForDelayedMessage(EVENT_SHOW_NO_UPSTREAM, TIMEOUT_MS)
+        verifyNotificationCancelled(listOf(ROAMING_NOTIFICATION_ID), false)
+        verifyNotification(NOTIFICATION_ICON_ID, TEST_NO_UPSTREAM_TITLE, TEST_NO_UPSTREAM_MESSAGE,
+                NO_UPSTREAM_NOTIFICATION_ID)
 
         // Set R.bool.config_upstream_roaming_notification to false and change upstream
         // network to roaming state again. No roaming notification.
diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
index 132b692..da9bdf3 100644
--- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
@@ -1023,8 +1023,7 @@
         try {
             mMainHandler.post(PooledLambda.obtainRunnable((nonArg) -> {
                 final ScreenshotGraphicBuffer screenshotBuffer = LocalServices
-                        .getService(DisplayManagerInternal.class)
-                        .screenshotWithoutSecureLayer(displayId);
+                        .getService(DisplayManagerInternal.class).userScreenshot(displayId);
                 if (screenshotBuffer != null) {
                     sendScreenshotSuccess(screenshotBuffer, callback);
                 } else {
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index f21f0e7..1a72cf0 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -2732,7 +2732,7 @@
     public void onShellCommand(FileDescriptor in, FileDescriptor out,
             FileDescriptor err, String[] args, ShellCallback callback,
             ResultReceiver resultReceiver) {
-        new AccessibilityShellCommand(this).exec(this, in, out, err, args,
+        new AccessibilityShellCommand(this, mSystemActionPerformer).exec(this, in, out, err, args,
                 callback, resultReceiver);
     }
 
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityShellCommand.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityShellCommand.java
index 20a11bd..b36626f 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityShellCommand.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityShellCommand.java
@@ -18,6 +18,8 @@
 
 import android.annotation.NonNull;
 import android.app.ActivityManager;
+import android.os.Binder;
+import android.os.Process;
 import android.os.ShellCommand;
 import android.os.UserHandle;
 
@@ -28,9 +30,12 @@
  */
 final class AccessibilityShellCommand extends ShellCommand {
     final @NonNull AccessibilityManagerService mService;
+    final @NonNull SystemActionPerformer mSystemActionPerformer;
 
-    AccessibilityShellCommand(@NonNull AccessibilityManagerService service) {
+    AccessibilityShellCommand(@NonNull AccessibilityManagerService service,
+            @NonNull SystemActionPerformer systemActionPerformer) {
         mService = service;
+        mSystemActionPerformer = systemActionPerformer;
     }
 
     @Override
@@ -45,6 +50,9 @@
             case "set-bind-instant-service-allowed": {
                 return runSetBindInstantServiceAllowed();
             }
+            case "call-system-action": {
+                return runCallSystemAction();
+            }
         }
         return -1;
     }
@@ -74,6 +82,22 @@
         return 0;
     }
 
+    private int runCallSystemAction() {
+        final int callingUid = Binder.getCallingUid();
+        if (callingUid != Process.ROOT_UID
+                && callingUid != Process.SYSTEM_UID
+                && callingUid != Process.SHELL_UID) {
+            return -1;
+        }
+        final String option = getNextArg();
+        if (option != null) {
+            int actionId = Integer.parseInt(option);
+            mSystemActionPerformer.performSystemAction(actionId);
+            return 0;
+        }
+        return -1;
+    }
+
     private Integer parseUserId() {
         final String option = getNextOption();
         if (option != null) {
@@ -97,5 +121,7 @@
         pw.println("    Set whether binding to services provided by instant apps is allowed.");
         pw.println("  get-bind-instant-service-allowed [--user <USER_ID>]");
         pw.println("    Get whether binding to services provided by instant apps is allowed.");
+        pw.println("  call-system-action <ACTION_ID>");
+        pw.println("    Calls the system action with the given action id.");
     }
 }
\ No newline at end of file
diff --git a/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java b/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java
index ef8d524..a1fc3fa 100644
--- a/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java
+++ b/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java
@@ -16,6 +16,8 @@
 
 package com.android.server.accessibility;
 
+import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_ACCESSIBILITY_ACTIONS;
+
 import android.accessibilityservice.AccessibilityService;
 import android.app.PendingIntent;
 import android.app.RemoteAction;
@@ -301,7 +303,11 @@
                     return lockScreen();
                 case AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT:
                     return takeScreenshot();
+                case AccessibilityService.GLOBAL_ACTION_KEYCODE_HEADSETHOOK :
+                    sendDownAndUpKeyEvents(KeyEvent.KEYCODE_HEADSETHOOK);
+                    return true;
                 default:
+                    Slog.e(TAG, "Invalid action id: " + actionId);
                     return false;
             }
         } finally {
@@ -395,7 +401,8 @@
         ScreenshotHelper screenshotHelper = (mScreenshotHelperSupplier != null)
                 ? mScreenshotHelperSupplier.get() : new ScreenshotHelper(mContext);
         screenshotHelper.takeScreenshot(android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN,
-                true, true, new Handler(Looper.getMainLooper()), null);
+                true, true, SCREENSHOT_ACCESSIBILITY_ACTIONS,
+                new Handler(Looper.getMainLooper()), null);
         return true;
     }
 }
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 9d1ad42..35089d6 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -117,6 +117,7 @@
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Consumer;
+import java.util.function.Function;
 
 /**
  * A session for a given activity.
@@ -717,10 +718,11 @@
             Consumer<InlineSuggestionsRequest> inlineSuggestionsRequestConsumer =
                     mAssistReceiver.newAutofillRequestLocked(/*isInlineRequest=*/ true);
             if (inlineSuggestionsRequestConsumer != null) {
+                final AutofillId focusedId = mCurrentViewId;
                 remoteRenderService.getInlineSuggestionsRendererInfo(
                         new RemoteCallback((extras) -> {
                             mInlineSessionController.onCreateInlineSuggestionsRequestLocked(
-                                    mCurrentViewId, inlineSuggestionsRequestConsumer, extras);
+                                    focusedId, inlineSuggestionsRequestConsumer, extras);
                         }
                 ));
             }
@@ -2786,6 +2788,12 @@
      */
     private boolean requestShowInlineSuggestionsLocked(@NonNull FillResponse response,
             @Nullable String filterText) {
+        if (mCurrentViewId == null) {
+            Log.w(TAG, "requestShowInlineSuggestionsLocked(): no view currently focused");
+            return false;
+        }
+        final AutofillId focusedId = mCurrentViewId;
+
         final Optional<InlineSuggestionsRequest> inlineSuggestionsRequest =
                 mInlineSessionController.getInlineSuggestionsRequestLocked();
         if (!inlineSuggestionsRequest.isPresent()) {
@@ -2800,17 +2808,17 @@
             return false;
         }
 
-        final ViewState currentView = mViewStates.get(mCurrentViewId);
+        final ViewState currentView = mViewStates.get(focusedId);
         if ((currentView.getState() & ViewState.STATE_INLINE_DISABLED) != 0) {
             response.getDatasets().clear();
         }
         InlineSuggestionsResponse inlineSuggestionsResponse =
                 InlineSuggestionFactory.createInlineSuggestionsResponse(
-                        inlineSuggestionsRequest.get(), response, filterText, mCurrentViewId,
+                        inlineSuggestionsRequest.get(), response, filterText, focusedId,
                         this, () -> {
                             synchronized (mLock) {
                                 mInlineSessionController.hideInlineSuggestionsUiLocked(
-                                        mCurrentViewId);
+                                        focusedId);
                             }
                         }, remoteRenderService);
         if (inlineSuggestionsResponse == null) {
@@ -2818,7 +2826,7 @@
             return false;
         }
 
-        return mInlineSessionController.onInlineSuggestionsResponseLocked(mCurrentViewId,
+        return mInlineSessionController.onInlineSuggestionsResponseLocked(focusedId,
                 inlineSuggestionsResponse);
     }
 
@@ -3072,19 +3080,13 @@
         final boolean isWhitelisted = mService
                 .isWhitelistedForAugmentedAutofillLocked(mComponentName);
 
-        final String historyItem =
-                "aug:id=" + id + " u=" + uid + " m=" + mode
-                + " a=" + ComponentName.flattenToShortString(mComponentName)
-                + " f=" + mCurrentViewId
-                + " s=" + remoteService.getComponentName()
-                + " w=" + isWhitelisted;
-        mService.getMaster().logRequestLocked(historyItem);
-
         if (!isWhitelisted) {
             if (sVerbose) {
                 Slog.v(TAG, "triggerAugmentedAutofillLocked(): "
                         + ComponentName.flattenToShortString(mComponentName) + " not whitelisted ");
             }
+            logAugmentedAutofillRequestLocked(mode, remoteService.getComponentName(),
+                    mCurrentViewId, isWhitelisted, /*isInline*/null);
             return null;
         }
 
@@ -3107,26 +3109,29 @@
                 remoteService.getComponentName().getPackageName());
         mAugmentedRequestsLogs.add(log);
 
-        final AutofillId focusedId = AutofillId.withoutSession(mCurrentViewId);
+        final AutofillId focusedId = mCurrentViewId;
 
+        final Function<InlineSuggestionsResponse, Boolean> inlineSuggestionsResponseCallback =
+                response -> {
+                    synchronized (mLock) {
+                        return mInlineSessionController.onInlineSuggestionsResponseLocked(
+                                focusedId, response);
+                    }
+                };
         final Consumer<InlineSuggestionsRequest> requestAugmentedAutofill =
                 (inlineSuggestionsRequest) -> {
-                    remoteService.onRequestAutofillLocked(id, mClient, taskId, mComponentName,
-                            focusedId,
-                            currentValue, inlineSuggestionsRequest,
-                            /*inlineSuggestionsCallback=*/
-                            response -> {
-                                synchronized (mLock) {
-                                    return mInlineSessionController
-                                            .onInlineSuggestionsResponseLocked(
-                                            mCurrentViewId, response);
-                                }
-                            },
-                            /*onErrorCallback=*/ () -> {
-                                synchronized (mLock) {
-                                    cancelAugmentedAutofillLocked();
-                                }
-                            }, mService.getRemoteInlineSuggestionRenderServiceLocked());
+                    synchronized (mLock) {
+                        logAugmentedAutofillRequestLocked(mode, remoteService.getComponentName(),
+                                focusedId, isWhitelisted, inlineSuggestionsRequest != null);
+                        remoteService.onRequestAutofillLocked(id, mClient, taskId, mComponentName,
+                                AutofillId.withoutSession(focusedId), currentValue,
+                                inlineSuggestionsRequest, inlineSuggestionsResponseCallback,
+                                /*onErrorCallback=*/ () -> {
+                                    synchronized (mLock) {
+                                        cancelAugmentedAutofillLocked();
+                                    }
+                                }, mService.getRemoteInlineSuggestionRenderServiceLocked());
+                    }
                 };
 
         // When the inline suggestion render service is available, there are 2 cases when
@@ -3143,9 +3148,11 @@
             if (sDebug) Slog.d(TAG, "Create inline request for augmented autofill");
             remoteRenderService.getInlineSuggestionsRendererInfo(new RemoteCallback(
                     (extras) -> {
-                        mInlineSessionController.onCreateInlineSuggestionsRequestLocked(
-                                mCurrentViewId, /*requestConsumer=*/ requestAugmentedAutofill,
-                                extras);
+                        synchronized (mLock) {
+                            mInlineSessionController.onCreateInlineSuggestionsRequestLocked(
+                                    focusedId, /*requestConsumer=*/ requestAugmentedAutofill,
+                                    extras);
+                        }
                     }, mHandler));
         } else {
             requestAugmentedAutofill.accept(
@@ -3158,6 +3165,20 @@
     }
 
     @GuardedBy("mLock")
+    private void logAugmentedAutofillRequestLocked(int mode,
+            ComponentName augmentedRemoteServiceName, AutofillId focusedId, boolean isWhitelisted,
+            Boolean isInline) {
+        final String historyItem =
+                "aug:id=" + id + " u=" + uid + " m=" + mode
+                        + " a=" + ComponentName.flattenToShortString(mComponentName)
+                        + " f=" + focusedId
+                        + " s=" + augmentedRemoteServiceName
+                        + " w=" + isWhitelisted
+                        + " i=" + isInline;
+        mService.getMaster().logRequestLocked(historyItem);
+    }
+
+    @GuardedBy("mLock")
     private void cancelAugmentedAutofillLocked() {
         final RemoteAugmentedAutofillService remoteService = mService
                 .getRemoteAugmentedAutofillServiceLocked();
diff --git a/services/core/java/android/content/pm/PackageManagerInternal.java b/services/core/java/android/content/pm/PackageManagerInternal.java
index c27ec66..81de29c 100644
--- a/services/core/java/android/content/pm/PackageManagerInternal.java
+++ b/services/core/java/android/content/pm/PackageManagerInternal.java
@@ -480,6 +480,12 @@
     public abstract void pruneInstantApps();
 
     /**
+     * Prunes the cache of the APKs in the given APEXes.
+     * @param apexPackages The list of APEX packages that may contain APK-in-APEX.
+     */
+    public abstract void pruneCachedApksInApex(@NonNull List<PackageInfo> apexPackages);
+
+    /**
      * @return The SetupWizard package name.
      */
     public abstract String getSetupWizardPackageName();
@@ -977,4 +983,9 @@
      * Returns if a package name is a valid system package.
      */
     public abstract boolean isSystemPackage(@NonNull String packageName);
+
+    /**
+     * Unblocks uninstall for all packages for the user.
+     */
+    public abstract void clearBlockUninstallForUser(@UserIdInt int userId);
 }
diff --git a/services/core/java/com/android/server/AlarmManagerService.java b/services/core/java/com/android/server/AlarmManagerService.java
index ce65110..4009caf 100644
--- a/services/core/java/com/android/server/AlarmManagerService.java
+++ b/services/core/java/com/android/server/AlarmManagerService.java
@@ -603,8 +603,7 @@
             }
 
             pw.print(KEY_APP_STANDBY_RESTRICTED_QUOTA); pw.print("=");
-            TimeUtils.formatDuration(APP_STANDBY_RESTRICTED_QUOTA, pw);
-            pw.println();
+            pw.println(APP_STANDBY_RESTRICTED_QUOTA);
 
             pw.print(KEY_APP_STANDBY_RESTRICTED_WINDOW); pw.print("=");
             TimeUtils.formatDuration(APP_STANDBY_RESTRICTED_WINDOW, pw);
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index 2bbf278..97a5cfe 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -1146,7 +1146,7 @@
                     && registrationLimit >= 1
                     && numRecordsForPid >= registrationLimit) {
                 String errorMsg = "Pid " + callingPid + " has exceeded the number of permissible"
-                        + "registered listeners. Ignoring request to add.";
+                        + " registered listeners. Ignoring request to add.";
                 loge(errorMsg);
                 if (mConfigurationProvider
                         .isRegistrationLimitEnabledInPlatformCompat(callingUid)) {
@@ -1157,7 +1157,7 @@
                 // Log the warning independently of the dynamically set limit -- apps shouldn't be
                 // doing this regardless of whether we're throwing them an exception for it.
                 Rlog.w(TAG, "Pid " + callingPid + " has exceeded half the number of permissible"
-                        + "registered listeners. Now at " + numRecordsForPid);
+                        + " registered listeners. Now at " + numRecordsForPid);
             }
 
             r = new Record();
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 21760cd..419389f 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -119,7 +119,6 @@
 import java.io.StringWriter;
 import java.util.ArrayList;
 import java.util.Comparator;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
 import java.util.function.Predicate;
@@ -1753,8 +1752,8 @@
     private void updateServiceForegroundLocked(ProcessRecord proc, boolean oomAdj) {
         boolean anyForeground = false;
         int fgServiceTypes = 0;
-        for (int i = proc.services.size() - 1; i >= 0; i--) {
-            ServiceRecord sr = proc.services.valueAt(i);
+        for (int i = proc.numberOfRunningServices() - 1; i >= 0; i--) {
+            ServiceRecord sr = proc.getRunningServiceAt(i);
             if (sr.isForeground || sr.fgRequired) {
                 anyForeground = true;
                 fgServiceTypes |= sr.foregroundServiceType;
@@ -1765,8 +1764,8 @@
 
     private void updateWhitelistManagerLocked(ProcessRecord proc) {
         proc.whitelistManager = false;
-        for (int i=proc.services.size()-1; i>=0; i--) {
-            ServiceRecord sr = proc.services.valueAt(i);
+        for (int i = proc.numberOfRunningServices() - 1; i >= 0; i--) {
+            ServiceRecord sr = proc.getRunningServiceAt(i);
             if (sr.whitelistManager) {
                 proc.whitelistManager = true;
                 break;
@@ -1802,8 +1801,8 @@
         }
 
         boolean anyClientActivities = false;
-        for (int i=proc.services.size()-1; i>=0 && !anyClientActivities; i--) {
-            ServiceRecord sr = proc.services.valueAt(i);
+        for (int i = proc.numberOfRunningServices() - 1; i >= 0 && !anyClientActivities; i--) {
+            ServiceRecord sr = proc.getRunningServiceAt(i);
             ArrayMap<IBinder, ArrayList<ConnectionRecord>> connections = sr.getConnections();
             for (int conni = connections.size() - 1; conni >= 0 && !anyClientActivities; conni--) {
                 ArrayList<ConnectionRecord> clist = connections.valueAt(conni);
@@ -2995,7 +2994,7 @@
         r.setProcess(app);
         r.restartTime = r.lastActivity = SystemClock.uptimeMillis();
 
-        final boolean newService = app.services.add(r);
+        final boolean newService = app.startService(r);
         bumpServiceExecutingLocked(r, execInFg, "create");
         mAm.updateLruProcessLocked(app, false, null);
         updateServiceForegroundLocked(r.app, /* oomAdj= */ false);
@@ -3036,7 +3035,7 @@
 
                 // Cleanup.
                 if (newService) {
-                    app.services.remove(r);
+                    app.stopService(r);
                     r.setProcess(null);
                 }
 
@@ -3362,7 +3361,7 @@
             synchronized (r.stats.getBatteryStats()) {
                 r.stats.stopLaunchedLocked();
             }
-            r.app.services.remove(r);
+            r.app.stopService(r);
             r.app.updateBoundClientUids();
             if (r.whitelistManager) {
                 updateWhitelistManagerLocked(r.app);
@@ -3652,7 +3651,7 @@
             }
             if (finishing) {
                 if (r.app != null && !r.app.isPersistent()) {
-                    r.app.services.remove(r);
+                    r.app.stopService(r);
                     r.app.updateBoundClientUids();
                     if (r.whitelistManager) {
                         updateWhitelistManagerLocked(r.app);
@@ -3748,7 +3747,7 @@
                 didSomething = true;
                 Slog.i(TAG, "  Force stopping service " + service);
                 if (service.app != null && !service.app.isPersistent()) {
-                    service.app.services.remove(service);
+                    service.app.stopService(service);
                     service.app.updateBoundClientUids();
                     if (service.whitelistManager) {
                         updateWhitelistManagerLocked(service.app);
@@ -3861,24 +3860,22 @@
         if (false) {
             // XXX we are letting the client link to the service for
             // death notifications.
-            if (app.services.size() > 0) {
-                Iterator<ServiceRecord> it = app.services.iterator();
-                while (it.hasNext()) {
-                    ServiceRecord r = it.next();
-                    ArrayMap<IBinder, ArrayList<ConnectionRecord>> connections = r.getConnections();
-                    for (int conni=connections.size()-1; conni>=0; conni--) {
-                        ArrayList<ConnectionRecord> cl = connections.valueAt(conni);
-                        for (int i=0; i<cl.size(); i++) {
-                            ConnectionRecord c = cl.get(i);
-                            if (c.binding.client != app) {
-                                try {
-                                    //c.conn.connected(r.className, null);
-                                } catch (Exception e) {
-                                    // todo: this should be asynchronous!
-                                    Slog.w(TAG, "Exception thrown disconnected servce "
-                                          + r.shortInstanceName
-                                          + " from app " + app.processName, e);
-                                }
+            int numberOfRunningServices = app.numberOfRunningServices();
+            for (int sIndex = 0; sIndex < numberOfRunningServices; sIndex++) {
+                ServiceRecord r = app.getRunningServiceAt(sIndex);
+                ArrayMap<IBinder, ArrayList<ConnectionRecord>> connections = r.getConnections();
+                for (int conni = connections.size() - 1; conni >= 0; conni--) {
+                    ArrayList<ConnectionRecord> cl = connections.valueAt(conni);
+                    for (int i = 0; i < cl.size(); i++) {
+                        ConnectionRecord c = cl.get(i);
+                        if (c.binding.client != app) {
+                            try {
+                                //c.conn.connected(r.className, null);
+                            } catch (Exception e) {
+                                // todo: this should be asynchronous!
+                                Slog.w(TAG, "Exception thrown disconnected servce "
+                                        + r.shortInstanceName
+                                        + " from app " + app.processName, e);
                             }
                         }
                     }
@@ -3897,13 +3894,13 @@
         app.whitelistManager = false;
 
         // Clear app state from services.
-        for (int i = app.services.size() - 1; i >= 0; i--) {
-            ServiceRecord sr = app.services.valueAt(i);
+        for (int i = app.numberOfRunningServices() - 1; i >= 0; i--) {
+            ServiceRecord sr = app.getRunningServiceAt(i);
             synchronized (sr.stats.getBatteryStats()) {
                 sr.stats.stopLaunchedLocked();
             }
             if (sr.app != app && sr.app != null && !sr.app.isPersistent()) {
-                sr.app.services.remove(sr);
+                sr.app.stopService(sr);
                 sr.app.updateBoundClientUids();
             }
             sr.setProcess(null);
@@ -3962,13 +3959,13 @@
         ServiceMap smap = getServiceMapLocked(app.userId);
 
         // Now do remaining service cleanup.
-        for (int i=app.services.size()-1; i>=0; i--) {
-            ServiceRecord sr = app.services.valueAt(i);
+        for (int i = app.numberOfRunningServices() - 1; i >= 0; i--) {
+            ServiceRecord sr = app.getRunningServiceAt(i);
 
             // Unless the process is persistent, this process record is going away,
             // so make sure the service is cleaned out of it.
             if (!app.isPersistent()) {
-                app.services.removeAt(i);
+                app.stopService(sr);
                 app.updateBoundClientUids();
             }
 
@@ -4018,7 +4015,7 @@
         }
 
         if (!allowRestart) {
-            app.services.clear();
+            app.stopAllServices();
             app.clearBoundClientUids();
 
             // Make sure there are no more restarting services for this process.
@@ -4920,8 +4917,8 @@
             if (pr.uid != uid) {
                 continue;
             }
-            for (int j = pr.services.size() - 1; j >= 0; j--) {
-                ServiceRecord r = pr.services.valueAt(j);
+            for (int j = pr.numberOfRunningServices() - 1; j >= 0; j--) {
+                ServiceRecord r = pr.getRunningServiceAt(j);
                 if (!r.isForeground) {
                     continue;
                 }
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 3fdf541..9b2743c 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -18172,7 +18172,7 @@
         for (int i = mProcessList.mRemovedProcesses.size() - 1; i >= 0; i--) {
             final ProcessRecord app = mProcessList.mRemovedProcesses.get(i);
             if (!app.hasActivitiesOrRecentTasks()
-                    && app.curReceivers.isEmpty() && app.services.size() == 0) {
+                    && app.curReceivers.isEmpty() && app.numberOfRunningServices() == 0) {
                 Slog.i(
                     TAG, "Exiting empty application process "
                     + app.toShortString() + " ("
diff --git a/services/core/java/com/android/server/am/AppErrors.java b/services/core/java/com/android/server/am/AppErrors.java
index b1fc029..50d2cab 100644
--- a/services/core/java/com/android/server/am/AppErrors.java
+++ b/services/core/java/com/android/server/am/AppErrors.java
@@ -702,10 +702,10 @@
         }
 
         // Bump up the crash count of any services currently running in the proc.
-        for (int i = app.services.size() - 1; i >= 0; i--) {
+        for (int i = app.numberOfRunningServices() - 1; i >= 0; i--) {
             // Any services running in the application need to be placed
             // back in the pending list.
-            ServiceRecord sr = app.services.valueAt(i);
+            ServiceRecord sr = app.getRunningServiceAt(i);
             // If the service was restarted a while ago, then reset crash count, else increment it.
             if (now > sr.restartTime + ProcessList.MIN_CRASH_INTERVAL) {
                 sr.crashCount = 1;
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index dbad562..b647818 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -1013,9 +1013,7 @@
     @Override
     public void noteNetworkInterfaceType(String iface, int networkType) {
         enforceCallingPermission();
-        synchronized (mStats) {
-            mStats.noteNetworkInterfaceTypeLocked(iface, networkType);
-        }
+        mStats.noteNetworkInterfaceType(iface, networkType);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index 2d6ef81..ad85853 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -837,7 +837,8 @@
                         break;
                 }
 
-                if (app.isolated && app.services.size() <= 0 && app.isolatedEntryPoint == null) {
+                if (app.isolated && app.numberOfRunningServices() <= 0
+                        && app.isolatedEntryPoint == null) {
                     // If this is an isolated process, there are no services
                     // running in it, and it's not a special process with a
                     // custom entry point, then the process is no longer
@@ -1446,12 +1447,12 @@
         }
 
         int capabilityFromFGS = 0; // capability from foreground service.
-        for (int is = app.services.size() - 1;
+        for (int is = app.numberOfRunningServices() - 1;
                 is >= 0 && (adj > ProcessList.FOREGROUND_APP_ADJ
                         || schedGroup == ProcessList.SCHED_GROUP_BACKGROUND
                         || procState > PROCESS_STATE_TOP);
                 is--) {
-            ServiceRecord s = app.services.valueAt(is);
+            ServiceRecord s = app.getRunningServiceAt(is);
             if (s.startRequested) {
                 app.hasStartedServices = true;
                 if (procState > PROCESS_STATE_SERVICE) {
diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java
index 61ebc36..a1ec07c 100644
--- a/services/core/java/com/android/server/am/ProcessRecord.java
+++ b/services/core/java/com/android/server/am/ProcessRecord.java
@@ -261,9 +261,9 @@
     // Controller for error dialogs
     private final ErrorDialogController mDialogController = new ErrorDialogController();
     // Controller for driving the process state on the window manager side.
-    final private WindowProcessController mWindowProcessController;
+    private final WindowProcessController mWindowProcessController;
     // all ServiceRecord running in this process
-    final ArraySet<ServiceRecord> services = new ArraySet<>();
+    private final ArraySet<ServiceRecord> mServices = new ArraySet<>();
     // services that are currently executing code (need to remain foreground).
     final ArraySet<ServiceRecord> executingServices = new ArraySet<>();
     // All ConnectionRecord this process holds
@@ -577,10 +577,10 @@
             pw.println(Arrays.toString(isolatedEntryPointArgs));
         }
         mWindowProcessController.dump(pw, prefix);
-        if (services.size() > 0) {
+        if (mServices.size() > 0) {
             pw.print(prefix); pw.println("Services:");
-            for (int i=0; i<services.size(); i++) {
-                pw.print(prefix); pw.print("  - "); pw.println(services.valueAt(i));
+            for (int i = 0; i < mServices.size(); i++) {
+                pw.print(prefix); pw.print("  - "); pw.println(mServices.valueAt(i));
             }
         }
         if (executingServices.size() > 0) {
@@ -735,6 +735,60 @@
         }
     }
 
+    /**
+     * Records a service as running in the process. Note that this method does not actually start
+     * the service, but records the service as started for bookkeeping.
+     *
+     * @return true if the service was added, false otherwise.
+     */
+    boolean startService(ServiceRecord record) {
+        if (record == null) {
+            return false;
+        }
+        boolean added = mServices.add(record);
+        if (added && record.serviceInfo != null) {
+            mWindowProcessController.onServiceStarted(record.serviceInfo);
+        }
+        return added;
+    }
+
+    /**
+     * Records a service as stopped. Note that like {@link #startService(ServiceRecord)} this method
+     * does not actually stop the service, but records the service as stopped for bookkeeping.
+     *
+     * @return true if the service was removed, false otherwise.
+     */
+    boolean stopService(ServiceRecord record) {
+        return mServices.remove(record);
+    }
+
+    /**
+     * The same as calling {@link #stopService(ServiceRecord)} on all current running services.
+     */
+    void stopAllServices() {
+        mServices.clear();
+    }
+
+    /**
+     * Returns the number of services added with {@link #startService(ServiceRecord)} and not yet
+     * removed by a call to {@link #stopService(ServiceRecord)} or {@link #stopAllServices()}.
+     *
+     * @see #startService(ServiceRecord)
+     * @see #stopService(ServiceRecord)
+     */
+    int numberOfRunningServices() {
+        return mServices.size();
+    }
+
+    /**
+     * Returns the service at the specified {@code index}.
+     *
+     * @see #numberOfRunningServices()
+     */
+    ServiceRecord getRunningServiceAt(int index) {
+        return mServices.valueAt(index);
+    }
+
     void setCached(boolean cached) {
         if (mCached != cached) {
             mCached = cached;
@@ -768,9 +822,9 @@
             return true;
         }
 
-        final int servicesSize = services.size();
+        final int servicesSize = mServices.size();
         for (int i = 0; i < servicesSize; i++) {
-            ServiceRecord r = services.valueAt(i);
+            ServiceRecord r = mServices.valueAt(i);
             if (r.isForeground) {
                 return true;
             }
@@ -1289,16 +1343,16 @@
     }
 
     void updateBoundClientUids() {
-        if (services.isEmpty()) {
+        if (mServices.isEmpty()) {
             clearBoundClientUids();
             return;
         }
         // grab a set of clientUids of all connections of all services
         ArraySet<Integer> boundClientUids = new ArraySet<>();
-        final int K = services.size();
-        for (int j = 0; j < K; j++) {
+        final int serviceCount = mServices.size();
+        for (int j = 0; j < serviceCount; j++) {
             ArrayMap<IBinder, ArrayList<ConnectionRecord>> conns =
-                    services.valueAt(j).getConnections();
+                    mServices.valueAt(j).getConnections();
             final int N = conns.size();
             for (int conni = 0; conni < N; conni++) {
                 ArrayList<ConnectionRecord> c = conns.valueAt(conni);
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index c2c79d3..032ad63 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -30,6 +30,7 @@
 import android.media.AudioSystem;
 import android.media.IAudioRoutesObserver;
 import android.media.IStrategyPreferredDeviceDispatcher;
+import android.media.MediaMetrics;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
@@ -668,6 +669,13 @@
         }
         AudioService.sForceUseLogger.log(
                 new AudioServiceEvents.ForceUseEvent(useCase, config, eventSource));
+        new MediaMetrics.Item(MediaMetrics.Name.AUDIO_FORCE_USE + MediaMetrics.SEPARATOR
+                + AudioSystem.forceUseUsageToString(useCase))
+                .set(MediaMetrics.Property.EVENT, "onSetForceUse")
+                .set(MediaMetrics.Property.FORCE_USE_DUE_TO, eventSource)
+                .set(MediaMetrics.Property.FORCE_USE_MODE,
+                        AudioSystem.forceUseConfigToString(config))
+                .record();
         AudioSystem.setForceUse(useCase, config);
     }
 
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index c17ed3e..3e97a1e 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -33,6 +33,7 @@
 import android.media.AudioSystem;
 import android.media.IAudioRoutesObserver;
 import android.media.IStrategyPreferredDeviceDispatcher;
+import android.media.MediaMetrics;
 import android.os.Binder;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
@@ -64,10 +65,69 @@
     // lock to synchronize all access to mConnectedDevices and mApmConnectedDevices
     private final Object mDevicesLock = new Object();
 
+    //Audio Analytics ids.
+    private static final String mMetricsId = "audio.device.";
+
     // List of connected devices
     // Key for map created from DeviceInfo.makeDeviceListKey()
     @GuardedBy("mDevicesLock")
-    private final LinkedHashMap<String, DeviceInfo> mConnectedDevices = new LinkedHashMap<>();
+    private final LinkedHashMap<String, DeviceInfo> mConnectedDevices = new LinkedHashMap<>() {
+        @Override
+        public DeviceInfo put(String key, DeviceInfo value) {
+            final DeviceInfo result = super.put(key, value);
+            record("put", true /* connected */, key, value);
+            return result;
+        }
+
+        @Override
+        public DeviceInfo putIfAbsent(String key, DeviceInfo value) {
+            final DeviceInfo result = super.putIfAbsent(key, value);
+            if (result == null) {
+                record("putIfAbsent", true /* connected */, key, value);
+            }
+            return result;
+        }
+
+        @Override
+        public DeviceInfo remove(Object key) {
+            final DeviceInfo result = super.remove(key);
+            if (result != null) {
+                record("remove", false /* connected */, (String) key, result);
+            }
+            return result;
+        }
+
+        @Override
+        public boolean remove(Object key, Object value) {
+            final boolean result = super.remove(key, value);
+            if (result) {
+                record("remove", false /* connected */, (String) key, (DeviceInfo) value);
+            }
+            return result;
+        }
+
+        // Not overridden
+        // clear
+        // compute
+        // computeIfAbsent
+        // computeIfPresent
+        // merge
+        // putAll
+        // replace
+        // replaceAll
+        private void record(String event, boolean connected, String key, DeviceInfo value) {
+            // DeviceInfo - int mDeviceType;
+            // DeviceInfo - int mDeviceCodecFormat;
+            new MediaMetrics.Item(MediaMetrics.Name.AUDIO_DEVICE
+                    + MediaMetrics.SEPARATOR + AudioSystem.getDeviceName(value.mDeviceType))
+                    .set(MediaMetrics.Property.ADDRESS, value.mDeviceAddress)
+                    .set(MediaMetrics.Property.EVENT, event)
+                    .set(MediaMetrics.Property.NAME, value.mDeviceName)
+                    .set(MediaMetrics.Property.STATE, connected
+                            ? MediaMetrics.Value.CONNECTED : MediaMetrics.Value.DISCONNECTED)
+                    .record();
+        }
+    };
 
     // List of devices actually connected to AudioPolicy (through AudioSystem), only one
     // by device type, which is used as the key, value is the DeviceInfo generated key.
@@ -236,6 +296,16 @@
                         + " codec=" + a2dpCodec
                         + " vol=" + a2dpVolume));
 
+        new MediaMetrics.Item(mMetricsId + "a2dp")
+                .set(MediaMetrics.Property.ADDRESS, address)
+                .set(MediaMetrics.Property.ENCODING, AudioSystem.audioFormatToString(a2dpCodec))
+                .set(MediaMetrics.Property.EVENT, "onSetA2dpSinkConnectionState")
+                .set(MediaMetrics.Property.INDEX, a2dpVolume)
+                .set(MediaMetrics.Property.STATE,
+                        state == BluetoothProfile.STATE_CONNECTED
+                        ? MediaMetrics.Value.CONNECTED : MediaMetrics.Value.DISCONNECTED)
+                .record();
+
         synchronized (mDevicesLock) {
             final String key = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
                     btDevice.getAddress());
@@ -284,6 +354,15 @@
             final DeviceInfo di = mConnectedDevices.get(key);
             boolean isConnected = di != null;
 
+            new MediaMetrics.Item(mMetricsId + "onSetA2dpSourceConnectionState")
+                    .set(MediaMetrics.Property.ADDRESS, address)
+                    .set(MediaMetrics.Property.DEVICE,
+                            AudioSystem.getDeviceName(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP))
+                    .set(MediaMetrics.Property.STATE,
+                            state == BluetoothProfile.STATE_CONNECTED
+                            ? MediaMetrics.Value.CONNECTED : MediaMetrics.Value.DISCONNECTED)
+                    .record();
+
             if (isConnected && state != BluetoothProfile.STATE_CONNECTED) {
                 makeA2dpSrcUnavailable(address);
             } else if (!isConnected && state == BluetoothProfile.STATE_CONNECTED) {
@@ -301,6 +380,17 @@
         AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
                 "onSetHearingAidConnectionState addr=" + address));
 
+        new MediaMetrics.Item(mMetricsId + "onSetHearingAidConnectionState")
+                .set(MediaMetrics.Property.ADDRESS, address)
+                .set(MediaMetrics.Property.DEVICE,
+                        AudioSystem.getDeviceName(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP))
+                .set(MediaMetrics.Property.STATE,
+                        state == BluetoothProfile.STATE_CONNECTED
+                                ? MediaMetrics.Value.CONNECTED : MediaMetrics.Value.DISCONNECTED)
+                .set(MediaMetrics.Property.STREAM_TYPE,
+                        AudioSystem.streamToString(streamType))
+                .record();
+
         synchronized (mDevicesLock) {
             final String key = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_HEARING_AID,
                     btDevice.getAddress());
@@ -317,10 +407,15 @@
     }
 
     @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
-    /*package*/ void onBluetoothA2dpActiveDeviceChange(
+        /*package*/ void onBluetoothA2dpActiveDeviceChange(
             @NonNull BtHelper.BluetoothA2dpDeviceInfo btInfo, int event) {
+        MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId
+                + "onBluetoothA2dpActiveDeviceChange")
+                .set(MediaMetrics.Property.EVENT, BtHelper.a2dpDeviceEventToString(event));
+
         final BluetoothDevice btDevice = btInfo.getBtDevice();
         if (btDevice == null) {
+            mmi.set(MediaMetrics.Property.EARLY_RETURN, "btDevice null").record();
             return;
         }
         if (AudioService.DEBUG_DEVICES) {
@@ -341,6 +436,8 @@
             if (mDeviceBroker.hasScheduledA2dpSinkConnectionState(btDevice)) {
                 AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
                         "A2dp config change ignored (scheduled connection change)"));
+                mmi.set(MediaMetrics.Property.EARLY_RETURN, "A2dp config change ignored")
+                        .record();
                 return;
             }
             final String key = DeviceInfo.makeDeviceListKey(
@@ -348,9 +445,16 @@
             final DeviceInfo di = mConnectedDevices.get(key);
             if (di == null) {
                 Log.e(TAG, "invalid null DeviceInfo in onBluetoothA2dpActiveDeviceChange");
+                mmi.set(MediaMetrics.Property.EARLY_RETURN, "null DeviceInfo").record();
                 return;
             }
 
+            mmi.set(MediaMetrics.Property.ADDRESS, address)
+                    .set(MediaMetrics.Property.ENCODING,
+                            AudioSystem.audioFormatToString(a2dpCodec))
+                    .set(MediaMetrics.Property.INDEX, a2dpVolume)
+                    .set(MediaMetrics.Property.NAME, di.mDeviceName);
+
             if (event == BtHelper.EVENT_ACTIVE_DEVICE_CHANGE) {
                 // Device is connected
                 if (a2dpVolume != -1) {
@@ -388,6 +492,7 @@
                                 + address + " codec=" + a2dpCodec).printLog(TAG));
             }
         }
+        mmi.record();
     }
 
     /*package*/ void onMakeA2dpDeviceUnavailableNow(String address, int a2dpCodec) {
@@ -399,6 +504,9 @@
     /*package*/ void onReportNewRoutes() {
         int n = mRoutesObservers.beginBroadcast();
         if (n > 0) {
+            new MediaMetrics.Item(mMetricsId + "onReportNewRoutes")
+                    .set(MediaMetrics.Property.OBSERVERS, n)
+                    .record();
             AudioRoutesInfo routes;
             synchronized (mCurAudioRoutes) {
                 routes = new AudioRoutesInfo(mCurAudioRoutes);
@@ -428,6 +536,13 @@
                             AudioDeviceInventory.WiredDeviceConnectionState wdcs) {
         AudioService.sDeviceLogger.log(new AudioServiceEvents.WiredDevConnectEvent(wdcs));
 
+        MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId
+                + "onSetWiredDeviceConnectionState")
+                .set(MediaMetrics.Property.ADDRESS, wdcs.mAddress)
+                .set(MediaMetrics.Property.DEVICE, AudioSystem.getDeviceName(wdcs.mType))
+                .set(MediaMetrics.Property.STATE,
+                        wdcs.mState == AudioService.CONNECTION_STATE_DISCONNECTED
+                                ? MediaMetrics.Value.DISCONNECTED : MediaMetrics.Value.CONNECTED);
         synchronized (mDevicesLock) {
             if ((wdcs.mState == AudioService.CONNECTION_STATE_DISCONNECTED)
                     && DEVICE_OVERRIDE_A2DP_ROUTE_ON_PLUG_SET.contains(wdcs.mType)) {
@@ -438,6 +553,8 @@
             if (!handleDeviceConnection(wdcs.mState == AudioService.CONNECTION_STATE_CONNECTED,
                     wdcs.mType, wdcs.mAddress, wdcs.mName)) {
                 // change of connection state failed, bailout
+                mmi.set(MediaMetrics.Property.EARLY_RETURN, "change of connection state failed")
+                        .record();
                 return;
             }
             if (wdcs.mState != AudioService.CONNECTION_STATE_DISCONNECTED) {
@@ -453,15 +570,20 @@
             sendDeviceConnectionIntent(wdcs.mType, wdcs.mState, wdcs.mAddress, wdcs.mName);
             updateAudioRoutes(wdcs.mType, wdcs.mState);
         }
+        mmi.record();
     }
 
     /*package*/ void onToggleHdmi() {
+        MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "onToggleHdmi")
+                .set(MediaMetrics.Property.DEVICE,
+                        AudioSystem.getDeviceName(AudioSystem.DEVICE_OUT_HDMI));
         synchronized (mDevicesLock) {
             // Is HDMI connected?
             final String key = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_HDMI, "");
             final DeviceInfo di = mConnectedDevices.get(key);
             if (di == null) {
                 Log.e(TAG, "invalid null DeviceInfo in onToggleHdmi");
+                mmi.set(MediaMetrics.Property.EARLY_RETURN, "invalid null DeviceInfo").record();
                 return;
             }
             // Toggle HDMI to retrigger broadcast with proper formats.
@@ -472,6 +594,7 @@
                     AudioSystem.DEVICE_STATE_AVAILABLE, "", "",
                     "android"); // reconnect
         }
+        mmi.record();
     }
 
     /*package*/ void onSaveSetPreferredDevice(int strategy, @NonNull AudioDeviceAttributes device) {
@@ -535,6 +658,12 @@
                     + Integer.toHexString(device) + " address:" + address
                     + " name:" + deviceName + ")");
         }
+        MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "handleDeviceConnection")
+                .set(MediaMetrics.Property.ADDRESS, address)
+                .set(MediaMetrics.Property.DEVICE, AudioSystem.getDeviceName(device))
+                .set(MediaMetrics.Property.MODE, connect
+                        ? MediaMetrics.Value.CONNECT : MediaMetrics.Value.DISCONNECT)
+                .set(MediaMetrics.Property.NAME, deviceName);
         synchronized (mDevicesLock) {
             final String deviceKey = DeviceInfo.makeDeviceListKey(device, address);
             if (AudioService.DEBUG_DEVICES) {
@@ -550,13 +679,18 @@
                         AudioSystem.DEVICE_STATE_AVAILABLE, address, deviceName,
                         AudioSystem.AUDIO_FORMAT_DEFAULT);
                 if (res != AudioSystem.AUDIO_STATUS_OK) {
-                    Slog.e(TAG, "not connecting device 0x" + Integer.toHexString(device)
-                            + " due to command error " + res);
+                    final String reason = "not connecting device 0x" + Integer.toHexString(device)
+                            + " due to command error " + res;
+                    Slog.e(TAG, reason);
+                    mmi.set(MediaMetrics.Property.EARLY_RETURN, reason)
+                            .set(MediaMetrics.Property.STATE, MediaMetrics.Value.DISCONNECTED)
+                            .record();
                     return false;
                 }
                 mConnectedDevices.put(deviceKey, new DeviceInfo(
                         device, deviceName, address, AudioSystem.AUDIO_FORMAT_DEFAULT));
                 mDeviceBroker.postAccessoryPlugMediaUnmute(device);
+                mmi.set(MediaMetrics.Property.STATE, MediaMetrics.Value.CONNECTED).record();
                 return true;
             } else if (!connect && isConnected) {
                 mAudioSystem.setDeviceConnectionState(device,
@@ -564,11 +698,13 @@
                         AudioSystem.AUDIO_FORMAT_DEFAULT);
                 // always remove even if disconnection failed
                 mConnectedDevices.remove(deviceKey);
+                mmi.set(MediaMetrics.Property.STATE, MediaMetrics.Value.CONNECTED).record();
                 return true;
             }
             Log.w(TAG, "handleDeviceConnection() failed, deviceKey=" + deviceKey
                     + ", deviceSpec=" + di + ", connect=" + connect);
         }
+        mmi.set(MediaMetrics.Property.STATE, MediaMetrics.Value.DISCONNECTED).record();
         return false;
     }
 
@@ -582,6 +718,8 @@
                     toRemove.add(deviceInfo.mDeviceAddress);
                 }
             });
+            new MediaMetrics.Item(mMetricsId + "disconnectA2dp")
+                    .record();
             if (toRemove.size() > 0) {
                 final int delay = checkSendBecomingNoisyIntentInt(
                         AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
@@ -602,6 +740,8 @@
                     toRemove.add(deviceInfo.mDeviceAddress);
                 }
             });
+            new MediaMetrics.Item(mMetricsId + "disconnectA2dpSink")
+                    .record();
             toRemove.stream().forEach(deviceAddress -> makeA2dpSrcUnavailable(deviceAddress));
         }
     }
@@ -615,6 +755,8 @@
                     toRemove.add(deviceInfo.mDeviceAddress);
                 }
             });
+            new MediaMetrics.Item(mMetricsId + "disconnectHearingAid")
+                    .record();
             if (toRemove.size() > 0) {
                 final int delay = checkSendBecomingNoisyIntentInt(
                         AudioSystem.DEVICE_OUT_HEARING_AID, 0, AudioSystem.DEVICE_NONE);
@@ -743,6 +885,8 @@
         final int res = mAudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
                 AudioSystem.DEVICE_STATE_AVAILABLE, address, name, a2dpCodec);
 
+        // TODO: log in MediaMetrics once distinction between connection failure and
+        // double connection is made.
         if (res != AudioSystem.AUDIO_STATUS_OK) {
             AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
                     "APM failed to make available A2DP device addr=" + address
@@ -771,7 +915,12 @@
 
     @GuardedBy("mDevicesLock")
     private void makeA2dpDeviceUnavailableNow(String address, int a2dpCodec) {
+        MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "a2dp." + address)
+                .set(MediaMetrics.Property.ENCODING, AudioSystem.audioFormatToString(a2dpCodec))
+                .set(MediaMetrics.Property.EVENT, "makeA2dpDeviceUnavailableNow");
+
         if (address == null) {
+            mmi.set(MediaMetrics.Property.EARLY_RETURN, "address null").record();
             return;
         }
         final String deviceToRemoveKey =
@@ -783,6 +932,9 @@
             // removing A2DP device not currently used by AudioPolicy, log but don't act on it
             AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent(
                     "A2DP device " + address + " made unavailable, was not used")).printLog(TAG));
+            mmi.set(MediaMetrics.Property.EARLY_RETURN,
+                    "A2DP device made unavailable, was not used")
+                    .record();
             return;
         }
 
@@ -804,6 +956,7 @@
         mApmConnectedDevices.remove(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP);
         // Remove A2DP routes as well
         setCurrentAudioRouteNameIfPossible(null);
+        mmi.record();
     }
 
     @GuardedBy("mDevicesLock")
@@ -862,6 +1015,14 @@
         mDeviceBroker.postApplyVolumeOnDevice(streamType,
                 AudioSystem.DEVICE_OUT_HEARING_AID, "makeHearingAidDeviceAvailable");
         setCurrentAudioRouteNameIfPossible(name);
+        new MediaMetrics.Item(mMetricsId + "makeHearingAidDeviceAvailable")
+                .set(MediaMetrics.Property.ADDRESS, address != null ? address : "")
+                .set(MediaMetrics.Property.DEVICE,
+                        AudioSystem.getDeviceName(AudioSystem.DEVICE_OUT_HEARING_AID))
+                .set(MediaMetrics.Property.NAME, name)
+                .set(MediaMetrics.Property.STREAM_TYPE,
+                        AudioSystem.streamToString(streamType))
+                .record();
     }
 
     @GuardedBy("mDevicesLock")
@@ -873,6 +1034,11 @@
                 DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_HEARING_AID, address));
         // Remove Hearing Aid routes as well
         setCurrentAudioRouteNameIfPossible(null);
+        new MediaMetrics.Item(mMetricsId + "makeHearingAidDeviceUnavailable")
+                .set(MediaMetrics.Property.ADDRESS, address != null ? address : "")
+                .set(MediaMetrics.Property.DEVICE,
+                        AudioSystem.getDeviceName(AudioSystem.DEVICE_OUT_HEARING_AID))
+                .record();
     }
 
     @GuardedBy("mDevicesLock")
@@ -919,10 +1085,18 @@
     @GuardedBy("mDevicesLock")
     private int checkSendBecomingNoisyIntentInt(int device,
             @AudioService.ConnectionState int state, int musicDevice) {
+        MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId
+                + "checkSendBecomingNoisyIntentInt")
+                .set(MediaMetrics.Property.DEVICE, AudioSystem.getDeviceName(device))
+                .set(MediaMetrics.Property.STATE,
+                        state == AudioService.CONNECTION_STATE_CONNECTED
+                                ? MediaMetrics.Value.CONNECTED : MediaMetrics.Value.DISCONNECTED);
         if (state != AudioService.CONNECTION_STATE_DISCONNECTED) {
+            mmi.set(MediaMetrics.Property.DELAY_MS, 0).record(); // OK to return
             return 0;
         }
         if (!BECOMING_NOISY_INTENT_DEVICES_SET.contains(device)) {
+            mmi.set(MediaMetrics.Property.DELAY_MS, 0).record(); // OK to return
             return 0;
         }
         int delay = 0;
@@ -950,12 +1124,14 @@
                 // the pausing of some apps that are playing remotely
                 AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent(
                         "dropping ACTION_AUDIO_BECOMING_NOISY")).printLog(TAG));
+                mmi.set(MediaMetrics.Property.DELAY_MS, 0).record(); // OK to return
                 return 0;
             }
             mDeviceBroker.postBroadcastBecomingNoisy();
             delay = AudioService.BECOMING_NOISY_DELAY_MS;
         }
 
+        mmi.set(MediaMetrics.Property.DELAY_MS, delay).record();
         return delay;
     }
 
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index f840f2d..7cac376 100755
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -88,6 +88,7 @@
 import android.media.IVolumeController;
 import android.media.MediaExtractor;
 import android.media.MediaFormat;
+import android.media.MediaMetrics;
 import android.media.PlayerBase;
 import android.media.VolumePolicy;
 import android.media.audiofx.AudioEffect;
@@ -1880,6 +1881,16 @@
         synchronized (mExtVolumeControllerLock) {
             extVolCtlr = mExtVolumeController;
         }
+        new MediaMetrics.Item(mMetricsId + "adjustSuggestedStreamVolume")
+                .setUid(Binder.getCallingUid())
+                .set(MediaMetrics.Property.CALLING_PACKAGE, callingPackage)
+                .set(MediaMetrics.Property.CLIENT_NAME, caller)
+                .set(MediaMetrics.Property.DIRECTION, direction > 0
+                        ? MediaMetrics.Value.UP : MediaMetrics.Value.DOWN)
+                .set(MediaMetrics.Property.EXTERNAL, extVolCtlr != null
+                        ? MediaMetrics.Value.YES : MediaMetrics.Value.NO)
+                .set(MediaMetrics.Property.FLAGS, flags)
+                .record();
         if (extVolCtlr != null) {
             sendMsg(mAudioHandler, MSG_NOTIFY_VOL_EVENT, SENDMSG_QUEUE,
                     direction, 0 /*ignored*/,
@@ -3141,21 +3152,32 @@
         if (uid == android.os.Process.SYSTEM_UID) {
             uid = UserHandle.getUid(userId, UserHandle.getAppId(uid));
         }
+        MediaMetrics.Item mmi = new MediaMetrics.Item(MediaMetrics.Name.AUDIO_MIC)
+                .setUid(uid)
+                .set(MediaMetrics.Property.CALLING_PACKAGE, callingPackage)
+                .set(MediaMetrics.Property.EVENT, "setMicrophoneMute")
+                .set(MediaMetrics.Property.REQUEST, on
+                        ? MediaMetrics.Value.MUTE : MediaMetrics.Value.UNMUTE);
+
         // If OP_MUTE_MICROPHONE is set, disallow unmuting.
         if (!on && mAppOps.noteOp(AppOpsManager.OP_MUTE_MICROPHONE, uid, callingPackage)
                 != AppOpsManager.MODE_ALLOWED) {
+            mmi.set(MediaMetrics.Property.EARLY_RETURN, "disallow unmuting").record();
             return;
         }
         if (!checkAudioSettingsPermission("setMicrophoneMute()")) {
+            mmi.set(MediaMetrics.Property.EARLY_RETURN, "!checkAudioSettingsPermission").record();
             return;
         }
         if (userId != UserHandle.getCallingUserId() &&
                 mContext.checkCallingOrSelfPermission(
                 android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)
                 != PackageManager.PERMISSION_GRANTED) {
+            mmi.set(MediaMetrics.Property.EARLY_RETURN, "permission").record();
             return;
         }
         mMicMuteFromApi = on;
+        mmi.record(); // record now, the no caller check will set the mute state.
         setMicrophoneMuteNoCallerCheck(userId);
     }
 
@@ -3167,6 +3189,12 @@
             return;
         }
         mMicMuteFromSwitch = on;
+        new MediaMetrics.Item(MediaMetrics.Name.AUDIO_MIC)
+                .setUid(userId)
+                .set(MediaMetrics.Property.EVENT, "setMicrophoneMuteFromSwitch")
+                .set(MediaMetrics.Property.REQUEST, on
+                        ? MediaMetrics.Value.MUTE : MediaMetrics.Value.UNMUTE)
+                .record();
         setMicrophoneMuteNoCallerCheck(userId);
     }
 
@@ -3207,6 +3235,17 @@
                 Log.e(TAG, "Error changing mic mute state to " + muted + " current:"
                         + mMicMuteFromSystemCached);
             }
+
+            new MediaMetrics.Item(MediaMetrics.Name.AUDIO_MIC)
+                    .setUid(userId)
+                    .set(MediaMetrics.Property.EVENT, "setMicrophoneMuteNoCallerCheck")
+                    .set(MediaMetrics.Property.MUTE, mMicMuteFromSystemCached
+                            ? MediaMetrics.Value.ON : MediaMetrics.Value.OFF)
+                    .set(MediaMetrics.Property.REQUEST, muted
+                            ? MediaMetrics.Value.MUTE : MediaMetrics.Value.UNMUTE)
+                    .set(MediaMetrics.Property.STATUS, ret)
+                    .record();
+
             try {
                 // send the intent even if there was a failure to change the actual mute state:
                 // the AudioManager.setMicrophoneMute API doesn't have a return value to
@@ -3941,10 +3980,20 @@
         }
 
         // for logging only
+        final int uid = Binder.getCallingUid();
+        final int pid = Binder.getCallingPid();
         final String eventSource = new StringBuilder("setSpeakerphoneOn(").append(on)
-                .append(") from u/pid:").append(Binder.getCallingUid()).append("/")
-                .append(Binder.getCallingPid()).toString();
+                .append(") from u/pid:").append(uid).append("/")
+                .append(pid).toString();
         final boolean stateChanged = mDeviceBroker.setSpeakerphoneOn(on, eventSource);
+        new MediaMetrics.Item(MediaMetrics.Name.AUDIO_DEVICE
+                + MediaMetrics.SEPARATOR + "setSpeakerphoneOn")
+                .setUid(uid)
+                .setPid(pid)
+                .set(MediaMetrics.Property.STATE, on
+                        ? MediaMetrics.Value.ON : MediaMetrics.Value.OFF)
+                .record();
+
         if (stateChanged) {
             final long ident = Binder.clearCallingIdentity();
             try {
@@ -3975,9 +4024,19 @@
         }
 
         // for logging only
+        final int uid = Binder.getCallingUid();
+        final int pid = Binder.getCallingPid();
         final String eventSource = new StringBuilder("setBluetoothScoOn(").append(on)
-                .append(") from u/pid:").append(Binder.getCallingUid()).append("/")
-                .append(Binder.getCallingPid()).toString();
+                .append(") from u/pid:").append(uid).append("/").append(pid).toString();
+
+        //bt sco
+        new MediaMetrics.Item(MediaMetrics.Name.AUDIO_DEVICE
+                + MediaMetrics.SEPARATOR + "setBluetoothScoOn")
+                .setUid(uid)
+                .setPid(pid)
+                .set(MediaMetrics.Property.STATE, on
+                        ? MediaMetrics.Value.ON : MediaMetrics.Value.OFF)
+                .record();
 
         mDeviceBroker.setBluetoothScoOn(on, eventSource);
     }
@@ -3993,9 +4052,20 @@
     /** @see AudioManager#setBluetoothA2dpOn(boolean) */
     public void setBluetoothA2dpOn(boolean on) {
         // for logging only
+        final int uid = Binder.getCallingUid();
+        final int pid = Binder.getCallingPid();
         final String eventSource = new StringBuilder("setBluetoothA2dpOn(").append(on)
-                .append(") from u/pid:").append(Binder.getCallingUid()).append("/")
-                .append(Binder.getCallingPid()).toString();
+                .append(") from u/pid:").append(uid).append("/")
+                .append(pid).toString();
+
+        new MediaMetrics.Item(MediaMetrics.Name.AUDIO_DEVICE
+                + MediaMetrics.SEPARATOR + "setBluetoothA2dpOn")
+                .setUid(uid)
+                .setPid(pid)
+                .set(MediaMetrics.Property.STATE, on
+                        ? MediaMetrics.Value.ON : MediaMetrics.Value.OFF)
+                .record();
+
         mDeviceBroker.setBluetoothA2dpOn_Async(on, eventSource);
     }
 
@@ -4006,31 +4076,59 @@
 
     /** @see AudioManager#startBluetoothSco() */
     public void startBluetoothSco(IBinder cb, int targetSdkVersion) {
+        final int uid = Binder.getCallingUid();
+        final int pid = Binder.getCallingPid();
         final int scoAudioMode =
                 (targetSdkVersion < Build.VERSION_CODES.JELLY_BEAN_MR2) ?
                         BtHelper.SCO_MODE_VIRTUAL_CALL : BtHelper.SCO_MODE_UNDEFINED;
         final String eventSource = new StringBuilder("startBluetoothSco()")
-                .append(") from u/pid:").append(Binder.getCallingUid()).append("/")
-                .append(Binder.getCallingPid()).toString();
+                .append(") from u/pid:").append(uid).append("/")
+                .append(pid).toString();
+
+        new MediaMetrics.Item(MediaMetrics.Name.AUDIO_BLUETOOTH)
+                .setUid(uid)
+                .setPid(pid)
+                .set(MediaMetrics.Property.EVENT, "startBluetoothSco")
+                .set(MediaMetrics.Property.SCO_AUDIO_MODE,
+                        BtHelper.scoAudioModeToString(scoAudioMode))
+                .record();
         startBluetoothScoInt(cb, scoAudioMode, eventSource);
+
     }
 
     /** @see AudioManager#startBluetoothScoVirtualCall() */
     public void startBluetoothScoVirtualCall(IBinder cb) {
+        final int uid = Binder.getCallingUid();
+        final int pid = Binder.getCallingPid();
         final String eventSource = new StringBuilder("startBluetoothScoVirtualCall()")
-                .append(") from u/pid:").append(Binder.getCallingUid()).append("/")
-                .append(Binder.getCallingPid()).toString();
+                .append(") from u/pid:").append(uid).append("/")
+                .append(pid).toString();
+
+        new MediaMetrics.Item(MediaMetrics.Name.AUDIO_BLUETOOTH)
+                .setUid(uid)
+                .setPid(pid)
+                .set(MediaMetrics.Property.EVENT, "startBluetoothScoVirtualCall")
+                .set(MediaMetrics.Property.SCO_AUDIO_MODE,
+                        BtHelper.scoAudioModeToString(BtHelper.SCO_MODE_VIRTUAL_CALL))
+                .record();
         startBluetoothScoInt(cb, BtHelper.SCO_MODE_VIRTUAL_CALL, eventSource);
     }
 
     void startBluetoothScoInt(IBinder cb, int scoAudioMode, @NonNull String eventSource) {
+        MediaMetrics.Item mmi = new MediaMetrics.Item(MediaMetrics.Name.AUDIO_BLUETOOTH)
+                .set(MediaMetrics.Property.EVENT, "startBluetoothScoInt")
+                .set(MediaMetrics.Property.SCO_AUDIO_MODE,
+                        BtHelper.scoAudioModeToString(scoAudioMode));
+
         if (!checkAudioSettingsPermission("startBluetoothSco()") ||
                 !mSystemReady) {
+            mmi.set(MediaMetrics.Property.EARLY_RETURN, "permission or systemReady").record();
             return;
         }
         synchronized (mDeviceBroker.mSetModeLock) {
             mDeviceBroker.startBluetoothScoForClient_Sync(cb, scoAudioMode, eventSource);
         }
+        mmi.record();
     }
 
     /** @see AudioManager#stopBluetoothSco() */
@@ -4039,12 +4137,21 @@
                 !mSystemReady) {
             return;
         }
+        final int uid = Binder.getCallingUid();
+        final int pid = Binder.getCallingPid();
         final String eventSource =  new StringBuilder("stopBluetoothSco()")
-                .append(") from u/pid:").append(Binder.getCallingUid()).append("/")
-                .append(Binder.getCallingPid()).toString();
+                .append(") from u/pid:").append(uid).append("/")
+                .append(pid).toString();
         synchronized (mDeviceBroker.mSetModeLock) {
             mDeviceBroker.stopBluetoothScoForClient_Sync(cb, eventSource);
         }
+        new MediaMetrics.Item(MediaMetrics.Name.AUDIO_BLUETOOTH)
+                .setUid(uid)
+                .setPid(pid)
+                .set(MediaMetrics.Property.EVENT, "stopBluetoothSco")
+                .set(MediaMetrics.Property.SCO_AUDIO_MODE,
+                        BtHelper.scoAudioModeToString(BtHelper.SCO_MODE_UNDEFINED))
+                .record();
     }
 
 
@@ -4806,6 +4913,14 @@
                 && state != CONNECTION_STATE_DISCONNECTED) {
             throw new IllegalArgumentException("Invalid state " + state);
         }
+        new MediaMetrics.Item(mMetricsId + "setWiredDeviceConnectionState")
+                .set(MediaMetrics.Property.ADDRESS, address)
+                .set(MediaMetrics.Property.CLIENT_NAME, caller)
+                .set(MediaMetrics.Property.DEVICE, AudioSystem.getDeviceName(type))
+                .set(MediaMetrics.Property.NAME, name)
+                .set(MediaMetrics.Property.STATE,
+                        state == CONNECTION_STATE_CONNECTED ? "connected" : "disconnected")
+                .record();
         mDeviceBroker.setWiredDeviceConnectionState(type, state, address, name, caller);
     }
 
@@ -5266,7 +5381,32 @@
         private String mVolumeIndexSettingName;
         private int mObservedDevices;
 
-        private final SparseIntArray mIndexMap = new SparseIntArray(8);
+        private final SparseIntArray mIndexMap = new SparseIntArray(8) {
+            @Override
+            public void put(int key, int value) {
+                super.put(key, value);
+                record("put", key, value);
+            }
+            @Override
+            public void setValueAt(int index, int value) {
+                super.setValueAt(index, value);
+                record("setValueAt", keyAt(index), value);
+            }
+
+            // Record all changes in the VolumeStreamState
+            private void record(String event, int key, int value) {
+                final String device = key == AudioSystem.DEVICE_OUT_DEFAULT ? "default"
+                        : AudioSystem.getOutputDeviceName(key);
+                new MediaMetrics.Item(MediaMetrics.Name.AUDIO_VOLUME + MediaMetrics.SEPARATOR
+                        + AudioSystem.streamToString(mStreamType)
+                        + "." + device)
+                        .set(MediaMetrics.Property.EVENT, event)
+                        .set(MediaMetrics.Property.INDEX, value)
+                        .set(MediaMetrics.Property.MIN_INDEX, mIndexMin)
+                        .set(MediaMetrics.Property.MAX_INDEX, mIndexMax)
+                        .record();
+            }
+        };
         private final Intent mVolumeChanged;
         private final Intent mStreamDevicesChanged;
 
@@ -5949,6 +6089,13 @@
                                 + eventSource);
                         break;
                     }
+                    new MediaMetrics.Item(MediaMetrics.Name.AUDIO_FORCE_USE
+                            + MediaMetrics.SEPARATOR + AudioSystem.forceUseUsageToString(useCase))
+                            .set(MediaMetrics.Property.EVENT, "setForceUse")
+                            .set(MediaMetrics.Property.FORCE_USE_DUE_TO, eventSource)
+                            .set(MediaMetrics.Property.FORCE_USE_MODE,
+                                    AudioSystem.forceUseConfigToString(config))
+                            .record();
                     sForceUseLogger.log(
                             new AudioServiceEvents.ForceUseEvent(useCase, config, eventSource));
                     AudioSystem.setForceUse(useCase, config);
@@ -6450,23 +6597,42 @@
     public int requestAudioFocus(AudioAttributes aa, int durationHint, IBinder cb,
             IAudioFocusDispatcher fd, String clientId, String callingPackageName, int flags,
             IAudioPolicyCallback pcb, int sdk) {
+        final int uid = Binder.getCallingUid();
+        MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "focus")
+                .setUid(uid)
+                //.putInt("durationHint", durationHint)
+                .set(MediaMetrics.Property.CALLING_PACKAGE, callingPackageName)
+                .set(MediaMetrics.Property.CLIENT_NAME, clientId)
+                .set(MediaMetrics.Property.EVENT, "requestAudioFocus")
+                .set(MediaMetrics.Property.FLAGS, flags);
+
         // permission checks
         if (aa != null && !isValidAudioAttributesUsage(aa)) {
-            Log.w(TAG, "Request using unsupported usage.");
+            final String reason = "Request using unsupported usage";
+            Log.w(TAG, reason);
+            mmi.set(MediaMetrics.Property.EARLY_RETURN, reason)
+                    .record();
             return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
         }
         if ((flags & AudioManager.AUDIOFOCUS_FLAG_LOCK) == AudioManager.AUDIOFOCUS_FLAG_LOCK) {
             if (AudioSystem.IN_VOICE_COMM_FOCUS_ID.equals(clientId)) {
                 if (PackageManager.PERMISSION_GRANTED != mContext.checkCallingOrSelfPermission(
                             android.Manifest.permission.MODIFY_PHONE_STATE)) {
-                    Log.e(TAG, "Invalid permission to (un)lock audio focus", new Exception());
+                    final String reason = "Invalid permission to (un)lock audio focus";
+                    Log.e(TAG, reason, new Exception());
+                    mmi.set(MediaMetrics.Property.EARLY_RETURN, reason)
+                            .record();
                     return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
                 }
             } else {
                 // only a registered audio policy can be used to lock focus
                 synchronized (mAudioPolicies) {
                     if (!mAudioPolicies.containsKey(pcb.asBinder())) {
-                        Log.e(TAG, "Invalid unregistered AudioPolicy to (un)lock audio focus");
+                        final String reason =
+                                "Invalid unregistered AudioPolicy to (un)lock audio focus";
+                        Log.e(TAG, reason);
+                        mmi.set(MediaMetrics.Property.EARLY_RETURN, reason)
+                                .record();
                         return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
                     }
                 }
@@ -6474,25 +6640,40 @@
         }
 
         if (callingPackageName == null || clientId == null || aa == null) {
-            Log.e(TAG, "Invalid null parameter to request audio focus");
+            final String reason = "Invalid null parameter to request audio focus";
+            Log.e(TAG, reason);
+            mmi.set(MediaMetrics.Property.EARLY_RETURN, reason)
+                    .record();
             return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
         }
-
+        mmi.record();
         return mMediaFocusControl.requestAudioFocus(aa, durationHint, cb, fd,
                 clientId, callingPackageName, flags, sdk,
-                forceFocusDuckingForAccessibility(aa, durationHint, Binder.getCallingUid()));
+                forceFocusDuckingForAccessibility(aa, durationHint, uid));
     }
 
     public int abandonAudioFocus(IAudioFocusDispatcher fd, String clientId, AudioAttributes aa,
             String callingPackageName) {
+        MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "focus")
+                .set(MediaMetrics.Property.CALLING_PACKAGE, callingPackageName)
+                .set(MediaMetrics.Property.CLIENT_NAME, clientId)
+                .set(MediaMetrics.Property.EVENT, "abandonAudioFocus");
+
         if (aa != null && !isValidAudioAttributesUsage(aa)) {
             Log.w(TAG, "Request using unsupported usage.");
+            mmi.set(MediaMetrics.Property.EARLY_RETURN, "unsupported usage").record();
+
             return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
         }
+        mmi.record();
         return mMediaFocusControl.abandonAudioFocus(fd, clientId, aa, callingPackageName);
     }
 
     public void unregisterAudioFocusClient(String clientId) {
+        new MediaMetrics.Item(mMetricsId + "focus")
+                .set(MediaMetrics.Property.CLIENT_NAME, clientId)
+                .set(MediaMetrics.Property.EVENT, "unregisterAudioFocusClient")
+                .record();
         mMediaFocusControl.unregisterAudioFocusClient(clientId);
     }
 
@@ -7066,6 +7247,12 @@
         }
     }
 
+    /**
+     * Audio Analytics ids.
+     */
+    private static final String mMetricsId = MediaMetrics.Name.AUDIO_SERVICE
+            + MediaMetrics.SEPARATOR;
+
     private static String safeMediaVolumeStateToString(int state) {
         switch(state) {
             case SAFE_MEDIA_VOLUME_NOT_CONFIGURED: return "SAFE_MEDIA_VOLUME_NOT_CONFIGURED";
diff --git a/services/core/java/com/android/server/audio/AudioServiceEvents.java b/services/core/java/com/android/server/audio/AudioServiceEvents.java
index add620e..5913567 100644
--- a/services/core/java/com/android/server/audio/AudioServiceEvents.java
+++ b/services/core/java/com/android/server/audio/AudioServiceEvents.java
@@ -19,6 +19,7 @@
 import android.media.AudioAttributes;
 import android.media.AudioManager;
 import android.media.AudioSystem;
+import android.media.MediaMetrics;
 
 import com.android.server.audio.AudioDeviceInventory.WiredDeviceConnectionState;
 
@@ -120,6 +121,7 @@
             mCaller = caller;
             mGroupName = null;
             mAudioAttributes = null;
+            logMetricEvent();
         }
 
         /** used for VOL_SET_HEARING_AID_VOL*/
@@ -132,6 +134,7 @@
             mCaller = null;
             mGroupName = null;
             mAudioAttributes = null;
+            logMetricEvent();
         }
 
         /** used for VOL_SET_AVRCP_VOL */
@@ -144,6 +147,7 @@
             mCaller = null;
             mGroupName = null;
             mAudioAttributes = null;
+            logMetricEvent();
         }
 
         /** used for VOL_VOICE_ACTIVITY_HEARING_AID */
@@ -156,6 +160,7 @@
             mCaller = null;
             mGroupName = null;
             mAudioAttributes = null;
+            logMetricEvent();
         }
 
         /** used for VOL_MODE_CHANGE_HEARING_AID */
@@ -168,6 +173,7 @@
             mCaller = null;
             mGroupName = null;
             mAudioAttributes = null;
+            logMetricEvent();
         }
 
         /** used for VOL_SET_GROUP_VOL */
@@ -179,6 +185,102 @@
             mCaller = caller;
             mGroupName = group;
             mAudioAttributes = aa;
+            logMetricEvent();
+        }
+
+
+        /**
+         * Audio Analytics unique Id.
+         */
+        private static final String mMetricsId = MediaMetrics.Name.AUDIO_VOLUME_EVENT;
+
+        /**
+         * Log mediametrics event
+         */
+        private void logMetricEvent() {
+            switch (mOp) {
+                case VOL_ADJUST_SUGG_VOL:
+                case VOL_ADJUST_VOL_UID:
+                case VOL_ADJUST_STREAM_VOL: {
+                    String eventName;
+                    switch (mOp) {
+                        case VOL_ADJUST_SUGG_VOL:
+                            eventName = "adjustSuggestedStreamVolume";
+                            break;
+                        case VOL_ADJUST_STREAM_VOL:
+                            eventName = "adjustStreamVolume";
+                            break;
+                        case VOL_ADJUST_VOL_UID:
+                            eventName = "adjustStreamVolumeForUid";
+                            break;
+                        default:
+                            return; // not possible, just return here
+                    }
+                    new MediaMetrics.Item(mMetricsId)
+                            .set(MediaMetrics.Property.CALLING_PACKAGE, mCaller)
+                            .set(MediaMetrics.Property.DIRECTION, mVal1 > 0 ? "up" : "down")
+                            .set(MediaMetrics.Property.EVENT, eventName)
+                            .set(MediaMetrics.Property.FLAGS, mVal2)
+                            .set(MediaMetrics.Property.STREAM_TYPE,
+                                    AudioSystem.streamToString(mStream))
+                            .record();
+                    return;
+                }
+                case VOL_SET_STREAM_VOL:
+                    new MediaMetrics.Item(mMetricsId)
+                            .set(MediaMetrics.Property.CALLING_PACKAGE, mCaller)
+                            .set(MediaMetrics.Property.EVENT, "setStreamVolume")
+                            .set(MediaMetrics.Property.FLAGS, mVal2)
+                            .set(MediaMetrics.Property.INDEX, mVal1)
+                            .set(MediaMetrics.Property.STREAM_TYPE,
+                                    AudioSystem.streamToString(mStream))
+                            .record();
+                    return;
+                case VOL_SET_HEARING_AID_VOL:
+                    new MediaMetrics.Item(mMetricsId)
+                            .set(MediaMetrics.Property.EVENT, "setHearingAidVolume")
+                            .set(MediaMetrics.Property.GAIN_DB, (double) mVal2)
+                            .set(MediaMetrics.Property.INDEX, mVal1)
+                            .record();
+                    return;
+                case VOL_SET_AVRCP_VOL:
+                    new MediaMetrics.Item(mMetricsId)
+                            .set(MediaMetrics.Property.EVENT, "setAvrcpVolume")
+                            .set(MediaMetrics.Property.INDEX, mVal1)
+                            .record();
+                    return;
+                case VOL_VOICE_ACTIVITY_HEARING_AID:
+                    new MediaMetrics.Item(mMetricsId)
+                            .set(MediaMetrics.Property.EVENT, "voiceActivityHearingAid")
+                            .set(MediaMetrics.Property.INDEX, mVal1)
+                            .set(MediaMetrics.Property.STATE,
+                                    mVal2 == 1 ? "active" : "inactive")
+                            .set(MediaMetrics.Property.STREAM_TYPE,
+                                    AudioSystem.streamToString(mStream))
+                            .record();
+                    return;
+                case VOL_MODE_CHANGE_HEARING_AID:
+                    new MediaMetrics.Item(mMetricsId)
+                            .set(MediaMetrics.Property.EVENT, "modeChangeHearingAid")
+                            .set(MediaMetrics.Property.INDEX, mVal1)
+                            .set(MediaMetrics.Property.MODE, AudioSystem.modeToString(mVal2))
+                            .set(MediaMetrics.Property.STREAM_TYPE,
+                                    AudioSystem.streamToString(mStream))
+                            .record();
+                    return;
+                case VOL_SET_GROUP_VOL:
+                    new MediaMetrics.Item(mMetricsId)
+                            .set(MediaMetrics.Property.ATTRIBUTES, mAudioAttributes.toString())
+                            .set(MediaMetrics.Property.CALLING_PACKAGE, mCaller)
+                            .set(MediaMetrics.Property.EVENT, "setVolumeIndexForAttributes")
+                            .set(MediaMetrics.Property.FLAGS, mVal2)
+                            .set(MediaMetrics.Property.GROUP, mGroupName)
+                            .set(MediaMetrics.Property.INDEX, mVal1)
+                            .record();
+                    return;
+                default:
+                    return;
+            }
         }
 
         @Override
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index 93d1bed..accb90c 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -113,6 +113,24 @@
 
     private static final int BT_HEARING_AID_GAIN_MIN = -128;
 
+    /**
+     * Returns a string representation of the scoAudioMode.
+     */
+    public static String scoAudioModeToString(int scoAudioMode) {
+        switch (scoAudioMode) {
+            case SCO_MODE_UNDEFINED:
+                return "SCO_MODE_UNDEFINED";
+            case SCO_MODE_VIRTUAL_CALL:
+                return "SCO_MODE_VIRTUAL_CALL";
+            case SCO_MODE_RAW:
+                return "SCO_MODE_RAW";
+            case SCO_MODE_VR:
+                return "SCO_MODE_VR";
+            default:
+                return "SCO_MODE_(" + scoAudioMode + ")";
+        }
+    }
+
     //----------------------------------------------------------------------
     /*package*/ static class BluetoothA2dpDeviceInfo {
         private final @NonNull BluetoothDevice mBtDevice;
@@ -965,4 +983,27 @@
                 return AudioSystem.AUDIO_FORMAT_DEFAULT;
         }
     }
+
+    /**
+     * Returns the String equivalent of the btCodecType.
+     *
+     * This uses an "ENCODING_" prefix for consistency with Audio;
+     * we could alternately use the "SOURCE_CODEC_TYPE_" prefix from Bluetooth.
+     */
+    public static String bluetoothCodecToEncodingString(int btCodecType) {
+        switch (btCodecType) {
+            case BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC:
+                return "ENCODING_SBC";
+            case BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC:
+                return "ENCODING_AAC";
+            case BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX:
+                return "ENCODING_APTX";
+            case BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD:
+                return "ENCODING_APTX_HD";
+            case BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC:
+                return "ENCODING_LDAC";
+            default:
+                return "ENCODING_BT_CODEC_TYPE(" + btCodecType + ")";
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/audio/MediaFocusControl.java b/services/core/java/com/android/server/audio/MediaFocusControl.java
index bfab991..26281b7 100755
--- a/services/core/java/com/android/server/audio/MediaFocusControl.java
+++ b/services/core/java/com/android/server/audio/MediaFocusControl.java
@@ -25,6 +25,7 @@
 import android.media.AudioManager;
 import android.media.AudioSystem;
 import android.media.IAudioFocusDispatcher;
+import android.media.MediaMetrics;
 import android.media.audiopolicy.IAudioPolicyCallback;
 import android.os.Binder;
 import android.os.Build;
@@ -144,6 +145,8 @@
     private static final AudioEventLogger mEventLogger = new AudioEventLogger(50,
             "focus commands as seen by MediaFocusControl");
 
+    private static final String mMetricsId = MediaMetrics.Name.AUDIO_FOCUS;
+
     /*package*/ void noFocusForSuspendedApp(@NonNull String packageName, int uid) {
         synchronized (mAudioFocusLock) {
             final Iterator<FocusRequester> stackIterator = mFocusStack.iterator();
@@ -818,6 +821,17 @@
     protected int requestAudioFocus(@NonNull AudioAttributes aa, int focusChangeHint, IBinder cb,
             IAudioFocusDispatcher fd, @NonNull String clientId, @NonNull String callingPackageName,
             int flags, int sdk, boolean forceDuck) {
+        new MediaMetrics.Item(mMetricsId)
+                .setUid(Binder.getCallingUid())
+                .set(MediaMetrics.Property.CALLING_PACKAGE, callingPackageName)
+                .set(MediaMetrics.Property.CLIENT_NAME, clientId)
+                .set(MediaMetrics.Property.EVENT, "requestAudioFocus")
+                .set(MediaMetrics.Property.FLAGS, flags)
+                .set(MediaMetrics.Property.FOCUS_CHANGE_HINT,
+                        AudioManager.audioFocusToString(focusChangeHint))
+                //.set(MediaMetrics.Property.SDK, sdk)
+                .record();
+
         mEventLogger.log((new AudioEventLogger.StringEvent(
                 "requestAudioFocus() from uid/pid " + Binder.getCallingUid()
                     + "/" + Binder.getCallingPid()
@@ -982,6 +996,13 @@
      * */
     protected int abandonAudioFocus(IAudioFocusDispatcher fl, String clientId, AudioAttributes aa,
             String callingPackageName) {
+        new MediaMetrics.Item(mMetricsId)
+                .setUid(Binder.getCallingUid())
+                .set(MediaMetrics.Property.CALLING_PACKAGE, callingPackageName)
+                .set(MediaMetrics.Property.CLIENT_NAME, clientId)
+                .set(MediaMetrics.Property.EVENT, "abandonAudioFocus")
+                .record();
+
         // AudioAttributes are currently ignored, to be used for zones / a11y
         mEventLogger.log((new AudioEventLogger.StringEvent(
                 "abandonAudioFocus() from uid/pid " + Binder.getCallingUid()
diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
index f4d7f9a..36d69c9 100644
--- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java
+++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
@@ -89,10 +89,8 @@
     private final BrightnessMappingStrategy mBrightnessMapper;
 
     // The minimum and maximum screen brightnesses.
-    private final int mScreenBrightnessRangeMinimum;
-    private final int mScreenBrightnessRangeMaximum;
-    private final float mScreenBrightnessRangeMinimumFloat;
-    private final float mScreenBrightnessRangeMaximumFloat;
+    private final float mScreenBrightnessRangeMinimum;
+    private final float mScreenBrightnessRangeMaximum;
 
     // How much to scale doze brightness by (should be (0, 1.0]).
     private final float mDozeScaleFactor;
@@ -156,7 +154,6 @@
     // The screen brightness threshold at which to brighten or darken the screen.
     private float mScreenBrighteningThreshold;
     private float mScreenDarkeningThreshold;
-
     // The most recent light sample.
     private float mLastObservedLux;
 
@@ -177,8 +174,9 @@
     // We preserve this value even when we stop using the light sensor so
     // that we can quickly revert to the previous auto-brightness level
     // while the light sensor warms up.
-    // Use -1 if there is no current auto-brightness value available.
-    private int mScreenAutoBrightness = PowerManager.BRIGHTNESS_INVALID;
+    // Use PowerManager.BRIGHTNESS_INVALID_FLOAT if there is no current auto-brightness value
+    // available.
+    private float mScreenAutoBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
 
     // The current display policy. This is useful, for example,  for knowing when we're dozing,
     // where the light sensor may not be available.
@@ -188,7 +186,7 @@
     // for the initial state of the sample.
     private boolean mBrightnessAdjustmentSamplePending;
     private float mBrightnessAdjustmentSampleOldLux;
-    private int mBrightnessAdjustmentSampleOldBrightness;
+    private float mBrightnessAdjustmentSampleOldBrightness;
 
     // When the short term model is invalidated, we don't necessarily reset it (i.e. clear the
     // user's adjustment) immediately, but wait for a drastic enough change in the ambient light.
@@ -243,13 +241,8 @@
         mCallbacks = callbacks;
         mSensorManager = sensorManager;
         mBrightnessMapper = mapper;
-        mScreenBrightnessRangeMinimum =
-                BrightnessSynchronizer.brightnessFloatToInt(mContext, brightnessMin);
-        mScreenBrightnessRangeMaximum =
-                com.android.internal.BrightnessSynchronizer.brightnessFloatToInt(
-                        mContext, brightnessMax);
-        mScreenBrightnessRangeMinimumFloat = brightnessMin;
-        mScreenBrightnessRangeMaximumFloat = brightnessMax;
+        mScreenBrightnessRangeMinimum = brightnessMin;
+        mScreenBrightnessRangeMaximum = brightnessMax;
         mLightSensorWarmUpTimeConfig = lightSensorWarmUpTime;
         mDozeScaleFactor = dozeScaleFactor;
         mNormalLightSensorRate = lightSensorRate;
@@ -299,12 +292,12 @@
         return true;
     }
 
-    public int getAutomaticScreenBrightness() {
+    public float getAutomaticScreenBrightness() {
         if (!mAmbientLuxValid) {
-            return -1;
+            return PowerManager.BRIGHTNESS_INVALID_FLOAT;
         }
         if (mDisplayPolicy == DisplayPowerRequest.POLICY_DOZE) {
-            return Math.round(mScreenAutoBrightness * mDozeScaleFactor);
+            return mScreenAutoBrightness * mDozeScaleFactor;
         }
         return mScreenAutoBrightness;
     }
@@ -385,7 +378,7 @@
 
     private boolean setScreenBrightnessByUser(float brightness) {
         if (!mAmbientLuxValid) {
-            // If we don't have a valid ambient lux then we don't have a valid brightness anyways,
+            // If we don't have a valid ambient lux then we don't have a valid brightness anyway,
             // and we can't use this data to add a new control point to the short-term model.
             return false;
         }
@@ -486,7 +479,7 @@
         } else if (mLightSensorEnabled) {
             mLightSensorEnabled = false;
             mAmbientLuxValid = !mResetAmbientLuxAfterWarmUpConfig;
-            mScreenAutoBrightness = PowerManager.BRIGHTNESS_INVALID;
+            mScreenAutoBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
             mRecentLightSamples = 0;
             mAmbientLightRingBuffer.clear();
             mCurrentLightSensorRate = -1;
@@ -735,29 +728,28 @@
 
         float value = mBrightnessMapper.getBrightness(mAmbientLux, mForegroundAppPackageName,
                 mForegroundAppCategory);
-        int newScreenAutoBrightness = BrightnessSynchronizer.brightnessFloatToInt(
-                mContext, clampScreenBrightnessFloat(value));
+        float newScreenAutoBrightness = clampScreenBrightness(value);
         // If screenAutoBrightness is set, we should have screen{Brightening,Darkening}Threshold,
         // in which case we ignore the new screen brightness if it doesn't differ enough from the
         // previous one.
-        if (mScreenAutoBrightness != -1
+        if (!Float.isNaN(mScreenAutoBrightness)
                 && !isManuallySet
                 && newScreenAutoBrightness > mScreenDarkeningThreshold
                 && newScreenAutoBrightness < mScreenBrighteningThreshold) {
             if (mLoggingEnabled) {
-                Slog.d(TAG, "ignoring newScreenAutoBrightness: " + mScreenDarkeningThreshold
-                        + " < " + newScreenAutoBrightness + " < " + mScreenBrighteningThreshold);
+                Slog.d(TAG, "ignoring newScreenAutoBrightness: "
+                        + mScreenDarkeningThreshold + " < " + newScreenAutoBrightness
+                        + " < " + mScreenBrighteningThreshold);
             }
             return;
         }
-
-        if (mScreenAutoBrightness != newScreenAutoBrightness) {
+        if (!BrightnessSynchronizer.floatEquals(mScreenAutoBrightness,
+                newScreenAutoBrightness)) {
             if (mLoggingEnabled) {
-                Slog.d(TAG, "updateAutoBrightness: " +
-                        "mScreenAutoBrightness=" + mScreenAutoBrightness + ", " +
-                        "newScreenAutoBrightness=" + newScreenAutoBrightness);
+                Slog.d(TAG, "updateAutoBrightness: "
+                        + "mScreenAutoBrightness=" + mScreenAutoBrightness + ", "
+                        + "newScreenAutoBrightness=" + newScreenAutoBrightness);
             }
-
             mScreenAutoBrightness = newScreenAutoBrightness;
             mScreenBrighteningThreshold = clampScreenBrightness(
                     mScreenBrightnessThresholds.getBrighteningThreshold(newScreenAutoBrightness));
@@ -770,19 +762,12 @@
         }
     }
 
-    // Clamps values with float range [1.0-255.0]
-    // TODO(brightnessfloat): convert everything that uses this to float system
+    // Clamps values with float range [0.0-1.0]
     private float clampScreenBrightness(float value) {
         return MathUtils.constrain(value,
                 mScreenBrightnessRangeMinimum, mScreenBrightnessRangeMaximum);
     }
 
-    // Clamps values with float range [0.0-1.0]
-    private float clampScreenBrightnessFloat(float value) {
-        return MathUtils.constrain(value,
-                mScreenBrightnessRangeMinimumFloat, mScreenBrightnessRangeMaximumFloat);
-    }
-
     private void prepareBrightnessAdjustmentSample() {
         if (!mBrightnessAdjustmentSamplePending) {
             mBrightnessAdjustmentSamplePending = true;
@@ -806,12 +791,13 @@
     private void collectBrightnessAdjustmentSample() {
         if (mBrightnessAdjustmentSamplePending) {
             mBrightnessAdjustmentSamplePending = false;
-            if (mAmbientLuxValid && mScreenAutoBrightness >= 0) {
+            if (mAmbientLuxValid && (mScreenAutoBrightness >= PowerManager.BRIGHTNESS_MIN
+                    || mScreenAutoBrightness == PowerManager.BRIGHTNESS_OFF_FLOAT)) {
                 if (mLoggingEnabled) {
-                    Slog.d(TAG, "Auto-brightness adjustment changed by user: " +
-                            "lux=" + mAmbientLux + ", " +
-                            "brightness=" + mScreenAutoBrightness + ", " +
-                            "ring=" + mAmbientLightRingBuffer);
+                    Slog.d(TAG, "Auto-brightness adjustment changed by user: "
+                            + "lux=" + mAmbientLux + ", "
+                            + "brightness=" + mScreenAutoBrightness + ", "
+                            + "ring=" + mAmbientLightRingBuffer);
                 }
 
                 EventLog.writeEvent(EventLogTags.AUTO_BRIGHTNESS_ADJ,
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index b4f7cdb..02d499f 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -28,6 +28,8 @@
 import static android.hardware.display.DisplayViewport.VIEWPORT_EXTERNAL;
 import static android.hardware.display.DisplayViewport.VIEWPORT_INTERNAL;
 import static android.hardware.display.DisplayViewport.VIEWPORT_VIRTUAL;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
 
 import android.Manifest;
 import android.annotation.NonNull;
@@ -1363,8 +1365,7 @@
         return null;
     }
 
-    private SurfaceControl.ScreenshotGraphicBuffer screenshotInternal(int displayId,
-            boolean captureSecureLayer) {
+    private SurfaceControl.ScreenshotGraphicBuffer systemScreenshotInternal(int displayId) {
         synchronized (mSyncRoot) {
             final IBinder token = getDisplayToken(displayId);
             if (token == null) {
@@ -1376,15 +1377,42 @@
             }
 
             final DisplayInfo displayInfo = logicalDisplay.getDisplayInfoLocked();
-            if (captureSecureLayer) {
-                return SurfaceControl.screenshotToBufferWithSecureLayersUnsafe(token, new Rect(),
-                        displayInfo.getNaturalWidth(), displayInfo.getNaturalHeight(),
-                        false /* useIdentityTransform */, 0 /* rotation */);
-            } else {
-                return SurfaceControl.screenshotToBuffer(token, new Rect(),
-                        displayInfo.getNaturalWidth(), displayInfo.getNaturalHeight(),
-                        false /* useIdentityTransform */, 0 /* rotation */);
+            return SurfaceControl.screenshotToBufferWithSecureLayersUnsafe(token, new Rect(),
+                    displayInfo.getNaturalWidth(), displayInfo.getNaturalHeight(),
+                    false /* useIdentityTransform */, 0 /* rotation */);
+        }
+    }
+
+    private SurfaceControl.ScreenshotGraphicBuffer userScreenshotInternal(int displayId) {
+        synchronized (mSyncRoot) {
+            final IBinder token = getDisplayToken(displayId);
+            if (token == null) {
+                return null;
             }
+            final LogicalDisplay logicalDisplay = mLogicalDisplays.get(displayId);
+            if (logicalDisplay == null) {
+                return null;
+            }
+
+            final DisplayInfo displayInfo = logicalDisplay.getDisplayInfoLocked();
+            // Takes screenshot based on current device orientation.
+            final Display display = DisplayManagerGlobal.getInstance()
+                    .getRealDisplay(displayId);
+            if (display == null) {
+                return null;
+            }
+            final Point displaySize = new Point();
+            display.getRealSize(displaySize);
+
+            int rotation = displayInfo.rotation;
+            // TODO (b/153382624) : This workaround solution would be removed after
+            // SurfaceFlinger fixes the inconsistency with rotation direction issue.
+            if (rotation == ROTATION_90 || rotation == ROTATION_270) {
+                rotation = (rotation == ROTATION_90) ? ROTATION_270 : ROTATION_90;
+            }
+
+            return SurfaceControl.screenshotToBuffer(token, new Rect(), displaySize.x,
+                    displaySize.y, false /* useIdentityTransform */, rotation /* rotation */);
         }
     }
 
@@ -2502,13 +2530,13 @@
         }
 
         @Override
-        public SurfaceControl.ScreenshotGraphicBuffer screenshot(int displayId) {
-            return screenshotInternal(displayId, true);
+        public SurfaceControl.ScreenshotGraphicBuffer systemScreenshot(int displayId) {
+            return systemScreenshotInternal(displayId);
         }
 
         @Override
-        public SurfaceControl.ScreenshotGraphicBuffer screenshotWithoutSecureLayer(int displayId) {
-            return screenshotInternal(displayId, false);
+        public SurfaceControl.ScreenshotGraphicBuffer userScreenshot(int displayId) {
+            return userScreenshotInternal(displayId);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 48e30bf..bafeb77 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -948,8 +948,7 @@
         if (Float.isNaN(brightnessState)) {
             float newAutoBrightnessAdjustment = autoBrightnessAdjustment;
             if (autoBrightnessEnabled) {
-                brightnessState = BrightnessSynchronizer.brightnessIntToFloat(
-                mContext, mAutomaticBrightnessController.getAutomaticScreenBrightness());
+                brightnessState = mAutomaticBrightnessController.getAutomaticScreenBrightness();
                 newAutoBrightnessAdjustment =
                         mAutomaticBrightnessController.getAutomaticScreenBrightnessAdjustment();
             }
diff --git a/services/core/java/com/android/server/display/HysteresisLevels.java b/services/core/java/com/android/server/display/HysteresisLevels.java
index f0a505d..2b56569 100644
--- a/services/core/java/com/android/server/display/HysteresisLevels.java
+++ b/services/core/java/com/android/server/display/HysteresisLevels.java
@@ -64,8 +64,8 @@
      * Return the brightening hysteresis threshold for the given value level.
      */
     public float getBrighteningThreshold(float value) {
-        float brightConstant = getReferenceLevel(value, mBrighteningThresholds);
-        float brightThreshold = value * (1.0f + brightConstant);
+        final float brightConstant = getReferenceLevel(value, mBrighteningThresholds);
+        final float brightThreshold = value * (1.0f + brightConstant);
         if (DEBUG) {
             Slog.d(TAG, "bright hysteresis constant=" + brightConstant + ", threshold="
                     + brightThreshold + ", value=" + value);
@@ -77,8 +77,8 @@
      * Return the darkening hysteresis threshold for the given value level.
      */
     public float getDarkeningThreshold(float value) {
-        float darkConstant = getReferenceLevel(value, mDarkeningThresholds);
-        float darkThreshold = value * (1.0f - darkConstant);
+        final float darkConstant = getReferenceLevel(value, mDarkeningThresholds);
+        final float darkThreshold = value * (1.0f - darkConstant);
         if (DEBUG) {
             Slog.d(TAG, "dark hysteresis constant=: " + darkConstant + ", threshold="
                     + darkThreshold + ", value=" + value);
diff --git a/services/core/java/com/android/server/emergency/EmergencyAffordanceService.java b/services/core/java/com/android/server/emergency/EmergencyAffordanceService.java
index 1cf27ff..cc7915c 100644
--- a/services/core/java/com/android/server/emergency/EmergencyAffordanceService.java
+++ b/services/core/java/com/android/server/emergency/EmergencyAffordanceService.java
@@ -20,90 +20,80 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.os.Binder;
+import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Looper;
 import android.os.Message;
 import android.provider.Settings;
-import android.telephony.CellInfo;
-import android.telephony.CellInfoGsm;
-import android.telephony.CellInfoLte;
-import android.telephony.CellInfoWcdma;
-import android.telephony.CellLocation;
-import android.telephony.PhoneStateListener;
 import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Slog;
 
+import com.android.internal.util.DumpUtils;
+import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.SystemService;
 
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
 
 /**
- * A service that listens to connectivity and SIM card changes and determines if the emergency mode
- * should be enabled
+ * A service that listens to connectivity and SIM card changes and determines if the emergency
+ * affordance should be enabled.
  */
 public class EmergencyAffordanceService extends SystemService {
 
     private static final String TAG = "EmergencyAffordanceService";
+    private static final boolean DBG = false;
 
-    private static final int NUM_SCANS_UNTIL_ABORT = 4;
+    private static final String SERVICE_NAME = "emergency_affordance";
 
     private static final int INITIALIZE_STATE = 1;
-    private static final int CELL_INFO_STATE_CHANGED = 2;
-    private static final int SUBSCRIPTION_CHANGED = 3;
-
     /**
-     * Global setting, whether the last scan of the sim cards reveal that a sim was inserted that
-     * requires the emergency affordance. The value is a boolean (1 or 0).
-     * @hide
+     * @param arg1 slot Index
+     * @param arg2 0
+     * @param obj ISO country code
      */
-    private static final String EMERGENCY_SIM_INSERTED_SETTING = "emergency_sim_inserted_before";
+    private static final int NETWORK_COUNTRY_CHANGED = 2;
+    private static final int SUBSCRIPTION_CHANGED = 3;
+    private static final int UPDATE_AIRPLANE_MODE_STATUS = 4;
+
+    // Global Settings to override emergency affordance country ISO for debugging.
+    // Available only on debug build. The value is a country ISO string in lower case (eg. "us").
+    private static final String EMERGENCY_AFFORDANCE_OVERRIDE_ISO =
+            "emergency_affordance_override_iso";
 
     private final Context mContext;
-    private final ArrayList<Integer> mEmergencyCallMccNumbers;
-
-    private final Object mLock = new Object();
-
-    private TelephonyManager mTelephonyManager;
+    // Country ISOs that require affordance
+    private final ArrayList<String> mEmergencyCallCountryIsos;
     private SubscriptionManager mSubscriptionManager;
-    private boolean mEmergencyAffordanceNeeded;
+    private TelephonyManager mTelephonyManager;
     private MyHandler mHandler;
-    private int mScansCompleted;
-    private PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
-        @Override
-        public void onCellInfoChanged(List<CellInfo> cellInfo) {
-            if (!isEmergencyAffordanceNeeded()) {
-                requestCellScan();
-            }
-        }
-
-        @Override
-        public void onCellLocationChanged(CellLocation location) {
-            if (!isEmergencyAffordanceNeeded()) {
-                requestCellScan();
-            }
-        }
-    };
-    private BroadcastReceiver mAirplaneModeReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            if (Settings.Global.getInt(context.getContentResolver(),
-                    Settings.Global.AIRPLANE_MODE_ON, 0) == 0) {
-                startScanning();
-                requestCellScan();
-            }
-        }
-    };
-    private boolean mSimNeedsEmergencyAffordance;
-    private boolean mNetworkNeedsEmergencyAffordance;
+    private boolean mAnySimNeedsEmergencyAffordance;
+    private boolean mAnyNetworkNeedsEmergencyAffordance;
+    private boolean mEmergencyAffordanceNeeded;
+    private boolean mAirplaneModeEnabled;
     private boolean mVoiceCapable;
 
-    private void requestCellScan() {
-        mHandler.obtainMessage(CELL_INFO_STATE_CHANGED).sendToTarget();
-    }
+    private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED.equals(intent.getAction())) {
+                String countryCode = intent.getStringExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY);
+                int slotId = intent.getIntExtra(SubscriptionManager.EXTRA_SLOT_INDEX,
+                        SubscriptionManager.INVALID_SIM_SLOT_INDEX);
+                mHandler.obtainMessage(
+                        NETWORK_COUNTRY_CHANGED, slotId, 0, countryCode).sendToTarget();
+            } else if (Intent.ACTION_AIRPLANE_MODE_CHANGED.equals(intent.getAction())) {
+                mHandler.obtainMessage(UPDATE_AIRPLANE_MODE_STATUS).sendToTarget();
+            }
+        }
+    };
 
     private SubscriptionManager.OnSubscriptionsChangedListener mSubscriptionChangedListener
             = new SubscriptionManager.OnSubscriptionsChangedListener() {
@@ -116,207 +106,200 @@
     public EmergencyAffordanceService(Context context) {
         super(context);
         mContext = context;
-        int[] numbers = context.getResources().getIntArray(
-                com.android.internal.R.array.config_emergency_mcc_codes);
-        mEmergencyCallMccNumbers = new ArrayList<>(numbers.length);
-        for (int i = 0; i < numbers.length; i++) {
-            mEmergencyCallMccNumbers.add(numbers[i]);
+        String[] isos = context.getResources().getStringArray(
+                com.android.internal.R.array.config_emergency_iso_country_codes);
+        mEmergencyCallCountryIsos = new ArrayList<>(isos.length);
+        for (String iso : isos) {
+            mEmergencyCallCountryIsos.add(iso);
         }
-    }
 
-    private void updateEmergencyAffordanceNeeded() {
-        synchronized (mLock) {
-            mEmergencyAffordanceNeeded = mVoiceCapable && (mSimNeedsEmergencyAffordance ||
-                    mNetworkNeedsEmergencyAffordance);
-            Settings.Global.putInt(mContext.getContentResolver(),
-                    Settings.Global.EMERGENCY_AFFORDANCE_NEEDED,
-                    mEmergencyAffordanceNeeded ? 1 : 0);
-            if (mEmergencyAffordanceNeeded) {
-                stopScanning();
+        if (Build.IS_DEBUGGABLE) {
+            String overrideIso = Settings.Global.getString(
+                    mContext.getContentResolver(), EMERGENCY_AFFORDANCE_OVERRIDE_ISO);
+            if (!TextUtils.isEmpty(overrideIso)) {
+                if (DBG) Slog.d(TAG, "Override ISO to " + overrideIso);
+                mEmergencyCallCountryIsos.clear();
+                mEmergencyCallCountryIsos.add(overrideIso);
             }
         }
     }
 
-    private void stopScanning() {
-        synchronized (mLock) {
-            mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
-            mScansCompleted = 0;
-        }
-    }
-
-    private boolean isEmergencyAffordanceNeeded() {
-        synchronized (mLock) {
-            return mEmergencyAffordanceNeeded;
-        }
-    }
-
     @Override
     public void onStart() {
+        if (DBG) Slog.i(TAG, "onStart");
+        publishBinderService(SERVICE_NAME, new BinderService());
     }
 
     @Override
     public void onBootPhase(int phase) {
         if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) {
-            mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
-            mVoiceCapable = mTelephonyManager.isVoiceCapable();
-            if (!mVoiceCapable) {
-                updateEmergencyAffordanceNeeded();
-                return;
-            }
-            mSubscriptionManager = SubscriptionManager.from(mContext);
-            HandlerThread thread = new HandlerThread(TAG);
-            thread.start();
-            mHandler = new MyHandler(thread.getLooper());
-            mHandler.obtainMessage(INITIALIZE_STATE).sendToTarget();
-            startScanning();
-            IntentFilter filter = new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED);
-            mContext.registerReceiver(mAirplaneModeReceiver, filter);
-            mSubscriptionManager.addOnSubscriptionsChangedListener(mSubscriptionChangedListener);
+            if (DBG) Slog.i(TAG, "onBootPhase");
+            handleThirdPartyBootPhase();
         }
     }
 
-    private void startScanning() {
-        mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CELL_INFO
-                | PhoneStateListener.LISTEN_CELL_LOCATION);
-    }
-
     /** Handler to do the heavier work on */
     private class MyHandler extends Handler {
-
         public MyHandler(Looper l) {
             super(l);
         }
 
         @Override
         public void handleMessage(Message msg) {
+            if (DBG) Slog.d(TAG, "handleMessage: " + msg.what);
             switch (msg.what) {
                 case INITIALIZE_STATE:
                     handleInitializeState();
                     break;
-                case CELL_INFO_STATE_CHANGED:
-                    handleUpdateCellInfo();
+                case NETWORK_COUNTRY_CHANGED:
+                    final String countryIso = (String) msg.obj;
+                    final int slotId = msg.arg1;
+                    handleNetworkCountryChanged(countryIso, slotId);
                     break;
                 case SUBSCRIPTION_CHANGED:
                     handleUpdateSimSubscriptionInfo();
                     break;
+                case UPDATE_AIRPLANE_MODE_STATUS:
+                    handleUpdateAirplaneModeStatus();
+                    break;
+                default:
+                    Slog.e(TAG, "Unexpected message received: " + msg.what);
             }
         }
     }
 
     private void handleInitializeState() {
-        if (handleUpdateSimSubscriptionInfo()) {
-            return;
-        }
-        if (handleUpdateCellInfo()) {
-            return;
-        }
+        if (DBG) Slog.d(TAG, "handleInitializeState");
+        handleUpdateAirplaneModeStatus();
+        handleUpdateSimSubscriptionInfo();
+        updateNetworkCountry();
         updateEmergencyAffordanceNeeded();
     }
 
-    private boolean handleUpdateSimSubscriptionInfo() {
-        boolean neededBefore = simNeededAffordanceBefore();
-        boolean neededNow = neededBefore;
+    private void handleThirdPartyBootPhase() {
+        if (DBG) Slog.d(TAG, "handleThirdPartyBootPhase");
+        mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
+        mVoiceCapable = mTelephonyManager.isVoiceCapable();
+        if (!mVoiceCapable) {
+            updateEmergencyAffordanceNeeded();
+            return;
+        }
+
+        HandlerThread thread = new HandlerThread(TAG);
+        thread.start();
+        mHandler = new MyHandler(thread.getLooper());
+
+        mSubscriptionManager = SubscriptionManager.from(mContext);
+        mSubscriptionManager.addOnSubscriptionsChangedListener(mSubscriptionChangedListener);
+
+        IntentFilter filter = new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        filter.addAction(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED);
+        mContext.registerReceiver(mBroadcastReceiver, filter);
+
+        mHandler.obtainMessage(INITIALIZE_STATE).sendToTarget();
+    }
+
+    private void handleUpdateAirplaneModeStatus() {
+        mAirplaneModeEnabled = Settings.Global.getInt(mContext.getContentResolver(),
+                Settings.Global.AIRPLANE_MODE_ON, 0) == 1;
+        if (DBG) Slog.d(TAG, "APM status updated to " + mAirplaneModeEnabled);
+    }
+
+    private void handleUpdateSimSubscriptionInfo() {
         List<SubscriptionInfo> activeSubscriptionInfoList =
                 mSubscriptionManager.getActiveSubscriptionInfoList();
+        if (DBG) Slog.d(TAG, "handleUpdateSimSubscriptionInfo: " + activeSubscriptionInfoList);
         if (activeSubscriptionInfoList == null) {
-            setSimNeedsEmergencyAffordance(neededNow);
-            return neededNow;
+            return;
         }
+
+        boolean needsAffordance = false;
         for (SubscriptionInfo info : activeSubscriptionInfoList) {
-            int mcc = info.getMcc();
-            if (mccRequiresEmergencyAffordance(mcc)) {
-                neededNow = true;
+            if (isoRequiresEmergencyAffordance(info.getCountryIso())) {
+                needsAffordance = true;
                 break;
-            } else if (mcc != 0 && mcc != Integer.MAX_VALUE){
-                // a Sim with a different mcc code was found
-                neededNow = false;
-            }
-            String simOperator = mTelephonyManager
-                    .createForSubscriptionId(info.getSubscriptionId()).getSimOperator();
-            mcc = 0;
-            if (simOperator != null && simOperator.length() >= 3) {
-                mcc = Integer.parseInt(simOperator.substring(0, 3));
-            }
-            if (mcc != 0) {
-                if (mccRequiresEmergencyAffordance(mcc)) {
-                    neededNow = true;
-                    break;
-                } else {
-                    // a Sim with a different mcc code was found
-                    neededNow = false;
-                }
             }
         }
-        setSimNeedsEmergencyAffordance(neededNow);
-        return neededNow;
+
+        mAnySimNeedsEmergencyAffordance = needsAffordance;
+        updateEmergencyAffordanceNeeded();
     }
 
-    private void setSimNeedsEmergencyAffordance(boolean simNeedsEmergencyAffordance) {
-        if (simNeededAffordanceBefore() != simNeedsEmergencyAffordance) {
+    private void handleNetworkCountryChanged(String countryIso, int slotId) {
+        if (DBG) {
+            Slog.d(TAG, "handleNetworkCountryChanged: countryIso=" + countryIso
+                    + ", slotId=" + slotId);
+        }
+
+        if (TextUtils.isEmpty(countryIso) && mAirplaneModeEnabled) {
+            Slog.w(TAG, "Ignore empty countryIso report when APM is on.");
+            return;
+        }
+
+        updateNetworkCountry();
+
+        updateEmergencyAffordanceNeeded();
+    }
+
+    private void updateNetworkCountry() {
+        boolean needsAffordance = false;
+
+        final int activeModems = mTelephonyManager.getActiveModemCount();
+        for (int i = 0; i < activeModems; i++) {
+            String countryIso = mTelephonyManager.getNetworkCountryIso(i);
+            if (DBG) Slog.d(TAG, "UpdateNetworkCountry: slotId=" + i + " countryIso=" + countryIso);
+            if (isoRequiresEmergencyAffordance(countryIso)) {
+                needsAffordance = true;
+                break;
+            }
+        }
+
+        mAnyNetworkNeedsEmergencyAffordance = needsAffordance;
+
+        updateEmergencyAffordanceNeeded();
+    }
+
+    private boolean isoRequiresEmergencyAffordance(String iso) {
+        return mEmergencyCallCountryIsos.contains(iso);
+    }
+
+    private void updateEmergencyAffordanceNeeded() {
+        if (DBG) {
+            Slog.d(TAG, "updateEmergencyAffordanceNeeded: mEmergencyAffordanceNeeded="
+                    + mEmergencyAffordanceNeeded + ", mVoiceCapable=" + mVoiceCapable
+                    + ", mAnySimNeedsEmergencyAffordance=" + mAnySimNeedsEmergencyAffordance
+                    + ", mAnyNetworkNeedsEmergencyAffordance="
+                    + mAnyNetworkNeedsEmergencyAffordance);
+        }
+        boolean lastAffordanceNeeded = mEmergencyAffordanceNeeded;
+
+        mEmergencyAffordanceNeeded = mVoiceCapable
+                && (mAnySimNeedsEmergencyAffordance || mAnyNetworkNeedsEmergencyAffordance);
+
+        if (lastAffordanceNeeded != mEmergencyAffordanceNeeded) {
             Settings.Global.putInt(mContext.getContentResolver(),
-                    EMERGENCY_SIM_INSERTED_SETTING,
-                    simNeedsEmergencyAffordance ? 1 : 0);
-        }
-        if (simNeedsEmergencyAffordance != mSimNeedsEmergencyAffordance) {
-            mSimNeedsEmergencyAffordance = simNeedsEmergencyAffordance;
-            updateEmergencyAffordanceNeeded();
+                    Settings.Global.EMERGENCY_AFFORDANCE_NEEDED,
+                    mEmergencyAffordanceNeeded ? 1 : 0);
         }
     }
 
-    private boolean simNeededAffordanceBefore() {
-        return Settings.Global.getInt(mContext.getContentResolver(),
-                EMERGENCY_SIM_INSERTED_SETTING, 0) != 0;
+    private void dumpInternal(IndentingPrintWriter ipw) {
+        ipw.println("EmergencyAffordanceService (dumpsys emergency_affordance) state:\n");
+        ipw.println("mEmergencyAffordanceNeeded=" + mEmergencyAffordanceNeeded);
+        ipw.println("mVoiceCapable=" + mVoiceCapable);
+        ipw.println("mAnySimNeedsEmergencyAffordance=" + mAnySimNeedsEmergencyAffordance);
+        ipw.println("mAnyNetworkNeedsEmergencyAffordance=" + mAnyNetworkNeedsEmergencyAffordance);
+        ipw.println("mEmergencyCallCountryIsos=" + String.join(",", mEmergencyCallCountryIsos));
     }
 
-    private boolean handleUpdateCellInfo() {
-        List<CellInfo> cellInfos = mTelephonyManager.getAllCellInfo();
-        if (cellInfos == null) {
-            return false;
-        }
-        boolean stopScanningAfterScan = false;
-        for (CellInfo cellInfo : cellInfos) {
-            int mcc = 0;
-            if (cellInfo instanceof CellInfoGsm) {
-                mcc = ((CellInfoGsm) cellInfo).getCellIdentity().getMcc();
-            } else if (cellInfo instanceof CellInfoLte) {
-                mcc = ((CellInfoLte) cellInfo).getCellIdentity().getMcc();
-            } else if (cellInfo instanceof CellInfoWcdma) {
-                mcc = ((CellInfoWcdma) cellInfo).getCellIdentity().getMcc();
+    private final class BinderService extends Binder {
+        @Override
+        protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+            if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) {
+                return;
             }
-            if (mccRequiresEmergencyAffordance(mcc)) {
-                setNetworkNeedsEmergencyAffordance(true);
-                return true;
-            } else if (mcc != 0 && mcc != Integer.MAX_VALUE) {
-                // we found an mcc that isn't in the list, abort
-                stopScanningAfterScan = true;
-            }
-        }
-        if (stopScanningAfterScan) {
-            stopScanning();
-        } else {
-            onCellScanFinishedUnsuccessful();
-        }
-        setNetworkNeedsEmergencyAffordance(false);
-        return false;
-    }
 
-    private void setNetworkNeedsEmergencyAffordance(boolean needsAffordance) {
-        synchronized (mLock) {
-            mNetworkNeedsEmergencyAffordance = needsAffordance;
-            updateEmergencyAffordanceNeeded();
+            dumpInternal(new IndentingPrintWriter(pw, "  "));
         }
     }
-
-    private void onCellScanFinishedUnsuccessful() {
-        synchronized (mLock) {
-            mScansCompleted++;
-            if (mScansCompleted >= NUM_SCANS_UNTIL_ABORT) {
-                stopScanning();
-            }
-        }
-    }
-
-    private boolean mccRequiresEmergencyAffordance(int mcc) {
-        return mEmergencyCallMccNumbers.contains(mcc);
-    }
 }
diff --git a/services/core/java/com/android/server/media/BluetoothRouteProvider.java b/services/core/java/com/android/server/media/BluetoothRouteProvider.java
index 28f8380..3cf22c8 100644
--- a/services/core/java/com/android/server/media/BluetoothRouteProvider.java
+++ b/services/core/java/com/android/server/media/BluetoothRouteProvider.java
@@ -215,7 +215,6 @@
                 .setConnectionState(MediaRoute2Info.CONNECTION_STATE_DISCONNECTED)
                 .setDescription(mContext.getResources().getText(
                         R.string.bluetooth_a2dp_audio_route_name).toString())
-                //TODO: Set type correctly (BLUETOOTH_A2DP or HEARING_AID)
                 .setType(MediaRoute2Info.TYPE_BLUETOOTH_A2DP)
                 .setVolumeHandling(MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE)
                 .build();
@@ -236,6 +235,8 @@
         // Update volume when the connection state is changed.
         MediaRoute2Info.Builder builder = new MediaRoute2Info.Builder(btRoute.route)
                 .setConnectionState(state);
+        builder.setType(btRoute.connectedProfiles.get(BluetoothProfile.HEARING_AID, false)
+                ? MediaRoute2Info.TYPE_HEARING_AID : MediaRoute2Info.TYPE_BLUETOOTH_A2DP);
 
         if (state == MediaRoute2Info.CONNECTION_STATE_CONNECTED) {
             int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index 0d89997..d8bf9ed 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -16,6 +16,8 @@
 
 package com.android.server.media;
 
+import static android.media.MediaRoute2ProviderService.REASON_ROUTE_NOT_AVAILABLE;
+import static android.media.MediaRoute2ProviderService.REASON_UNKNOWN_ERROR;
 import static android.media.MediaRoute2ProviderService.REQUEST_ID_NONE;
 import static android.media.MediaRouter2Utils.getOriginalId;
 import static android.media.MediaRouter2Utils.getProviderId;
@@ -247,6 +249,22 @@
         }
     }
 
+    public void notifySessionHintsForCreatingSession(IMediaRouter2 router,
+            long uniqueRequestId, MediaRoute2Info route, Bundle sessionHints) {
+        Objects.requireNonNull(router, "router must not be null");
+        Objects.requireNonNull(route, "route must not be null");
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                notifySessionHintsForCreatingSessionLocked(uniqueRequestId,
+                        router, route, sessionHints);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
     public void selectRouteWithRouter2(IMediaRouter2 router, String uniqueSessionId,
             MediaRoute2Info route) {
         Objects.requireNonNull(router, "router must not be null");
@@ -265,7 +283,6 @@
         }
     }
 
-
     public void deselectRouteWithRouter2(IMediaRouter2 router, String uniqueSessionId,
             MediaRoute2Info route) {
         Objects.requireNonNull(router, "router must not be null");
@@ -634,12 +651,30 @@
 
         long uniqueRequestId = toUniqueRequestId(routerRecord.mRouterId, requestId);
         routerRecord.mUserRecord.mHandler.sendMessage(
-                obtainMessage(UserHandler::requestCreateSessionOnHandler,
+                obtainMessage(UserHandler::requestCreateSessionWithRouter2OnHandler,
                         routerRecord.mUserRecord.mHandler,
-                        uniqueRequestId, routerRecord, /* managerRecord= */ null, route,
+                        uniqueRequestId, routerRecord, route,
                         sessionHints));
     }
 
+    private void notifySessionHintsForCreatingSessionLocked(long uniqueRequestId,
+            @NonNull IMediaRouter2 router,
+            @NonNull MediaRoute2Info route, @Nullable Bundle sessionHints) {
+        final IBinder binder = router.asBinder();
+        final RouterRecord routerRecord = mAllRouterRecords.get(binder);
+
+        if (routerRecord == null) {
+            Slog.w(TAG, "requestCreateSessionWithRouter2ByManagerRequestLocked: "
+                    + "Ignoring unknown router.");
+            return;
+        }
+
+        routerRecord.mUserRecord.mHandler.sendMessage(
+                obtainMessage(UserHandler::requestCreateSessionWithManagerOnHandler,
+                        routerRecord.mUserRecord.mHandler,
+                        uniqueRequestId, routerRecord, route, sessionHints));
+    }
+
     private void selectRouteWithRouter2Locked(@NonNull IMediaRouter2 router,
             @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
         final IBinder binder = router.asBinder();
@@ -826,12 +861,13 @@
         }
 
         long uniqueRequestId = toUniqueRequestId(managerRecord.mManagerId, requestId);
-        //TODO(b/152851868): Use MediaRouter2's OnCreateSessionListener to send session hints.
+
+        // Before requesting to the provider, get session hints from the media router.
+        // As a return, media router will request to create a session.
         routerRecord.mUserRecord.mHandler.sendMessage(
-                obtainMessage(UserHandler::requestCreateSessionOnHandler,
+                obtainMessage(UserHandler::getSessionHintsForCreatingSessionOnHandler,
                         routerRecord.mUserRecord.mHandler,
-                        uniqueRequestId, routerRecord, managerRecord, route,
-                        /* sessionHints= */ null));
+                        uniqueRequestId, routerRecord, managerRecord, route));
     }
 
     private void selectRouteWithManagerLocked(int requestId, @NonNull IMediaRouter2Manager manager,
@@ -1149,7 +1185,6 @@
                     this, provider, uniqueRequestId, sessionInfo));
         }
 
-
         @Override
         public void onSessionUpdated(@NonNull MediaRoute2Provider provider,
                 @NonNull RoutingSessionInfo sessionInfo) {
@@ -1267,8 +1302,26 @@
             return -1;
         }
 
-        private void requestCreateSessionOnHandler(long uniqueRequestId,
-                @NonNull RouterRecord routerRecord, @Nullable ManagerRecord managerRecord,
+        private void getSessionHintsForCreatingSessionOnHandler(long uniqueRequestId,
+                @NonNull RouterRecord routerRecord, @NonNull ManagerRecord managerRecord,
+                @NonNull MediaRoute2Info route) {
+            SessionCreationRequest request =
+                    new SessionCreationRequest(routerRecord, uniqueRequestId, route, managerRecord);
+            mSessionCreationRequests.add(request);
+
+            try {
+                routerRecord.mRouter.getSessionHintsForCreatingSession(uniqueRequestId, route);
+            } catch (RemoteException ex) {
+                Slog.w(TAG, "requestGetSessionHintsOnHandler: "
+                        + "Failed to request. Router probably died.");
+                mSessionCreationRequests.remove(request);
+                notifyRequestFailedToManager(managerRecord.mManager,
+                        toOriginalRequestId(uniqueRequestId), REASON_UNKNOWN_ERROR);
+            }
+        }
+
+        private void requestCreateSessionWithRouter2OnHandler(long uniqueRequestId,
+                @NonNull RouterRecord routerRecord,
                 @NonNull MediaRoute2Info route, @Nullable Bundle sessionHints) {
 
             final MediaRoute2Provider provider = findProvider(route.getProviderId());
@@ -1281,13 +1334,50 @@
             }
 
             SessionCreationRequest request =
-                    new SessionCreationRequest(routerRecord, uniqueRequestId, route, managerRecord);
+                    new SessionCreationRequest(routerRecord, uniqueRequestId, route, null);
             mSessionCreationRequests.add(request);
 
             provider.requestCreateSession(uniqueRequestId, routerRecord.mPackageName,
                     route.getOriginalId(), sessionHints);
         }
 
+        private void requestCreateSessionWithManagerOnHandler(long uniqueRequestId,
+                @NonNull RouterRecord routerRecord,
+                @NonNull MediaRoute2Info route, @Nullable Bundle sessionHints) {
+            SessionCreationRequest matchingRequest = null;
+            for (SessionCreationRequest request : mSessionCreationRequests) {
+                if (request.mUniqueRequestId == uniqueRequestId) {
+                    matchingRequest = request;
+                    break;
+                }
+            }
+            if (matchingRequest == null) {
+                Slog.w(TAG, "requestCreateSessionWithKnownRequestOnHandler: "
+                        + "Ignoring an unknown request.");
+                return;
+            }
+
+            if (!TextUtils.equals(matchingRequest.mRoute.getId(), route.getId())) {
+                Slog.w(TAG, "requestCreateSessionWithKnownRequestOnHandler: "
+                        + "The given route is different from the requested route.");
+                return;
+            }
+
+            final MediaRoute2Provider provider = findProvider(route.getProviderId());
+            if (provider == null) {
+                Slog.w(TAG, "Ignoring session creation request since no provider found for"
+                        + " given route=" + route);
+
+                mSessionCreationRequests.remove(matchingRequest);
+                notifyRequestFailedToManager(matchingRequest.mRequestedManagerRecord.mManager,
+                        toOriginalRequestId(uniqueRequestId), REASON_ROUTE_NOT_AVAILABLE);
+                return;
+            }
+
+            provider.requestCreateSession(uniqueRequestId, routerRecord.mPackageName,
+                    route.getOriginalId(), sessionHints);
+        }
+
         // routerRecord can be null if the session is system's or RCN.
         private void selectRouteOnHandler(long uniqueRequestId, @Nullable RouterRecord routerRecord,
                 @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java
index d6bf9fb..bf2cc5e 100644
--- a/services/core/java/com/android/server/media/MediaRouterService.java
+++ b/services/core/java/com/android/server/media/MediaRouterService.java
@@ -487,6 +487,14 @@
 
     // Binder call
     @Override
+    public void notifySessionHintsForCreatingSession(IMediaRouter2 router,
+            long uniqueRequestId, MediaRoute2Info route, Bundle sessionHints) {
+        mService2.notifySessionHintsForCreatingSession(router,
+                uniqueRequestId, route, sessionHints);
+    }
+
+    // Binder call
+    @Override
     public void selectRouteWithRouter2(IMediaRouter2 router, String sessionId,
             MediaRoute2Info route) {
         mService2.selectRouteWithRouter2(router, sessionId, route);
diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
index 1345e37..41d7fff 100644
--- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
+++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
@@ -19,6 +19,11 @@
 import static android.media.MediaRoute2Info.FEATURE_LIVE_AUDIO;
 import static android.media.MediaRoute2Info.FEATURE_LIVE_VIDEO;
 import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER;
+import static android.media.MediaRoute2Info.TYPE_DOCK;
+import static android.media.MediaRoute2Info.TYPE_HDMI;
+import static android.media.MediaRoute2Info.TYPE_USB_DEVICE;
+import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES;
+import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET;
 
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -194,19 +199,27 @@
 
     private void updateDeviceRoute(AudioRoutesInfo newRoutes) {
         int name = R.string.default_audio_route_name;
+        int type = TYPE_BUILTIN_SPEAKER;
         if (newRoutes != null) {
             mCurAudioRoutesInfo.mainType = newRoutes.mainType;
-            if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADPHONES) != 0
-                    || (newRoutes.mainType & AudioRoutesInfo.MAIN_HEADSET) != 0) {
+            if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADPHONES) != 0) {
+                type = TYPE_WIRED_HEADPHONES;
+                name = com.android.internal.R.string.default_audio_route_name_headphones;
+            } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADSET) != 0) {
+                type = TYPE_WIRED_HEADSET;
                 name = com.android.internal.R.string.default_audio_route_name_headphones;
             } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) {
+                type = TYPE_DOCK;
                 name = com.android.internal.R.string.default_audio_route_name_dock_speakers;
             } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HDMI) != 0) {
+                type = TYPE_HDMI;
                 name = com.android.internal.R.string.default_audio_route_name_hdmi;
             } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_USB) != 0) {
+                type = TYPE_USB_DEVICE;
                 name = com.android.internal.R.string.default_audio_route_name_usb;
             }
         }
+
         mDeviceRoute = new MediaRoute2Info.Builder(
                 DEVICE_ROUTE_ID, mContext.getResources().getText(name).toString())
                 .setVolumeHandling(mAudioManager.isVolumeFixed()
@@ -214,8 +227,7 @@
                         : MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE)
                 .setVolumeMax(mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC))
                 .setVolume(mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC))
-                //TODO: Guess the exact type using AudioDevice
-                .setType(TYPE_BUILTIN_SPEAKER)
+                .setType(type)
                 .addFeature(FEATURE_LIVE_AUDIO)
                 .addFeature(FEATURE_LIVE_VIDEO)
                 .setConnectionState(MediaRoute2Info.CONNECTION_STATE_CONNECTED)
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index d432fc8..3e6d6f5 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -819,7 +819,9 @@
             }
             if (fromTargetApp) {
                 channel.setLockscreenVisibility(r.visibility);
-                channel.setAllowBubbles(existing != null && existing.canBubble());
+                channel.setAllowBubbles(existing != null
+                        ? existing.getAllowBubbles()
+                        : NotificationChannel.DEFAULT_ALLOW_BUBBLE);
             }
             clearLockedFieldsLocked(channel);
             channel.setImportanceLockedByOEM(r.oemLockedImportance);
@@ -1704,7 +1706,7 @@
         if (original.canShowBadge() != update.canShowBadge()) {
             update.lockFields(NotificationChannel.USER_LOCKED_SHOW_BADGE);
         }
-        if (original.canBubble() != update.canBubble()) {
+        if (original.getAllowBubbles() != update.getAllowBubbles()) {
             update.lockFields(NotificationChannel.USER_LOCKED_ALLOW_BUBBLE);
         }
     }
diff --git a/services/core/java/com/android/server/notification/ShortcutHelper.java b/services/core/java/com/android/server/notification/ShortcutHelper.java
index 1d48438..96da649 100644
--- a/services/core/java/com/android/server/notification/ShortcutHelper.java
+++ b/services/core/java/com/android/server/notification/ShortcutHelper.java
@@ -21,7 +21,6 @@
 import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED;
 
 import android.annotation.NonNull;
-import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.LauncherApps;
 import android.content.pm.ShortcutInfo;
@@ -41,9 +40,18 @@
 /**
  * Helper for querying shortcuts.
  */
-class ShortcutHelper {
+public class ShortcutHelper {
     private static final String TAG = "ShortcutHelper";
 
+    private static final IntentFilter SHARING_FILTER = new IntentFilter();
+    static {
+        try {
+            SHARING_FILTER.addDataType("*/*");
+        } catch (IntentFilter.MalformedMimeTypeException e) {
+            Slog.e(TAG, "Bad mime type", e);
+        }
+    }
+
     /**
      * Listener to call when a shortcut we're tracking has been removed.
      */
@@ -54,7 +62,6 @@
     private LauncherApps mLauncherAppsService;
     private ShortcutListener mShortcutListener;
     private ShortcutServiceInternal mShortcutServiceInternal;
-    private IntentFilter mSharingFilter;
 
     // Key: packageName Value: <shortcutId, notifId>
     private HashMap<String, HashMap<String, String>> mActiveShortcutBubbles = new HashMap<>();
@@ -122,12 +129,6 @@
             ShortcutServiceInternal shortcutServiceInternal) {
         mLauncherAppsService = launcherApps;
         mShortcutListener = listener;
-        mSharingFilter = new IntentFilter();
-        try {
-            mSharingFilter.addDataType("*/*");
-        } catch (IntentFilter.MalformedMimeTypeException e) {
-            Slog.e(TAG, "Bad mime type", e);
-        }
         mShortcutServiceInternal = shortcutServiceInternal;
     }
 
@@ -142,7 +143,21 @@
     }
 
     /**
-     * Only returns shortcut info if it's found and if it's {@link ShortcutInfo#isLongLived()}.
+     * Returns whether the given shortcut info is a conversation shortcut.
+     */
+    public static boolean isConversationShortcut(
+            ShortcutInfo shortcutInfo, ShortcutServiceInternal mShortcutServiceInternal,
+            int callingUserId) {
+        if (shortcutInfo == null || !shortcutInfo.isLongLived() || !shortcutInfo.isEnabled()) {
+            return false;
+        }
+        return mShortcutServiceInternal.isSharingShortcut(callingUserId, "android",
+                shortcutInfo.getPackage(), shortcutInfo.getId(), shortcutInfo.getUserId(),
+                SHARING_FILTER);
+    }
+
+    /**
+     * Only returns shortcut info if it's found and if it's a conversation shortcut.
      */
     ShortcutInfo getValidShortcutInfo(String shortcutId, String packageName, UserHandle user) {
         if (mLauncherAppsService == null) {
@@ -161,11 +176,7 @@
             ShortcutInfo info = shortcuts != null && shortcuts.size() > 0
                     ? shortcuts.get(0)
                     : null;
-            if (info == null || !info.isLongLived() || !info.isEnabled()) {
-                return null;
-            }
-            if (mShortcutServiceInternal.isSharingShortcut(user.getIdentifier(),
-                    "android", packageName, shortcutId, user.getIdentifier(), mSharingFilter)) {
+            if (isConversationShortcut(info, mShortcutServiceInternal, user.getIdentifier())) {
                 return info;
             }
             return null;
diff --git a/services/core/java/com/android/server/pm/AppsFilter.java b/services/core/java/com/android/server/pm/AppsFilter.java
index 7c47cf0..d36d038 100644
--- a/services/core/java/com/android/server/pm/AppsFilter.java
+++ b/services/core/java/com/android/server/pm/AppsFilter.java
@@ -612,7 +612,7 @@
                     }
                     final int insert = ~loc;
                     System.arraycopy(appIds, insert, buffer, 0, whitelistSize - insert);
-                    appIds[insert] = existingUid;
+                    appIds[insert] = existingAppId;
                     System.arraycopy(buffer, 0, appIds, insert + 1, whitelistSize - insert);
                     whitelistSize++;
                 }
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 2221644..6b1ef3a 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -150,7 +150,6 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
@@ -2099,11 +2098,13 @@
                             continue;
                         }
 
-                        mResolvedInstructionSets.add(archSubDir.getName());
-                        List<File> oatFiles = Arrays.asList(archSubDir.listFiles());
-                        if (!oatFiles.isEmpty()) {
-                            mResolvedInheritedFiles.addAll(oatFiles);
+                        File[] files = archSubDir.listFiles();
+                        if (files == null || files.length == 0) {
+                            continue;
                         }
+
+                        mResolvedInstructionSets.add(archSubDir.getName());
+                        mResolvedInheritedFiles.addAll(Arrays.asList(files));
                     }
                 }
             }
@@ -2117,7 +2118,8 @@
                     if (!libDir.exists() || !libDir.isDirectory()) {
                         continue;
                     }
-                    final List<File> libDirsToInherit = new LinkedList<>();
+                    final List<String> libDirsToInherit = new ArrayList<>();
+                    final List<File> libFilesToInherit = new ArrayList<>();
                     for (File archSubDir : libDir.listFiles()) {
                         if (!archSubDir.isDirectory()) {
                             continue;
@@ -2129,14 +2131,24 @@
                             Slog.e(TAG, "Skipping linking of native library directory!", e);
                             // shouldn't be possible, but let's avoid inheriting these to be safe
                             libDirsToInherit.clear();
+                            libFilesToInherit.clear();
                             break;
                         }
-                        if (!mResolvedNativeLibPaths.contains(relLibPath)) {
-                            mResolvedNativeLibPaths.add(relLibPath);
+
+                        File[] files = archSubDir.listFiles();
+                        if (files == null || files.length == 0) {
+                            continue;
                         }
-                        libDirsToInherit.addAll(Arrays.asList(archSubDir.listFiles()));
+
+                        libDirsToInherit.add(relLibPath);
+                        libFilesToInherit.addAll(Arrays.asList(files));
                     }
-                    mResolvedInheritedFiles.addAll(libDirsToInherit);
+                    for (String subDir : libDirsToInherit) {
+                        if (!mResolvedNativeLibPaths.contains(subDir)) {
+                            mResolvedNativeLibPaths.add(subDir);
+                        }
+                    }
+                    mResolvedInheritedFiles.addAll(libFilesToInherit);
                 }
             }
         }
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 9f543b4..7adafe3 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -351,6 +351,7 @@
 import com.android.server.pm.dex.DexoptOptions;
 import com.android.server.pm.dex.PackageDexUsage;
 import com.android.server.pm.dex.ViewCompiler;
+import com.android.server.pm.parsing.PackageCacher;
 import com.android.server.pm.parsing.PackageInfoUtils;
 import com.android.server.pm.parsing.PackageParser2;
 import com.android.server.pm.parsing.library.PackageBackwardCompatibility;
@@ -24232,6 +24233,25 @@
         }
 
         @Override
+        public void pruneCachedApksInApex(@NonNull List<PackageInfo> apexPackages) {
+            if (mCacheDir == null) {
+                return;
+            }
+
+            final PackageCacher cacher = new PackageCacher(mCacheDir);
+            synchronized (mLock) {
+                for (int i = 0, size = apexPackages.size(); i < size; i++) {
+                    final List<String> apkNames =
+                            mApexManager.getApksInApex(apexPackages.get(i).packageName);
+                    for (int j = 0, apksInApex = apkNames.size(); j < apksInApex; j++) {
+                        final AndroidPackage pkg = getPackage(apkNames.get(j));
+                        cacher.cleanCachedResult(new File(pkg.getCodePath()));
+                    }
+                }
+            }
+        }
+
+        @Override
         public String getSetupWizardPackageName() {
             return mSetupWizardPackage;
         }
@@ -24701,6 +24721,14 @@
             return packageName.equals(
                     PackageManagerService.this.ensureSystemPackageName(packageName));
         }
+
+        @Override
+        public void clearBlockUninstallForUser(@UserIdInt int userId) {
+            synchronized (mLock) {
+                mSettings.clearBlockUninstallLPw(userId);
+                mSettings.writePackageRestrictionsLPr(userId);
+            }
+        }
     }
 
     @GuardedBy("mLock")
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 44a61d8..ddeab29 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -1833,6 +1833,10 @@
         }
     }
 
+    void clearBlockUninstallLPw(int userId) {
+        mBlockUninstallPackages.remove(userId);
+    }
+
     boolean getBlockUninstallLPr(int userId, String packageName) {
         ArraySet<String> packages = mBlockUninstallPackages.get(userId);
         if (packages == null) {
diff --git a/services/core/java/com/android/server/pm/StagingManager.java b/services/core/java/com/android/server/pm/StagingManager.java
index d1d9877..1c1e64d 100644
--- a/services/core/java/com/android/server/pm/StagingManager.java
+++ b/services/core/java/com/android/server/pm/StagingManager.java
@@ -1226,8 +1226,9 @@
             // APEX checks. For single-package sessions, check if they contain an APEX. For
             // multi-package sessions, find all the child sessions that contain an APEX.
             if (hasApex) {
+                final List<PackageInfo> apexPackages;
                 try {
-                    final List<PackageInfo> apexPackages = submitSessionToApexService(session);
+                    apexPackages = submitSessionToApexService(session);
                     for (int i = 0, size = apexPackages.size(); i < size; i++) {
                         validateApexSignature(apexPackages.get(i));
                     }
@@ -1235,6 +1236,10 @@
                     session.setStagedSessionFailed(e.error, e.getMessage());
                     return;
                 }
+
+                final PackageManagerInternal packageManagerInternal =
+                        LocalServices.getService(PackageManagerInternal.class);
+                packageManagerInternal.pruneCachedApksInApex(apexPackages);
             }
 
             notifyPreRebootVerification_Apex_Complete(session.sessionId);
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index fc70af4..4561d2e 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -104,6 +104,7 @@
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.FastXmlSerializer;
 import com.android.internal.util.FrameworkStatsLog;
+import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.Preconditions;
 import com.android.internal.util.XmlUtils;
 import com.android.internal.widget.LockPatternUtils;
@@ -3153,13 +3154,17 @@
 
     /**
      * Removes the app restrictions file for a specific package and user id, if it exists.
+     *
+     * @return whether there were any restrictions.
      */
-    private static void cleanAppRestrictionsForPackageLAr(String pkg, @UserIdInt int userId) {
-        File dir = Environment.getUserSystemDirectory(userId);
-        File resFile = new File(dir, packageToRestrictionsFileName(pkg));
+    private static boolean cleanAppRestrictionsForPackageLAr(String pkg, @UserIdInt int userId) {
+        final File dir = Environment.getUserSystemDirectory(userId);
+        final File resFile = new File(dir, packageToRestrictionsFileName(pkg));
         if (resFile.exists()) {
             resFile.delete();
+            return true;
         }
+        return false;
     }
 
     /**
@@ -4003,17 +4008,24 @@
         if (restrictions != null) {
             restrictions.setDefusable(true);
         }
+        final boolean changed;
         synchronized (mAppRestrictionsLock) {
             if (restrictions == null || restrictions.isEmpty()) {
-                cleanAppRestrictionsForPackageLAr(packageName, userId);
+                changed = cleanAppRestrictionsForPackageLAr(packageName, userId);
             } else {
                 // Write the restrictions to XML
                 writeApplicationRestrictionsLAr(packageName, restrictions, userId);
+                // TODO(b/154323615): avoid unnecessary broadcast when there is no change.
+                changed = true;
             }
         }
 
+        if (!changed) {
+            return;
+        }
+
         // Notify package of changes via an intent - only sent to explicitly registered receivers.
-        Intent changeIntent = new Intent(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED);
+        final Intent changeIntent = new Intent(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED);
         changeIntent.setPackage(packageName);
         changeIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
         mContext.sendBroadcastAsUser(changeIntent, UserHandle.of(userId));
@@ -4505,6 +4517,8 @@
             switch(cmd) {
                 case "list":
                     return runList(pw, shell);
+                case "list-missing-system-packages":
+                    return runListMissingSystemPackages(pw, shell);
                 default:
                     return shell.handleDefaultCommands(cmd);
             }
@@ -4571,6 +4585,30 @@
         }
     }
 
+    private int runListMissingSystemPackages(PrintWriter pw, Shell shell) {
+        boolean verbose = false;
+        boolean force = false;
+        String opt;
+        while ((opt = shell.getNextOption()) != null) {
+            switch (opt) {
+                case "-v":
+                    verbose = true;
+                    break;
+                case "--force":
+                    force = true;
+                    break;
+                default:
+                    pw.println("Invalid option: " + opt);
+                    return -1;
+            }
+        }
+
+        try (IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ")) {
+            mSystemPackageInstaller.dumpMissingSystemPackages(ipw, force, verbose);
+        }
+        return 0;
+    }
+
     @Override
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, pw)) return;
@@ -5143,6 +5181,9 @@
             pw.println("");
             pw.println("  list [-v] [-all]");
             pw.println("    Prints all users on the system.");
+            pw.println("  list-missing-system-packages [-v] [--force]");
+            pw.println("    Prints all system packages that were not explicitly configured to be "
+                    + "installed.");
         }
     }
 
diff --git a/services/core/java/com/android/server/pm/UserSystemPackageInstaller.java b/services/core/java/com/android/server/pm/UserSystemPackageInstaller.java
index 85c2306..cd1087f 100644
--- a/services/core/java/com/android/server/pm/UserSystemPackageInstaller.java
+++ b/services/core/java/com/android/server/pm/UserSystemPackageInstaller.java
@@ -22,15 +22,16 @@
 import android.annotation.UserIdInt;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
-import android.content.pm.PackageParser;
 import android.content.res.Resources;
 import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.util.ArrayMap;
 import android.util.ArraySet;
+import android.util.Pair;
 import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.LocalServices;
 import com.android.server.SystemConfig;
 import com.android.server.pm.parsing.pkg.AndroidPackage;
@@ -38,7 +39,9 @@
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -263,37 +266,85 @@
         if (!isLogMode(mode) && !isEnforceMode(mode)) {
             return;
         }
-        Slog.v(TAG,  "Checking that all system packages are whitelisted.");
-        final Set<String> allWhitelistedPackages = getWhitelistedSystemPackages();
-        PackageManagerInternal pmInt = LocalServices.getService(PackageManagerInternal.class);
-
-        // Check whether all whitelisted packages are indeed on the system.
-        for (String pkgName : allWhitelistedPackages) {
-            AndroidPackage pkg = pmInt.getPackage(pkgName);
-            if (pkg == null) {
-                Slog.w(TAG, pkgName + " is whitelisted but not present.");
-            } else if (!pkg.isSystem()) {
-                Slog.w(TAG, pkgName + " is whitelisted and present but not a system package.");
-            }
-        }
-
-        // Check whether all system packages are indeed whitelisted.
-        if (isImplicitWhitelistMode(mode) && !isLogMode(mode)) {
+        final List<Pair<Boolean, String>> warnings = checkSystemPackagesWhitelistWarnings(mode);
+        final int size = warnings.size();
+        if (size == 0) {
+            Slog.v(TAG, "checkWhitelistedSystemPackages(mode=" + mode + "): no warnings");
             return;
         }
-        final boolean doWtf = isEnforceMode(mode);
-        pmInt.forEachPackage(pkg -> {
-            if (pkg.isSystem() && !allWhitelistedPackages.contains(pkg.getManifestPackageName())) {
-                final String msg = "System package " + pkg.getManifestPackageName()
-                        + " is not whitelisted using 'install-in-user-type' in SystemConfig "
-                        + "for any user types!";
+
+        if (isImplicitWhitelistMode(mode) && !isLogMode(mode)) {
+            // Only shows whether all whitelisted packages are indeed on the system.
+            for (int i = 0; i < size; i++) {
+                final Pair<Boolean, String> pair = warnings.get(i);
+                final boolean isSevere = pair.first;
+                if (!isSevere) {
+                    final String msg = pair.second;
+                    Slog.w(TAG, msg);
+                }
+            }
+            return;
+        }
+
+        Slog.v(TAG, "checkWhitelistedSystemPackages(mode=" + mode + "): " + size + " warnings");
+        boolean doWtf = !isImplicitWhitelistMode(mode);
+        for (int i = 0; i < size; i++) {
+            final Pair<Boolean, String> pair = warnings.get(i);
+            final boolean isSevere = pair.first;
+            final String msg = pair.second;
+            if (isSevere) {
                 if (doWtf) {
                     Slog.wtf(TAG, msg);
                 } else {
                     Slog.e(TAG, msg);
                 }
+            } else {
+                Slog.w(TAG, msg);
+            }
+        }
+    }
+
+    // TODO: method below was created to refactor the one-time logging logic so it can be used on
+    // dump / cmd as well. It could to be further refactored (for example, creating a new
+    // structure for the warnings so it doesn't need a Pair).
+    /**
+     * Gets warnings for system user whitelisting.
+     *
+     * @return list of warnings, where {@code Pair.first} is the severity ({@code true} for WTF,
+     * {@code false} for WARN) and {@code Pair.second} the message.
+     */
+    @NonNull
+    private List<Pair<Boolean, String>> checkSystemPackagesWhitelistWarnings(
+            @PackageWhitelistMode int mode) {
+        final Set<String> allWhitelistedPackages = getWhitelistedSystemPackages();
+        final List<Pair<Boolean, String>> warnings = new ArrayList<>();
+        final PackageManagerInternal pmInt = LocalServices.getService(PackageManagerInternal.class);
+
+        // Check whether all whitelisted packages are indeed on the system.
+        final String notPresentFmt = "%s is whitelisted but not present.";
+        final String notSystemFmt = "%s is whitelisted and present but not a system package.";
+        for (String pkgName : allWhitelistedPackages) {
+            final AndroidPackage pkg = pmInt.getPackage(pkgName);
+            if (pkg == null) {
+                warnings.add(new Pair<>(false, String.format(notPresentFmt, pkgName)));
+            } else if (!pkg.isSystem()) {
+                warnings.add(new Pair<>(false, String.format(notSystemFmt, pkgName)));
+            }
+        }
+
+        // Check whether all system packages are indeed whitelisted.
+        final String logMessageFmt = "System package %s is not whitelisted using "
+                + "'install-in-user-type' in SystemConfig for any user types!";
+        final boolean isSevere = isEnforceMode(mode);
+        pmInt.forEachPackage(pkg -> {
+            if (!pkg.isSystem()) return;
+            final String pkgName = pkg.getManifestPackageName();
+            if (!allWhitelistedPackages.contains(pkgName)) {
+                warnings.add(new Pair<>(isSevere, String.format(logMessageFmt, pkgName)));
             }
         });
+
+        return warnings;
     }
 
     /** Whether to only install system packages in new users for which they are whitelisted. */
@@ -602,32 +653,45 @@
     }
 
     void dump(PrintWriter pw) {
-        final String prefix = "    ";
-        final String prefix2 = prefix + prefix;
+        try (IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "    ")) {
+            dumpIndented(ipw);
+        }
+    }
+
+    private void dumpIndented(IndentingPrintWriter pw) {
         final int mode = getWhitelistMode();
         pw.println("Whitelisted packages per user type");
-        pw.print(prefix); pw.print("Mode: ");
+
+        pw.increaseIndent();
+        pw.print("Mode: ");
         pw.print(mode);
         pw.print(isEnforceMode(mode) ? " (enforced)" : "");
         pw.print(isLogMode(mode) ? " (logged)" : "");
         pw.print(isImplicitWhitelistMode(mode) ? " (implicit)" : "");
         pw.print(isIgnoreOtaMode(mode) ? " (ignore OTAs)" : "");
         pw.println();
+        pw.decreaseIndent();
 
-        pw.print(prefix); pw.println("Legend");
+        pw.increaseIndent();
+        pw.println("Legend");
+        pw.increaseIndent();
         for (int idx = 0; idx < mUserTypes.length; idx++) {
-            pw.print(prefix2); pw.println(idx + " -> " + mUserTypes[idx]);
+            pw.println(idx + " -> " + mUserTypes[idx]);
         }
+        pw.decreaseIndent(); pw.decreaseIndent();
 
+        pw.increaseIndent();
         final int size = mWhitelistedPackagesForUserTypes.size();
         if (size == 0) {
-            pw.print(prefix); pw.println("No packages");
+            pw.println("No packages");
+            pw.decreaseIndent();
             return;
         }
-        pw.print(prefix); pw.print(size); pw.println(" packages:");
+        pw.print(size); pw.println(" packages:");
+        pw.increaseIndent();
         for (int pkgIdx = 0; pkgIdx < size; pkgIdx++) {
             final String pkgName = mWhitelistedPackagesForUserTypes.keyAt(pkgIdx);
-            pw.print(prefix2); pw.print(pkgName); pw.print(": ");
+            pw.print(pkgName); pw.print(": ");
             final long userTypesBitSet = mWhitelistedPackagesForUserTypes.valueAt(pkgIdx);
             for (int idx = 0; idx < mUserTypes.length; idx++) {
                 if ((userTypesBitSet & (1 << idx)) != 0) {
@@ -636,5 +700,40 @@
             }
             pw.println();
         }
+        pw.decreaseIndent(); pw.decreaseIndent();
+
+        pw.increaseIndent();
+        dumpMissingSystemPackages(pw, /* force= */ true, /* verbose= */ true);
+        pw.decreaseIndent();
+    }
+
+    void dumpMissingSystemPackages(IndentingPrintWriter pw, boolean force, boolean verbose) {
+        final int mode = getWhitelistMode();
+        final boolean show = force || (isEnforceMode(mode) && !isImplicitWhitelistMode(mode));
+        if (!show) return;
+
+        final List<Pair<Boolean, String>> warnings = checkSystemPackagesWhitelistWarnings(mode);
+        final int size = warnings.size();
+
+        if (size == 0) {
+            if (verbose) {
+                pw.println("All system packages are accounted for");
+            }
+            return;
+        }
+
+        if (verbose) {
+            pw.print(size); pw.println(" warnings for system user:");
+            pw.increaseIndent();
+        }
+        for (int i = 0; i < size; i++) {
+            final Pair<Boolean, String> pair = warnings.get(i);
+            final String lvl = pair.first ? "WTF" : "WARN";
+            final String msg = pair.second;
+            pw.print(lvl); pw.print(": "); pw.println(msg);
+        }
+        if (verbose) {
+            pw.decreaseIndent();
+        }
     }
 }
diff --git a/services/core/java/com/android/server/pm/parsing/PackageCacher.java b/services/core/java/com/android/server/pm/parsing/PackageCacher.java
index e5e1b0b..99c6dd1 100644
--- a/services/core/java/com/android/server/pm/parsing/PackageCacher.java
+++ b/services/core/java/com/android/server/pm/parsing/PackageCacher.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.content.pm.PackageParserCacheHelper;
+import android.os.FileUtils;
 import android.os.Parcel;
 import android.system.ErrnoException;
 import android.system.Os;
@@ -197,4 +198,18 @@
             Slog.w(TAG, "Error saving package cache.", e);
         }
     }
+
+    /**
+     * Delete the cache files for the given {@code packageFile}.
+     */
+    public void cleanCachedResult(@NonNull File packageFile) {
+        final String packageName = packageFile.getName();
+        final File[] files = FileUtils.listFilesOrEmpty(mCacheDir,
+                (dir, name) -> name.startsWith(packageName));
+        for (File file : files) {
+            if (!file.delete()) {
+                Slog.e(TAG, "Unable to clean cache file: " + file);
+            }
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java b/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java
index 780b234..4a1a6a7 100644
--- a/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java
+++ b/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java
@@ -31,6 +31,7 @@
 import android.content.pm.parsing.component.ParsedInstrumentation;
 import android.content.pm.parsing.component.ParsedProvider;
 import android.content.pm.parsing.component.ParsedService;
+import android.os.incremental.IncrementalManager;
 import android.text.TextUtils;
 
 import com.android.internal.content.NativeLibraryHelper;
@@ -141,8 +142,15 @@
 
     public static boolean canHaveOatDir(AndroidPackage pkg, boolean isUpdatedSystemApp) {
         // The following app types CANNOT have oat directory
-        // - non-updated system apps
-        return !pkg.isSystem() || isUpdatedSystemApp;
+        // - non-updated system apps,
+        // - incrementally installed apps.
+        if (pkg.isSystem() && !isUpdatedSystemApp) {
+            return false;
+        }
+        if (IncrementalManager.isIncrementalPath(pkg.getCodePath())) {
+            return false;
+        }
+        return true;
     }
 
     public static boolean hasComponentClassName(AndroidPackage pkg, String className) {
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 4624e9e..2f84a99 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -62,6 +62,8 @@
 import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION;
 import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
 import static android.view.WindowManager.LayoutParams.isSystemAlertWindowType;
+import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD;
+import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER;
 import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN;
 import static android.view.WindowManager.TAKE_SCREENSHOT_SELECTED_REGION;
 import static android.view.WindowManagerGlobal.ADD_OKAY;
@@ -1328,6 +1330,7 @@
                 mScreenshotChordVolumeDownKeyConsumed = true;
                 cancelPendingPowerKeyAction();
                 mScreenshotRunnable.setScreenshotType(TAKE_SCREENSHOT_FULLSCREEN);
+                mScreenshotRunnable.setScreenshotSource(SCREENSHOT_KEY_CHORD);
                 mHandler.postDelayed(mScreenshotRunnable, getScreenshotChordLongPressDelay());
             }
         }
@@ -1411,14 +1414,19 @@
 
     private class ScreenshotRunnable implements Runnable {
         private int mScreenshotType = TAKE_SCREENSHOT_FULLSCREEN;
+        private int mScreenshotSource = SCREENSHOT_KEY_OTHER;
 
         public void setScreenshotType(int screenshotType) {
             mScreenshotType = screenshotType;
         }
 
+        public void setScreenshotSource(int screenshotSource) {
+            mScreenshotSource = screenshotSource;
+        }
+
         @Override
         public void run() {
-            mDefaultDisplayPolicy.takeScreenshot(mScreenshotType);
+            mDefaultDisplayPolicy.takeScreenshot(mScreenshotType, mScreenshotSource);
         }
     }
 
@@ -2693,6 +2701,7 @@
                 int type = event.isShiftPressed() ? TAKE_SCREENSHOT_SELECTED_REGION
                         : TAKE_SCREENSHOT_FULLSCREEN;
                 mScreenshotRunnable.setScreenshotType(type);
+                mScreenshotRunnable.setScreenshotSource(SCREENSHOT_KEY_OTHER);
                 mHandler.post(mScreenshotRunnable);
                 return -1;
             }
@@ -2709,6 +2718,7 @@
         } else if (keyCode == KeyEvent.KEYCODE_SYSRQ) {
             if (down && repeatCount == 0) {
                 mScreenshotRunnable.setScreenshotType(TAKE_SCREENSHOT_FULLSCREEN);
+                mScreenshotRunnable.setScreenshotSource(SCREENSHOT_KEY_OTHER);
                 mHandler.post(mScreenshotRunnable);
             }
             return -1;
diff --git a/services/core/java/com/android/server/power/AttentionDetector.java b/services/core/java/com/android/server/power/AttentionDetector.java
index 1ab6ade..b69c450 100644
--- a/services/core/java/com/android/server/power/AttentionDetector.java
+++ b/services/core/java/com/android/server/power/AttentionDetector.java
@@ -17,7 +17,6 @@
 package com.android.server.power;
 
 import static android.provider.DeviceConfig.NAMESPACE_ATTENTION_MANAGER_SERVICE;
-import static android.provider.Settings.Secure.ADAPTIVE_SLEEP;
 
 import android.Manifest;
 import android.app.ActivityManager;
@@ -192,9 +191,6 @@
         }
 
         if (!isAttentionServiceSupported() || !serviceHasSufficientPermissions()) {
-            // Turns off adaptive sleep in settings for all users if attention service is not
-            // available. The setting itself should also be grayed out in this case.
-            Settings.Secure.putInt(mContentResolver, ADAPTIVE_SLEEP, 0);
             return nextScreenDimming;
         }
 
diff --git a/services/core/java/com/android/server/power/batterysaver/BatterySaverPolicy.java b/services/core/java/com/android/server/power/batterysaver/BatterySaverPolicy.java
index 059861b..701197e 100644
--- a/services/core/java/com/android/server/power/batterysaver/BatterySaverPolicy.java
+++ b/services/core/java/com/android/server/power/batterysaver/BatterySaverPolicy.java
@@ -214,7 +214,7 @@
      * required adjustments.
      */
     @GuardedBy("mLock")
-    private Policy mEffectivePolicy = OFF_POLICY;
+    private Policy mEffectivePolicyRaw = OFF_POLICY;
 
     @IntDef(prefix = {"POLICY_LEVEL_"}, value = {
             POLICY_LEVEL_OFF,
@@ -228,12 +228,8 @@
     static final int POLICY_LEVEL_ADAPTIVE = 1;
     static final int POLICY_LEVEL_FULL = 2;
 
-    /**
-     * Do not access directly; always use {@link #setPolicyLevel}
-     * and {@link #getPolicyLevelLocked}
-     */
     @GuardedBy("mLock")
-    private int mPolicyLevelRaw = POLICY_LEVEL_OFF;
+    private int mPolicyLevel = POLICY_LEVEL_OFF;
 
     private final Context mContext;
     private final ContentResolver mContentResolver;
@@ -338,7 +334,7 @@
     private void maybeNotifyListenersOfPolicyChange() {
         final BatterySaverPolicyListener[] listeners;
         synchronized (mLock) {
-            if (getPolicyLevelLocked() == POLICY_LEVEL_OFF) {
+            if (mPolicyLevel == POLICY_LEVEL_OFF) {
                 // Current policy is OFF, so there's no change to notify listeners of.
                 return;
             }
@@ -428,14 +424,14 @@
         boolean changed = false;
         Policy newFullPolicy = Policy.fromSettings(setting, deviceSpecificSetting,
                 DEFAULT_FULL_POLICY);
-        if (getPolicyLevelLocked() == POLICY_LEVEL_FULL && !mFullPolicy.equals(newFullPolicy)) {
+        if (mPolicyLevel == POLICY_LEVEL_FULL && !mFullPolicy.equals(newFullPolicy)) {
             changed = true;
         }
         mFullPolicy = newFullPolicy;
 
         mDefaultAdaptivePolicy = Policy.fromSettings(adaptiveSetting, adaptiveDeviceSpecificSetting,
                 DEFAULT_ADAPTIVE_POLICY);
-        if (getPolicyLevelLocked() == POLICY_LEVEL_ADAPTIVE
+        if (mPolicyLevel == POLICY_LEVEL_ADAPTIVE
                 && !mAdaptivePolicy.equals(mDefaultAdaptivePolicy)) {
             changed = true;
         }
@@ -451,8 +447,9 @@
     @GuardedBy("mLock")
     private void updatePolicyDependenciesLocked() {
         final Policy rawPolicy = getCurrentRawPolicyLocked();
-
         final int locationMode;
+
+        invalidatePowerSaveModeCaches();
         if (mCarModeEnabled
                 && rawPolicy.locationMode != PowerManager.LOCATION_MODE_NO_CHANGE
                 && rawPolicy.locationMode != PowerManager.LOCATION_MODE_FOREGROUND_ONLY) {
@@ -461,7 +458,8 @@
         } else {
             locationMode = rawPolicy.locationMode;
         }
-        mEffectivePolicy = new Policy(
+
+        mEffectivePolicyRaw = new Policy(
                 rawPolicy.adjustBrightnessFactor,
                 rawPolicy.advertiseIsEnabled,
                 rawPolicy.deferFullBackup,
@@ -489,24 +487,24 @@
 
         final StringBuilder sb = new StringBuilder();
 
-        if (mEffectivePolicy.forceAllAppsStandby) sb.append("A");
-        if (mEffectivePolicy.forceBackgroundCheck) sb.append("B");
+        if (mEffectivePolicyRaw.forceAllAppsStandby) sb.append("A");
+        if (mEffectivePolicyRaw.forceBackgroundCheck) sb.append("B");
 
-        if (mEffectivePolicy.disableVibration) sb.append("v");
-        if (mEffectivePolicy.disableAnimation) sb.append("a");
-        if (mEffectivePolicy.disableSoundTrigger) sb.append("s");
-        if (mEffectivePolicy.deferFullBackup) sb.append("F");
-        if (mEffectivePolicy.deferKeyValueBackup) sb.append("K");
-        if (mEffectivePolicy.enableFirewall) sb.append("f");
-        if (mEffectivePolicy.enableDataSaver) sb.append("d");
-        if (mEffectivePolicy.enableAdjustBrightness) sb.append("b");
+        if (mEffectivePolicyRaw.disableVibration) sb.append("v");
+        if (mEffectivePolicyRaw.disableAnimation) sb.append("a");
+        if (mEffectivePolicyRaw.disableSoundTrigger) sb.append("s");
+        if (mEffectivePolicyRaw.deferFullBackup) sb.append("F");
+        if (mEffectivePolicyRaw.deferKeyValueBackup) sb.append("K");
+        if (mEffectivePolicyRaw.enableFirewall) sb.append("f");
+        if (mEffectivePolicyRaw.enableDataSaver) sb.append("d");
+        if (mEffectivePolicyRaw.enableAdjustBrightness) sb.append("b");
 
-        if (mEffectivePolicy.disableLaunchBoost) sb.append("l");
-        if (mEffectivePolicy.disableOptionalSensors) sb.append("S");
-        if (mEffectivePolicy.disableAod) sb.append("o");
-        if (mEffectivePolicy.enableQuickDoze) sb.append("q");
+        if (mEffectivePolicyRaw.disableLaunchBoost) sb.append("l");
+        if (mEffectivePolicyRaw.disableOptionalSensors) sb.append("S");
+        if (mEffectivePolicyRaw.disableAod) sb.append("o");
+        if (mEffectivePolicyRaw.enableQuickDoze) sb.append("q");
 
-        sb.append(mEffectivePolicy.locationMode);
+        sb.append(mEffectivePolicyRaw.locationMode);
 
         mEventLogKeys = sb.toString();
     }
@@ -969,14 +967,14 @@
      */
     boolean setPolicyLevel(@PolicyLevel int level) {
         synchronized (mLock) {
-            if (getPolicyLevelLocked() == level) {
+            if (mPolicyLevel == level) {
                 return false;
             }
             switch (level) {
                 case POLICY_LEVEL_FULL:
                 case POLICY_LEVEL_ADAPTIVE:
                 case POLICY_LEVEL_OFF:
-                    setPolicyLevelLocked(level);
+                    mPolicyLevel = level;
                     break;
                 default:
                     Slog.wtf(TAG, "setPolicyLevel invalid level given: " + level);
@@ -998,7 +996,7 @@
         }
 
         mAdaptivePolicy = p;
-        if (getPolicyLevelLocked() == POLICY_LEVEL_ADAPTIVE) {
+        if (mPolicyLevel == POLICY_LEVEL_ADAPTIVE) {
             updatePolicyDependenciesLocked();
             return true;
         }
@@ -1011,11 +1009,11 @@
     }
 
     private Policy getCurrentPolicyLocked() {
-        return mEffectivePolicy;
+        return mEffectivePolicyRaw;
     }
 
     private Policy getCurrentRawPolicyLocked() {
-        switch (getPolicyLevelLocked()) {
+        switch (mPolicyLevel) {
             case POLICY_LEVEL_FULL:
                 return mFullPolicy;
             case POLICY_LEVEL_ADAPTIVE:
@@ -1077,12 +1075,12 @@
 
             pw.println("  mAccessibilityEnabled=" + mAccessibilityEnabled);
             pw.println("  mCarModeEnabled=" + mCarModeEnabled);
-            pw.println("  mPolicyLevel=" + getPolicyLevelLocked());
+            pw.println("  mPolicyLevel=" + mPolicyLevel);
 
             dumpPolicyLocked(pw, "  ", "full", mFullPolicy);
             dumpPolicyLocked(pw, "  ", "default adaptive", mDefaultAdaptivePolicy);
             dumpPolicyLocked(pw, "  ", "current adaptive", mAdaptivePolicy);
-            dumpPolicyLocked(pw, "  ", "effective", mEffectivePolicy);
+            dumpPolicyLocked(pw, "  ", "effective", mEffectivePolicyRaw);
         }
     }
 
@@ -1170,20 +1168,4 @@
             }
         }
     }
-
-    /** Non-blocking getter exists as a reminder not to modify cached fields directly */
-    @GuardedBy("mLock")
-    private int getPolicyLevelLocked() {
-        return mPolicyLevelRaw;
-    }
-
-    @GuardedBy("mLock")
-    private void setPolicyLevelLocked(int level) {
-        if (mPolicyLevelRaw == level) {
-            return;
-        }
-        // Under lock, invalidate before set ensures caches won't return stale values.
-        invalidatePowerSaveModeCaches();
-        mPolicyLevelRaw = level;
-    }
 }
diff --git a/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java b/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java
index 801d75b..f6d46e2 100644
--- a/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java
+++ b/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java
@@ -25,6 +25,7 @@
 import android.content.IntentFilter;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
 import android.content.pm.VersionedPackage;
 import android.content.rollback.PackageRollbackInfo;
 import android.content.rollback.RollbackInfo;
@@ -41,10 +42,13 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.FrameworkStatsLog;
+import com.android.server.LocalServices;
 import com.android.server.PackageWatchdog;
 import com.android.server.PackageWatchdog.FailureReasons;
 import com.android.server.PackageWatchdog.PackageHealthObserver;
 import com.android.server.PackageWatchdog.PackageHealthObserverImpact;
+import com.android.server.pm.ApexManager;
+import com.android.server.pm.parsing.pkg.AndroidPackage;
 
 import java.io.BufferedReader;
 import java.io.File;
@@ -71,6 +75,7 @@
 
     private final Context mContext;
     private final Handler mHandler;
+    private final ApexManager mApexManager;
     private final File mLastStagedRollbackIdsFile;
     // Staged rollback ids that have been committed but their session is not yet ready
     @GuardedBy("mPendingStagedRollbackIds")
@@ -85,6 +90,7 @@
         dataDir.mkdirs();
         mLastStagedRollbackIdsFile = new File(dataDir, "last-staged-rollback-ids");
         PackageWatchdog.getInstance(mContext).registerHealthObserver(this);
+        mApexManager = ApexManager.getInstance();
     }
 
     @Override
@@ -302,6 +308,18 @@
      * Returns true if the package name is the name of a module.
      */
     private boolean isModule(String packageName) {
+        // Check if the package is an APK inside an APEX. If it is, use the parent APEX package when
+        // querying PackageManager.
+        PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class);
+        AndroidPackage apkPackage = pmi.getPackage(packageName);
+        if (apkPackage != null) {
+            String apexPackageName = mApexManager.getActiveApexPackageNameContainingPackage(
+                    apkPackage);
+            if (apexPackageName != null) {
+                packageName = apexPackageName;
+            }
+        }
+
         PackageManager pm = mContext.getPackageManager();
         try {
             return pm.getModuleInfo(packageName, 0) != null;
diff --git a/services/core/java/com/android/server/tv/tunerresourcemanager/CasResource.java b/services/core/java/com/android/server/tv/tunerresourcemanager/CasResource.java
new file mode 100644
index 0000000..54ad1d2
--- /dev/null
+++ b/services/core/java/com/android/server/tv/tunerresourcemanager/CasResource.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.tv.tunerresourcemanager;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A Cas resource object used by the Tuner Resource Manager to record the cas
+ * information.
+ *
+ * @hide
+ */
+public final class CasResource {
+
+    private final int mSystemId;
+
+    private int mMaxSessionNum;
+
+    private int mAvailableSessionNum;
+
+    /**
+     * The owner clients' ids when part of the Cas is occupied.
+     */
+    private Map<Integer, Integer> mOwnerClientIdsToSessionNum = new HashMap<>();
+
+    private CasResource(Builder builder) {
+        this.mSystemId = builder.mSystemId;
+        this.mMaxSessionNum = builder.mMaxSessionNum;
+        this.mAvailableSessionNum = builder.mMaxSessionNum;
+    }
+
+    public int getSystemId() {
+        return mSystemId;
+    }
+
+    public int getMaxSessionNum() {
+        return mMaxSessionNum;
+    }
+
+    public int getUsedSessionNum() {
+        return (mMaxSessionNum - mAvailableSessionNum);
+    }
+
+    public boolean isFullyUsed() {
+        return mAvailableSessionNum == 0;
+    }
+
+    /**
+     * Update max session number.
+     *
+     * @param maxSessionNum the new max session num.
+     */
+    public void updateMaxSessionNum(int maxSessionNum) {
+        mAvailableSessionNum = Math.max(
+                0, mAvailableSessionNum + (maxSessionNum - mMaxSessionNum));
+        mMaxSessionNum = maxSessionNum;
+    }
+
+    /**
+     * Set an owner for the cas
+     *
+     * @param ownerId the client id of the owner.
+     */
+    public void setOwner(int ownerId) {
+        int sessionNum = mOwnerClientIdsToSessionNum.get(ownerId) == null
+                ? 1 : (mOwnerClientIdsToSessionNum.get(ownerId) + 1);
+        mOwnerClientIdsToSessionNum.put(ownerId, sessionNum);
+        mAvailableSessionNum--;
+    }
+
+    /**
+     * Remove an owner of the Cas.
+     *
+     * @param ownerId the removing client id of the owner.
+     */
+    public void removeOwner(int ownerId) {
+        mAvailableSessionNum += mOwnerClientIdsToSessionNum.get(ownerId);
+        mOwnerClientIdsToSessionNum.remove(ownerId);
+    }
+
+    public Set<Integer> getOwnerClientIds() {
+        return mOwnerClientIdsToSessionNum.keySet();
+    }
+
+    @Override
+    public String toString() {
+        return "CasResource[systemId=" + this.mSystemId
+                + ", isFullyUsed=" + (this.mAvailableSessionNum == 0)
+                + ", maxSessionNum=" + this.mMaxSessionNum
+                + ", ownerClients=" + ownersMapToString() + "]";
+    }
+
+    /**
+     * Builder class for {@link CasResource}.
+     */
+    public static class Builder {
+
+        private int mSystemId;
+        private int mMaxSessionNum;
+
+        Builder(int systemId) {
+            this.mSystemId = systemId;
+        }
+
+        /**
+         * Builder for {@link CasResource}.
+         *
+         * @param maxSessionNum the max session num the current Cas has.
+         */
+        public Builder maxSessionNum(int maxSessionNum) {
+            this.mMaxSessionNum = maxSessionNum;
+            return this;
+        }
+
+        /**
+         * Build a {@link CasResource}.
+         *
+         * @return {@link CasResource}.
+         */
+        public CasResource build() {
+            CasResource cas = new CasResource(this);
+            return cas;
+        }
+    }
+
+    private String ownersMapToString() {
+        StringBuilder string = new StringBuilder("{");
+        for (int clienId : mOwnerClientIdsToSessionNum.keySet()) {
+            string.append(" clientId=")
+                  .append(clienId)
+                  .append(", owns session num=")
+                  .append(mOwnerClientIdsToSessionNum.get(clienId))
+                  .append(",");
+        }
+        return string.append("}").toString();
+    }
+}
diff --git a/services/core/java/com/android/server/tv/tunerresourcemanager/ClientProfile.java b/services/core/java/com/android/server/tv/tunerresourcemanager/ClientProfile.java
index 4cdc172..2b0fe8a 100644
--- a/services/core/java/com/android/server/tv/tunerresourcemanager/ClientProfile.java
+++ b/services/core/java/com/android/server/tv/tunerresourcemanager/ClientProfile.java
@@ -27,6 +27,7 @@
 public final class ClientProfile {
 
     public static final int INVALID_GROUP_ID = -1;
+    public static final int INVALID_RESOURCE_ID = -1;
 
     /**
      * Client id sent to the client when registering with
@@ -56,7 +57,6 @@
      * also lose their resources.
      */
     private int mGroupId = INVALID_GROUP_ID;
-
     /**
      * Optional nice value for TRM to reduce client’s priority.
      */
@@ -73,6 +73,11 @@
     private Set<Integer> mUsingLnbIds = new HashSet<>();
 
     /**
+     * List of the Cas system ids that are used by the current client.
+     */
+    private int mUsingCasSystemId = INVALID_RESOURCE_ID;
+
+    /**
      * Optional arbitrary priority value given by the client.
      *
      * <p>This value can override the default priorotiy calculated from
@@ -172,11 +177,32 @@
     }
 
     /**
+     * Set when the client starts to use a Cas system.
+     *
+     * @param casSystemId cas being used.
+     */
+    public void useCas(int casSystemId) {
+        mUsingCasSystemId = casSystemId;
+    }
+
+    public int getInUseCasSystemId() {
+        return mUsingCasSystemId;
+    }
+
+    /**
+     * Called when the client released a Cas System.
+     */
+    public void releaseCas() {
+        mUsingCasSystemId = INVALID_RESOURCE_ID;
+    }
+
+    /**
      * Called to reclaim all the resources being used by the current client.
      */
     public void reclaimAllResources() {
         mUsingFrontendIds.clear();
         mUsingLnbIds.clear();
+        mUsingCasSystemId = INVALID_RESOURCE_ID;
     }
 
     @Override
diff --git a/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java b/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java
index 7231813..2f70840 100644
--- a/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java
+++ b/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java
@@ -65,6 +65,8 @@
     private Map<Integer, FrontendResource> mFrontendResources = new HashMap<>();
     // Map of the current available lnb resources
     private Map<Integer, LnbResource> mLnbResources = new HashMap<>();
+    // Map of the current available Cas resources
+    private Map<Integer, CasResource> mCasResources = new HashMap<>();
 
     @GuardedBy("mLock")
     private Map<Integer, ResourcesReclaimListenerRecord> mListeners = new HashMap<>();
@@ -158,10 +160,8 @@
         @Override
         public void updateCasInfo(int casSystemId, int maxSessionNum) {
             enforceTrmAccessPermission("updateCasInfo");
-            if (DEBUG) {
-                Slog.d(TAG,
-                        "updateCasInfo(casSystemId=" + casSystemId
-                                + ", maxSessionNum=" + maxSessionNum + ")");
+            synchronized (mLock) {
+                updateCasInfoInternal(casSystemId, maxSessionNum);
             }
         }
 
@@ -185,11 +185,11 @@
                 throw new RemoteException("frontendHandle can't be null");
             }
             synchronized (mLock) {
-                try {
-                    return requestFrontendInternal(request, frontendHandle);
-                } catch (RemoteException e) {
-                    throw e.rethrowFromSystemServer();
+                if (!checkClientExists(request.getClientId())) {
+                    throw new RemoteException("Request frontend from unregistered client:"
+                            + request.getClientId());
                 }
+                return requestFrontendInternal(request, frontendHandle);
             }
         }
 
@@ -211,32 +211,45 @@
                 throw new RemoteException("demuxHandle can't be null");
             }
             synchronized (mLock) {
+                if (!checkClientExists(request.getClientId())) {
+                    throw new RemoteException("Request demux from unregistered client:"
+                            + request.getClientId());
+                }
                 return requestDemuxInternal(request, demuxHandle);
             }
         }
 
         @Override
         public boolean requestDescrambler(@NonNull TunerDescramblerRequest request,
-                    @NonNull int[] descrambleHandle) throws RemoteException {
+                    @NonNull int[] descramblerHandle) throws RemoteException {
             enforceDescramblerAccessPermission("requestDescrambler");
             enforceTrmAccessPermission("requestDescrambler");
-            if (descrambleHandle == null) {
-                throw new RemoteException("descrambleHandle can't be null");
+            if (descramblerHandle == null) {
+                throw new RemoteException("descramblerHandle can't be null");
             }
             synchronized (mLock) {
-                return requestDescramblerInternal(request, descrambleHandle);
+                if (!checkClientExists(request.getClientId())) {
+                    throw new RemoteException("Request descrambler from unregistered client:"
+                            + request.getClientId());
+                }
+                return requestDescramblerInternal(request, descramblerHandle);
             }
         }
 
         @Override
-        public boolean requestCasSession(
-                @NonNull CasSessionRequest request, @NonNull int[] sessionResourceHandle) {
+        public boolean requestCasSession(@NonNull CasSessionRequest request,
+                @NonNull int[] casSessionHandle) throws RemoteException {
             enforceTrmAccessPermission("requestCasSession");
-            if (DEBUG) {
-                Slog.d(TAG, "requestCasSession(request=" + request + ")");
+            if (casSessionHandle == null) {
+                throw new RemoteException("casSessionHandle can't be null");
             }
-
-            return true;
+            synchronized (mLock) {
+                if (!checkClientExists(request.getClientId())) {
+                    throw new RemoteException("Request cas from unregistered client:"
+                            + request.getClientId());
+                }
+                return requestCasSessionInternal(request, casSessionHandle);
+            }
         }
 
         @Override
@@ -248,11 +261,11 @@
                 throw new RemoteException("lnbHandle can't be null");
             }
             synchronized (mLock) {
-                try {
-                    return requestLnbInternal(request, lnbHandle);
-                } catch (RemoteException e) {
-                    throw e.rethrowFromSystemServer();
+                if (!checkClientExists(request.getClientId())) {
+                    throw new RemoteException("Request lnb from unregistered client:"
+                            + request.getClientId());
                 }
+                return requestLnbInternal(request, lnbHandle);
             }
         }
 
@@ -264,15 +277,20 @@
                     frontendHandle)) {
                 throw new RemoteException("frontendHandle can't be invalid");
             }
-            int frontendId = getResourceIdFromHandle(frontendHandle);
-            FrontendResource fe = getFrontendResource(frontendId);
-            if (fe == null) {
-                throw new RemoteException("Releasing frontend does not exist.");
-            }
-            if (fe.getOwnerClientId() != clientId) {
-                throw new RemoteException("Client is not the current owner of the releasing fe.");
-            }
             synchronized (mLock) {
+                if (!checkClientExists(clientId)) {
+                    throw new RemoteException("Release frontend from unregistered client:"
+                            + clientId);
+                }
+                int frontendId = getResourceIdFromHandle(frontendHandle);
+                FrontendResource fe = getFrontendResource(frontendId);
+                if (fe == null) {
+                    throw new RemoteException("Releasing frontend does not exist.");
+                }
+                if (fe.getOwnerClientId() != clientId) {
+                    throw new RemoteException(
+                            "Client is not the current owner of the releasing fe.");
+                }
                 releaseFrontendInternal(fe);
             }
         }
@@ -296,10 +314,26 @@
         }
 
         @Override
-        public void releaseCasSession(int sessionResourceId, int clientId) {
+        public void releaseCasSession(int casSessionHandle, int clientId) throws RemoteException {
             enforceTrmAccessPermission("releaseCasSession");
-            if (DEBUG) {
-                Slog.d(TAG, "releaseCasSession(sessionResourceId=" + sessionResourceId + ")");
+            if (!validateResourceHandle(
+                    TunerResourceManager.TUNER_RESOURCE_TYPE_CAS_SESSION, casSessionHandle)) {
+                throw new RemoteException("casSessionHandle can't be invalid");
+            }
+            synchronized (mLock) {
+                if (!checkClientExists(clientId)) {
+                    throw new RemoteException("Release cas from unregistered client:" + clientId);
+                }
+                int casSystemId = getClientProfile(clientId).getInUseCasSystemId();
+                CasResource cas = getCasResource(casSystemId);
+                if (cas == null) {
+                    throw new RemoteException("Releasing cas does not exist.");
+                }
+                if (!cas.getOwnerClientIds().contains(clientId)) {
+                    throw new RemoteException(
+                            "Client is not the current owner of the releasing cas.");
+                }
+                releaseCasSessionInternal(cas, clientId);
             }
         }
 
@@ -310,6 +344,9 @@
             if (!validateResourceHandle(TunerResourceManager.TUNER_RESOURCE_TYPE_LNB, lnbHandle)) {
                 throw new RemoteException("lnbHandle can't be invalid");
             }
+            if (!checkClientExists(clientId)) {
+                throw new RemoteException("Release lnb from unregistered client:" + clientId);
+            }
             int lnbId = getResourceIdFromHandle(lnbHandle);
             LnbResource lnb = getLnbResource(lnbId);
             if (lnb == null) {
@@ -465,17 +502,42 @@
     }
 
     @VisibleForTesting
-    protected boolean requestFrontendInternal(TunerFrontendRequest request, int[] frontendHandle)
-            throws RemoteException {
+    protected void updateCasInfoInternal(int casSystemId, int maxSessionNum) {
+        if (DEBUG) {
+            Slog.d(TAG,
+                    "updateCasInfo(casSystemId=" + casSystemId
+                            + ", maxSessionNum=" + maxSessionNum + ")");
+        }
+        // If maxSessionNum is 0, removing the Cas Resource.
+        if (maxSessionNum == 0) {
+            removeCasResource(casSystemId);
+            return;
+        }
+        // If the Cas exists, updates the Cas Resource accordingly.
+        CasResource cas = getCasResource(casSystemId);
+        if (cas != null) {
+            if (cas.getUsedSessionNum() > maxSessionNum) {
+                // Sort and release the short number of Cas resources.
+                int releasingCasResourceNum = cas.getUsedSessionNum() - maxSessionNum;
+                releaseLowerPriorityClientCasResources(releasingCasResourceNum);
+            }
+            cas.updateMaxSessionNum(maxSessionNum);
+            return;
+        }
+        // Add the new Cas Resource.
+        cas = new CasResource.Builder(casSystemId)
+                             .maxSessionNum(maxSessionNum)
+                             .build();
+        addCasResource(cas);
+    }
+
+    @VisibleForTesting
+    protected boolean requestFrontendInternal(TunerFrontendRequest request, int[] frontendHandle) {
         if (DEBUG) {
             Slog.d(TAG, "requestFrontend(request=" + request + ")");
         }
 
         frontendHandle[0] = TunerResourceManager.INVALID_RESOURCE_HANDLE;
-        if (!checkClientExists(request.getClientId())) {
-            Slog.e(TAG, "Request frontend from unregistered client:" + request.getClientId());
-            return false;
-        }
         ClientProfile requestClient = getClientProfile(request.getClientId());
         int grantingFrontendId = -1;
         int inUseLowestPriorityFrId = -1;
@@ -496,7 +558,7 @@
                 } else if (grantingFrontendId < 0) {
                     // Record the frontend id with the lowest client priority among all the
                     // in use frontends when no available frontend has been found.
-                    int priority = getOwnerClientPriority(fr);
+                    int priority = getOwnerClientPriority(fr.getOwnerClientId());
                     if (currentLowestPriority > priority) {
                         inUseLowestPriorityFrId = fr.getId();
                         currentLowestPriority = priority;
@@ -530,17 +592,12 @@
     }
 
     @VisibleForTesting
-    protected boolean requestLnbInternal(TunerLnbRequest request, int[] lnbHandle)
-            throws RemoteException {
+    protected boolean requestLnbInternal(TunerLnbRequest request, int[] lnbHandle) {
         if (DEBUG) {
             Slog.d(TAG, "requestLnb(request=" + request + ")");
         }
 
         lnbHandle[0] = TunerResourceManager.INVALID_RESOURCE_HANDLE;
-        if (!checkClientExists(request.getClientId())) {
-            Slog.e(TAG, "Request lnb from unregistered client:" + request.getClientId());
-            return false;
-        }
         ClientProfile requestClient = getClientProfile(request.getClientId());
         int grantingLnbId = -1;
         int inUseLowestPriorityLnbId = -1;
@@ -554,7 +611,7 @@
             } else {
                 // Record the lnb id with the lowest client priority among all the
                 // in use lnb when no available lnb has been found.
-                int priority = getOwnerClientPriority(lnb);
+                int priority = getOwnerClientPriority(lnb.getOwnerClientId());
                 if (currentLowestPriority > priority) {
                     inUseLowestPriorityLnbId = lnb.getId();
                     currentLowestPriority = priority;
@@ -588,7 +645,55 @@
     }
 
     @VisibleForTesting
-    void releaseFrontendInternal(FrontendResource fe) {
+    protected boolean requestCasSessionInternal(CasSessionRequest request, int[] casSessionHandle) {
+        if (DEBUG) {
+            Slog.d(TAG, "requestCasSession(request=" + request + ")");
+        }
+        CasResource cas = getCasResource(request.getCasSystemId());
+        // Unregistered Cas System is treated as having unlimited sessions.
+        if (cas == null) {
+            cas = new CasResource.Builder(request.getCasSystemId())
+                                 .maxSessionNum(Integer.MAX_VALUE)
+                                 .build();
+            addCasResource(cas);
+        }
+        casSessionHandle[0] = TunerResourceManager.INVALID_RESOURCE_HANDLE;
+        ClientProfile requestClient = getClientProfile(request.getClientId());
+        int lowestPriorityOwnerId = -1;
+        // Priority max value is 1000
+        int currentLowestPriority = MAX_CLIENT_PRIORITY + 1;
+        if (!cas.isFullyUsed()) {
+            casSessionHandle[0] = generateResourceHandle(
+                    TunerResourceManager.TUNER_RESOURCE_TYPE_CAS_SESSION, cas.getSystemId());
+            updateCasClientMappingOnNewGrant(request.getCasSystemId(), request.getClientId());
+            return true;
+        }
+        for (int ownerId : cas.getOwnerClientIds()) {
+            // Record the client id with lowest priority that is using the current Cas system.
+            int priority = getOwnerClientPriority(ownerId);
+            if (currentLowestPriority > priority) {
+                lowestPriorityOwnerId = ownerId;
+                currentLowestPriority = priority;
+            }
+        }
+
+        // When all the Cas sessions are occupied, reclaim the lowest priority client if the
+        // request client has higher priority.
+        if (lowestPriorityOwnerId > -1 && (requestClient.getPriority() > currentLowestPriority)) {
+            if (!reclaimResource(lowestPriorityOwnerId,
+                    TunerResourceManager.TUNER_RESOURCE_TYPE_CAS_SESSION)) {
+                return false;
+            }
+            casSessionHandle[0] = generateResourceHandle(
+                    TunerResourceManager.TUNER_RESOURCE_TYPE_CAS_SESSION, cas.getSystemId());
+            updateCasClientMappingOnNewGrant(request.getCasSystemId(), request.getClientId());
+            return true;
+        }
+        return false;
+    }
+
+    @VisibleForTesting
+    protected void releaseFrontendInternal(FrontendResource fe) {
         if (DEBUG) {
             Slog.d(TAG, "releaseFrontend(id=" + fe.getId() + ")");
         }
@@ -596,7 +701,7 @@
     }
 
     @VisibleForTesting
-    void releaseLnbInternal(LnbResource lnb) {
+    protected void releaseLnbInternal(LnbResource lnb) {
         if (DEBUG) {
             Slog.d(TAG, "releaseLnb(lnbId=" + lnb.getId() + ")");
         }
@@ -604,7 +709,15 @@
     }
 
     @VisibleForTesting
-    boolean requestDemuxInternal(TunerDemuxRequest request, int[] demuxHandle) {
+    protected void releaseCasSessionInternal(CasResource cas, int ownerClientId) {
+        if (DEBUG) {
+            Slog.d(TAG, "releaseCasSession(sessionResourceId=" + cas.getSystemId() + ")");
+        }
+        updateCasClientMappingOnRelease(cas, ownerClientId);
+    }
+
+    @VisibleForTesting
+    protected boolean requestDemuxInternal(TunerDemuxRequest request, int[] demuxHandle) {
         if (DEBUG) {
             Slog.d(TAG, "requestDemux(request=" + request + ")");
         }
@@ -614,7 +727,8 @@
     }
 
     @VisibleForTesting
-    boolean requestDescramblerInternal(TunerDescramblerRequest request, int[] descramblerHandle) {
+    protected boolean requestDescramblerInternal(
+            TunerDescramblerRequest request, int[] descramblerHandle) {
         if (DEBUG) {
             Slog.d(TAG, "requestDescrambler(request=" + request + ")");
         }
@@ -742,14 +856,28 @@
         ownerProfile.releaseLnb(releasingLnb.getId());
     }
 
+    private void updateCasClientMappingOnNewGrant(int grantingId, int ownerClientId) {
+        CasResource grantingCas = getCasResource(grantingId);
+        ClientProfile ownerProfile = getClientProfile(ownerClientId);
+        grantingCas.setOwner(ownerClientId);
+        ownerProfile.useCas(grantingId);
+    }
+
+    private void updateCasClientMappingOnRelease(
+            @NonNull CasResource releasingCas, int ownerClientId) {
+        ClientProfile ownerProfile = getClientProfile(ownerClientId);
+        releasingCas.removeOwner(ownerClientId);
+        ownerProfile.releaseCas();
+    }
+
     /**
      * Get the owner client's priority from the resource id.
      *
-     * @param resource a in use tuner resource.
+     * @param clientId the owner client id.
      * @return the priority of the owner client of the resource.
      */
-    private int getOwnerClientPriority(TunerResourceBasic resource) {
-        return getClientProfile(resource.getOwnerClientId()).getPriority();
+    private int getOwnerClientPriority(int clientId) {
+        return getClientProfile(clientId).getPriority();
     }
 
     @VisibleForTesting
@@ -783,6 +911,9 @@
 
     private void removeFrontendResource(int removingId) {
         FrontendResource fe = getFrontendResource(removingId);
+        if (fe == null) {
+            return;
+        }
         if (fe.isInUse()) {
             releaseFrontendInternal(fe);
         }
@@ -811,6 +942,9 @@
 
     private void removeLnbResource(int removingId) {
         LnbResource lnb = getLnbResource(removingId);
+        if (lnb == null) {
+            return;
+        }
         if (lnb.isInUse()) {
             releaseLnbInternal(lnb);
         }
@@ -819,6 +953,39 @@
 
     @VisibleForTesting
     @Nullable
+    protected CasResource getCasResource(int systemId) {
+        return mCasResources.get(systemId);
+    }
+
+    @VisibleForTesting
+    protected Map<Integer, CasResource> getCasResources() {
+        return mCasResources;
+    }
+
+    private void addCasResource(CasResource newCas) {
+        // Update resource list and available id list
+        mCasResources.put(newCas.getSystemId(), newCas);
+    }
+
+    private void removeCasResource(int removingId) {
+        CasResource cas = getCasResource(removingId);
+        if (cas == null) {
+            return;
+        }
+        for (int ownerId : cas.getOwnerClientIds()) {
+            getClientProfile(ownerId).releaseCas();
+        }
+        mCasResources.remove(removingId);
+    }
+
+    private void releaseLowerPriorityClientCasResources(int releasingCasResourceNum) {
+        // TODO: Sort with a treemap
+
+        // select the first num client to release
+    }
+
+    @VisibleForTesting
+    @Nullable
     protected ClientProfile getClientProfile(int clientId) {
         return mClientProfiles.get(clientId);
     }
@@ -830,12 +997,7 @@
     }
 
     private void removeClientProfile(int clientId) {
-        for (int id : getClientProfile(clientId).getInUseFrontendIds()) {
-            getFrontendResource(id).removeOwner();
-            for (int groupMemberId : getFrontendResource(id).getExclusiveGroupMemberFeIds()) {
-                getFrontendResource(groupMemberId).removeOwner();
-            }
-        }
+        reclaimingResourcesFromClient(getClientProfile(clientId));
         mClientProfiles.remove(clientId);
         mListeners.remove(clientId);
     }
@@ -847,6 +1009,9 @@
         for (Integer lnbId : profile.getInUseLnbIds()) {
             getLnbResource(lnbId).removeOwner();
         }
+        if (profile.getInUseCasSystemId() != ClientProfile.INVALID_RESOURCE_ID) {
+            getCasResource(profile.getInUseCasSystemId()).removeOwner(profile.getId());
+        }
         profile.reclaimAllResources();
     }
 
diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java
index 3f9f95c..9d63029 100644
--- a/services/core/java/com/android/server/wm/AccessibilityController.java
+++ b/services/core/java/com/android/server/wm/AccessibilityController.java
@@ -66,6 +66,7 @@
 import com.android.server.wm.WindowManagerInternal.MagnificationCallbacks;
 import com.android.server.wm.WindowManagerInternal.WindowsForAccessibilityCallback;
 
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -341,6 +342,16 @@
         windowState.getTransformationMatrix(sTempFloats, outMatrix);
     }
 
+    void dump(PrintWriter pw, String prefix) {
+        for (int i = 0; i < mDisplayMagnifiers.size(); i++) {
+            final DisplayMagnifier displayMagnifier = mDisplayMagnifiers.valueAt(i);
+            if (displayMagnifier != null) {
+                displayMagnifier.dump(pw, prefix
+                        + "Magnification display# " + mDisplayMagnifiers.keyAt(i));
+            }
+        }
+    }
+
     /**
      * This class encapsulates the functionality related to display magnification.
      */
@@ -551,6 +562,10 @@
             mMagnifedViewport.drawWindowIfNeededLocked(t);
         }
 
+        void dump(PrintWriter pw, String prefix) {
+            mMagnifedViewport.dump(pw, prefix);
+        }
+
         private final class MagnifiedViewport {
 
             private final SparseArray<WindowState> mTempWindowStates =
@@ -820,6 +835,10 @@
                 }, false /* traverseTopToBottom */ );
             }
 
+            void dump(PrintWriter pw, String prefix) {
+                mWindow.dump(pw, prefix);
+            }
+
             private final class ViewportWindow {
                 private static final String SURFACE_TITLE = "Magnification Overlay";
 
@@ -985,6 +1004,14 @@
                     mSurface.release();
                 }
 
+                void dump(PrintWriter pw, String prefix) {
+                    pw.println(prefix
+                            + " mBounds= " + mBounds
+                            + " mDirtyRect= " + mDirtyRect
+                            + " mWidth= " + mSurfaceControl.getWidth()
+                            + " mHeight= " + mSurfaceControl.getHeight());
+                }
+
                 private final class AnimationController extends Handler {
                     private static final String PROPERTY_NAME_ALPHA = "alpha";
 
@@ -1245,6 +1272,16 @@
                     }
                 }
 
+                for (int i = dc.mShellRoots.size() - 1; i >= 0; --i) {
+                    final WindowInfo info = dc.mShellRoots.valueAt(i).getWindowInfo();
+                    if (info == null) {
+                        continue;
+                    }
+                    info.layer = addedWindows.size();
+                    windows.add(info);
+                    addedWindows.add(info.token);
+                }
+
                 // Remove child/parent references to windows that were not added.
                 final int windowCount = windows.size();
                 for (int i = 0; i < windowCount; i++) {
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index e76eda0..729f549 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -3137,7 +3137,6 @@
         commitVisibility(false /* visible */, true /* performLayout */);
 
         getDisplayContent().mOpeningApps.remove(this);
-        getDisplayContent().mChangingContainers.remove(this);
         getDisplayContent().mUnknownAppVisibilityController.appRemovedOrHidden(this);
         mWmService.mTaskSnapshotController.onAppRemoved(this);
         mStackSupervisor.getActivityMetricsLogger().notifyActivityRemoved(this);
@@ -4712,7 +4711,7 @@
      */
     private boolean shouldBeResumed(ActivityRecord activeActivity) {
         return shouldMakeActive(activeActivity) && isFocusable()
-                && getRootTask().getVisibility(activeActivity) == STACK_VISIBILITY_VISIBLE
+                && getTask().getVisibility(activeActivity) == STACK_VISIBILITY_VISIBLE
                 && canResumeByCompat();
     }
 
@@ -6130,19 +6129,8 @@
 
     @Override
     void cancelAnimation() {
-        cancelAnimationOnly();
-        clearThumbnail();
-        mSurfaceFreezer.unfreeze(getPendingTransaction());
-    }
-
-    /**
-     * This only cancels the animation. It doesn't do other teardown like cleaning-up thumbnail
-     * or interim leashes.
-     * <p>
-     * Used when canceling in preparation for starting a new animation.
-     */
-    void cancelAnimationOnly() {
         super.cancelAnimation();
+        clearThumbnail();
     }
 
     @VisibleForTesting
diff --git a/services/core/java/com/android/server/wm/ActivityStack.java b/services/core/java/com/android/server/wm/ActivityStack.java
index 0f57496..ff43e77 100644
--- a/services/core/java/com/android/server/wm/ActivityStack.java
+++ b/services/core/java/com/android/server/wm/ActivityStack.java
@@ -18,11 +18,8 @@
 
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SPLIT_SCREEN;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
-import static android.app.WindowConfiguration.PINNED_WINDOWING_MODE_ELEVATION_IN_DIP;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
@@ -142,13 +139,11 @@
 import android.os.Trace;
 import android.os.UserHandle;
 import android.service.voice.IVoiceInteractionSession;
-import android.util.DisplayMetrics;
 import android.util.Log;
 import android.util.Slog;
 import android.util.proto.ProtoOutputStream;
 import android.view.Display;
 import android.view.DisplayInfo;
-import android.view.SurfaceControl;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -157,7 +152,6 @@
 import com.android.internal.util.function.pooled.PooledConsumer;
 import com.android.internal.util.function.pooled.PooledFunction;
 import com.android.internal.util.function.pooled.PooledLambda;
-import com.android.internal.util.function.pooled.PooledPredicate;
 import com.android.server.Watchdog;
 import com.android.server.am.ActivityManagerService;
 import com.android.server.am.ActivityManagerService.ItemMatcher;
@@ -276,11 +270,6 @@
 
     Rect mPreAnimationBounds = new Rect();
 
-    /**
-     * For {@link #prepareSurfaces}.
-     */
-    private final Point mLastSurfaceSize = new Point();
-
     private final AnimatingActivityRegistry mAnimatingActivityRegistry =
             new AnimatingActivityRegistry();
 
@@ -606,10 +595,6 @@
 
         super.onConfigurationChanged(newParentConfig);
 
-        // Only need to update surface size here since the super method will handle updating
-        // surface position.
-        updateSurfaceSize(getPendingTransaction());
-
         final TaskDisplayArea taskDisplayArea = getDisplayArea();
         if (taskDisplayArea == null) {
             return;
@@ -3262,61 +3247,14 @@
         scheduleAnimation();
     }
 
-    /**
-     * Calculate an amount by which to expand the stack bounds in each direction.
-     * Used to make room for shadows in the pinned windowing mode.
-     */
-    int getStackOutset() {
-        // If we are drawing shadows on the task then don't outset the stack.
-        if (mWmService.mRenderShadowsInCompositor) {
-            return 0;
-        }
-        DisplayContent displayContent = getDisplayContent();
-        if (inPinnedWindowingMode() && displayContent != null) {
-            final DisplayMetrics displayMetrics = displayContent.getDisplayMetrics();
-
-            // We multiply by two to match the client logic for converting view elevation
-            // to insets, as in {@link WindowManager.LayoutParams#setSurfaceInsets}
-            return (int) Math.ceil(
-                    mWmService.dipToPixel(PINNED_WINDOWING_MODE_ELEVATION_IN_DIP, displayMetrics)
-                            * 2);
-        }
-        return 0;
-    }
-
     @Override
     void getRelativePosition(Point outPos) {
         super.getRelativePosition(outPos);
-        final int outset = getStackOutset();
+        final int outset = getTaskOutset();
         outPos.x -= outset;
         outPos.y -= outset;
     }
 
-    private void updateSurfaceSize(SurfaceControl.Transaction transaction) {
-        if (mSurfaceControl == null) {
-            return;
-        }
-
-        final Rect stackBounds = getBounds();
-        int width = stackBounds.width();
-        int height = stackBounds.height();
-
-        final int outset = getStackOutset();
-        width += 2 * outset;
-        height += 2 * outset;
-
-        if (width == mLastSurfaceSize.x && height == mLastSurfaceSize.y) {
-            return;
-        }
-        transaction.setWindowCrop(mSurfaceControl, width, height);
-        mLastSurfaceSize.set(width, height);
-    }
-
-    @VisibleForTesting
-    Point getLastSurfaceSize() {
-        return mLastSurfaceSize;
-    }
-
     @Override
     void onDisplayChanged(DisplayContent dc) {
         super.onDisplayChanged(dc);
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 0754a34..8edc84f 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -168,7 +168,6 @@
 
     // The task display area to launch the activity onto, barring any strong reason to do otherwise.
     private TaskDisplayArea mPreferredTaskDisplayArea;
-    // The windowing mode to apply to the root task, if possible
     private int mPreferredWindowingMode;
 
     private Task mInTask;
@@ -1560,9 +1559,6 @@
         if (!mAvoidMoveToFront && mDoResume) {
             mTargetStack.getStack().moveToFront("reuseOrNewTask", targetTask);
             if (mOptions != null) {
-                if (mPreferredWindowingMode != WINDOWING_MODE_UNDEFINED) {
-                    mTargetStack.setWindowingMode(mPreferredWindowingMode);
-                }
                 if (mOptions.getTaskAlwaysOnTop()) {
                     mTargetStack.setAlwaysOnTop(true);
                 }
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 736bc9f..02f6a69 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -5400,14 +5400,11 @@
         final boolean inputMethodExists = !(config.keyboard == Configuration.KEYBOARD_NOKEYS
                 && config.touchscreen == Configuration.TOUCHSCREEN_NOTOUCH
                 && config.navigation == Configuration.NAVIGATION_NONAV);
-        int modeType = config.uiMode & Configuration.UI_MODE_TYPE_MASK;
-        final boolean uiModeSupportsDialogs = (modeType != Configuration.UI_MODE_TYPE_CAR
-                && !(modeType == Configuration.UI_MODE_TYPE_WATCH && Build.IS_USER)
-                && modeType != Configuration.UI_MODE_TYPE_TELEVISION
-                && modeType != Configuration.UI_MODE_TYPE_VR_HEADSET);
         final boolean hideDialogsSet = Settings.Global.getInt(mContext.getContentResolver(),
                 HIDE_ERROR_DIALOGS, 0) != 0;
-        mShowDialogs = inputMethodExists && uiModeSupportsDialogs && !hideDialogsSet;
+        mShowDialogs = inputMethodExists
+                && ActivityTaskManager.currentUiModeSupportsErrorDialogs(mContext)
+                && !hideDialogsSet;
     }
 
     private void updateFontScaleIfNeeded(@UserIdInt int userId) {
@@ -6607,7 +6604,6 @@
                     }
                     return;
                 }
-                process.mIsImeProcess = true;
                 process.registerDisplayConfigurationListener(displayContent);
             }
         }
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index a47cdc6..e261632 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -26,7 +26,6 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
-import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
@@ -574,7 +573,7 @@
     /** Corner radius that windows should have in order to match the display. */
     private final float mWindowCornerRadius;
 
-    private final SparseArray<ShellRoot> mShellRoots = new SparseArray<>();
+    final SparseArray<ShellRoot> mShellRoots = new SparseArray<>();
     RemoteInsetsControlTarget mRemoteInsetsControlTarget = null;
     private final IBinder.DeathRecipient mRemoteInsetsDeath =
             () -> {
@@ -5450,6 +5449,46 @@
         return mWmService.mDisplayManagerInternal.getDisplayPosition(getDisplayId());
     }
 
+    /**
+     * Locates the appropriate target window for scroll capture. The search progresses top to
+     * bottom.
+     * If {@code searchBehind} is non-null, the search will only consider windows behind this one.
+     * If a valid taskId is specified, the target window must belong to the given task.
+     *
+     * @param searchBehind a window used to filter the search to windows behind it, or null to begin
+     *                     the search at the top window of the display
+     * @param taskId       specifies the id of a task the result must belong to or
+     *                     {@link android.app.ActivityTaskManager#INVALID_TASK_ID INVALID_TASK_ID}
+     *                     to match any window
+     * @return the located window or null if none could be found matching criteria
+     */
+    @Nullable
+    WindowState findScrollCaptureTargetWindow(@Nullable WindowState searchBehind, int taskId) {
+        return getWindow(new Predicate<WindowState>() {
+            boolean behindTopWindow = (searchBehind == null); // optional filter
+            @Override
+            public boolean test(WindowState nextWindow) {
+                // Skip through all windows until we pass topWindow (if specified)
+                if (!behindTopWindow) {
+                    if (nextWindow == searchBehind) {
+                        behindTopWindow = true;
+                    }
+                    return false; /* continue */
+                }
+                if (taskId != INVALID_TASK_ID) {
+                    Task task = nextWindow.getTask();
+                    if (task == null || !task.isTaskId(taskId)) {
+                        return false; /* continue */
+                    }
+                }
+                if (!nextWindow.canReceiveKeys()) {
+                    return false; /* continue */
+                }
+                return true; /* stop */
+            }
+        });
+    }
+
     class RemoteInsetsControlTarget implements InsetsControlTarget {
         private final IDisplayWindowInsetsController mRemoteInsetsController;
 
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index e9d3d56..8aace21 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -261,6 +261,8 @@
     @Px
     private int mRightGestureInset;
 
+    private boolean mNavButtonForcedVisible;
+
     StatusBarManagerInternal getStatusBarManagerInternal() {
         synchronized (mServiceAcquireLock) {
             if (mStatusBarManagerInternal == null) {
@@ -1046,12 +1048,14 @@
                             // calculate inset.
                             if (navigationBarPosition(displayFrames.mDisplayWidth,
                                     displayFrames.mDisplayHeight,
-                                    displayFrames.mRotation) == NAV_BAR_BOTTOM) {
+                                    displayFrames.mRotation) == NAV_BAR_BOTTOM
+                                    && !mNavButtonForcedVisible) {
+
                                 sTmpRect.set(displayFrames.mUnrestricted);
                                 sTmpRect.intersectUnchecked(displayFrames.mDisplayCutoutSafe);
                                 inOutFrame.top = sTmpRect.bottom
                                         - getNavigationBarHeight(displayFrames.mRotation,
-                                                mDisplayContent.getConfiguration().uiMode);
+                                        mDisplayContent.getConfiguration().uiMode);
                             }
                         },
 
@@ -2810,6 +2814,8 @@
         mNavBarOpacityMode = res.getInteger(R.integer.config_navBarOpacityMode);
         mLeftGestureInset = mGestureNavigationSettingsObserver.getLeftSensitivity(res);
         mRightGestureInset = mGestureNavigationSettingsObserver.getRightSensitivity(res);
+        mNavButtonForcedVisible =
+                mGestureNavigationSettingsObserver.areNavigationButtonForcedVisible();
         mNavigationBarLetsThroughTaps = res.getBoolean(R.bool.config_navBarTapThrough);
         mNavigationBarAlwaysShowOnSideGesture =
                 res.getBoolean(R.bool.config_navBarAlwaysShowOnSideEdgeGesture);
@@ -3783,13 +3789,14 @@
      * @param screenshotType The type of screenshot, for example either
      *                       {@link WindowManager#TAKE_SCREENSHOT_FULLSCREEN} or
      *                       {@link WindowManager#TAKE_SCREENSHOT_SELECTED_REGION}
+     * @param source Where the screenshot originated from (see WindowManager.ScreenshotSource)
      */
-    public void takeScreenshot(int screenshotType) {
+    public void takeScreenshot(int screenshotType, int source) {
         if (mScreenshotHelper != null) {
             mScreenshotHelper.takeScreenshot(screenshotType,
                     mStatusBar != null && mStatusBar.isVisibleLw(),
                     mNavigationBar != null && mNavigationBar.isVisibleLw(),
-                    mHandler, null /* completionConsumer */);
+                    source, mHandler, null /* completionConsumer */);
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/DockedStackDividerController.java b/services/core/java/com/android/server/wm/DockedStackDividerController.java
index 20738ed..803bec8 100644
--- a/services/core/java/com/android/server/wm/DockedStackDividerController.java
+++ b/services/core/java/com/android/server/wm/DockedStackDividerController.java
@@ -45,6 +45,11 @@
 
     void setTouchRegion(Rect touchRegion) {
         mTouchRegion.set(touchRegion);
+        // We need to report touchable region changes to accessibility.
+        if (mDisplayContent.mWmService.mAccessibilityController != null) {
+            mDisplayContent.mWmService.mAccessibilityController.onSomeWindowResizedOrMovedLocked(
+                    mDisplayContent.getDisplayId());
+        }
     }
 
     void getTouchRegion(Rect outRegion) {
diff --git a/services/core/java/com/android/server/wm/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java
index 8b34b9b..656dca5 100644
--- a/services/core/java/com/android/server/wm/InputMonitor.java
+++ b/services/core/java/com/android/server/wm/InputMonitor.java
@@ -487,7 +487,8 @@
                     || w.cantReceiveTouchInput()) {
                 if (w.mWinAnimator.hasSurface()) {
                     mInputTransaction.setInputWindowInfo(
-                            w.mWinAnimator.mSurfaceController.mSurfaceControl, mInvalidInputWindow);
+                        w.mWinAnimator.mSurfaceController.getClientViewRootSurface(),
+                        mInvalidInputWindow);
                 }
                 // Skip this window because it cannot possibly receive input.
                 return;
@@ -560,7 +561,8 @@
 
             if (w.mWinAnimator.hasSurface()) {
                 mInputTransaction.setInputWindowInfo(
-                        w.mWinAnimator.mSurfaceController.mSurfaceControl, inputWindowHandle);
+                    w.mWinAnimator.mSurfaceController.getClientViewRootSurface(),
+                    inputWindowHandle);
             }
         }
     }
diff --git a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
index 5f33ea1..86e0818 100644
--- a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
+++ b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
@@ -212,7 +212,7 @@
             final Surface surface = mService.mSurfaceFactory.get();
             surface.copyFrom(mScreenshotLayer);
             SurfaceControl.ScreenshotGraphicBuffer gb =
-                    mService.mDisplayManagerInternal.screenshot(displayId);
+                    mService.mDisplayManagerInternal.systemScreenshot(displayId);
             if (gb != null) {
                 Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER,
                         "ScreenRotationAnimation#getMedianBorderLuma");
@@ -597,8 +597,8 @@
             return startAnimation(initializeBuilder()
                             .setSurfaceControl(mScreenshotLayer)
                             .setAnimationLeashParent(mDisplayContent.getOverlayLayer())
-                            .setWidth(mWidth)
-                            .setHeight(mHeight)
+                            .setWidth(mDisplayContent.getSurfaceWidth())
+                            .setHeight(mDisplayContent.getSurfaceHeight())
                             .build(),
                     createWindowAnimationSpec(mRotateAlphaAnimation),
                     this::onAnimationEnd);
diff --git a/services/core/java/com/android/server/wm/ShellRoot.java b/services/core/java/com/android/server/wm/ShellRoot.java
index 701feff..0b1760d 100644
--- a/services/core/java/com/android/server/wm/ShellRoot.java
+++ b/services/core/java/com/android/server/wm/ShellRoot.java
@@ -23,12 +23,14 @@
 
 import android.annotation.NonNull;
 import android.graphics.Point;
+import android.graphics.Rect;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Slog;
 import android.view.DisplayInfo;
 import android.view.IWindow;
 import android.view.SurfaceControl;
+import android.view.WindowInfo;
 import android.view.animation.Animation;
 
 /**
@@ -102,5 +104,27 @@
         mToken.startAnimation(mToken.getPendingTransaction(), adapter, false /* hidden */,
                 ANIMATION_TYPE_WINDOW_ANIMATION, null /* animationFinishedCallback */);
     }
+
+    WindowInfo getWindowInfo() {
+        if (mToken.windowType != TYPE_DOCK_DIVIDER) {
+            return null;
+        }
+        if (!mDisplayContent.getDefaultTaskDisplayArea().isSplitScreenModeActivated()) {
+            return null;
+        }
+        WindowInfo windowInfo = WindowInfo.obtain();
+        windowInfo.displayId = mToken.getDisplayArea().getDisplayContent().mDisplayId;
+        windowInfo.type = mToken.windowType;
+        windowInfo.layer = mToken.getWindowLayerFromType();
+        windowInfo.token = mClient.asBinder();
+        windowInfo.title = "Splitscreen Divider";
+        windowInfo.focused = false;
+        windowInfo.inPictureInPicture = false;
+        windowInfo.hasFlagWatchOutsideTouch = false;
+        final Rect regionRect = new Rect();
+        mDisplayContent.getDockedDividerController().getTouchRegion(regionRect);
+        windowInfo.regionInScreen.set(regionRect);
+        return windowInfo;
+    }
 }
 
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 66ca0ac..b12eb70 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -120,6 +120,7 @@
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
 import android.content.res.Configuration;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.Debug;
 import android.os.IBinder;
@@ -213,7 +214,6 @@
 
     static final int INVALID_MIN_SIZE = -1;
     private float mShadowRadius = 0;
-    private final Rect mLastSurfaceCrop = new Rect();
 
     /**
      * The modes to control how the stack is moved to the front when calling {@link Task#reparent}.
@@ -397,6 +397,7 @@
 
     private Dimmer mDimmer = new Dimmer(this);
     private final Rect mTmpDimBoundsRect = new Rect();
+    private final Point mLastSurfaceSize = new Point();
 
     /** @see #setCanAffectSystemUiFlags */
     private boolean mCanAffectSystemUiFlags = true;
@@ -1943,6 +1944,10 @@
         mTmpPrevBounds.set(getBounds());
         final boolean wasInMultiWindowMode = inMultiWindowMode();
         super.onConfigurationChanged(newParentConfig);
+        // Only need to update surface size here since the super method will handle updating
+        // surface position.
+        updateSurfaceSize(getPendingTransaction());
+
         if (wasInMultiWindowMode != inMultiWindowMode()) {
             mStackSupervisor.scheduleUpdateMultiWindowMode(this);
         }
@@ -1987,7 +1992,8 @@
         if (mWmService.mDisableTransitionAnimation
                 || !isVisible()
                 || getDisplayContent().mAppTransition.isTransitionSet()
-                || getSurfaceControl() == null) {
+                || getSurfaceControl() == null
+                || !isLeafTask()) {
             return false;
         }
         // Only do an animation into and out-of freeform mode for now. Other mode
@@ -1995,6 +2001,57 @@
         return (prevWinMode == WINDOWING_MODE_FREEFORM) != (newWinMode == WINDOWING_MODE_FREEFORM);
     }
 
+    void updateSurfaceSize(SurfaceControl.Transaction transaction) {
+        if (mSurfaceControl == null || mCreatedByOrganizer) {
+            return;
+        }
+
+        // Apply crop to root tasks only and clear the crops of the descendant tasks.
+        int width = 0;
+        int height = 0;
+        if (isRootTask()) {
+            final Rect taskBounds = getBounds();
+            width = taskBounds.width();
+            height = taskBounds.height();
+
+            final int outset = getTaskOutset();
+            width += 2 * outset;
+            height += 2 * outset;
+        }
+        if (width == mLastSurfaceSize.x && height == mLastSurfaceSize.y) {
+            return;
+        }
+        transaction.setWindowCrop(mSurfaceControl, width, height);
+        mLastSurfaceSize.set(width, height);
+    }
+
+    /**
+     * Calculate an amount by which to expand the task bounds in each direction.
+     * Used to make room for shadows in the pinned windowing mode.
+     */
+    int getTaskOutset() {
+        // If we are drawing shadows on the task then don't outset the stack.
+        if (mWmService.mRenderShadowsInCompositor) {
+            return 0;
+        }
+        DisplayContent displayContent = getDisplayContent();
+        if (inPinnedWindowingMode() && displayContent != null) {
+            final DisplayMetrics displayMetrics = displayContent.getDisplayMetrics();
+
+            // We multiply by two to match the client logic for converting view elevation
+            // to insets, as in {@link WindowManager.LayoutParams#setSurfaceInsets}
+            return (int) Math.ceil(
+                    mWmService.dipToPixel(PINNED_WINDOWING_MODE_ELEVATION_IN_DIP, displayMetrics)
+                            * 2);
+        }
+        return 0;
+    }
+
+    @VisibleForTesting
+    Point getLastSurfaceSize() {
+        return mLastSurfaceSize;
+    }
+
     @VisibleForTesting
     boolean isInChangeTransition() {
         return mSurfaceFreezer.hasLeash() || AppTransition.isChangeTransit(mTransit);
@@ -2225,14 +2282,16 @@
         }
         density *= DisplayMetrics.DENSITY_DEFAULT_SCALE;
 
+        // If bounds have been overridden at this level, restrict config resources to these bounds
+        // rather than the parent because the overridden bounds can be larger than the parent.
+        boolean hasOverrideBounds = false;
+
         final Rect resolvedBounds = inOutConfig.windowConfiguration.getBounds();
-        if (resolvedBounds == null) {
-            mTmpFullBounds.setEmpty();
+        if (resolvedBounds == null || resolvedBounds.isEmpty()) {
+            mTmpFullBounds.set(parentConfig.windowConfiguration.getBounds());
         } else {
             mTmpFullBounds.set(resolvedBounds);
-        }
-        if (mTmpFullBounds.isEmpty()) {
-            mTmpFullBounds.set(parentConfig.windowConfiguration.getBounds());
+            hasOverrideBounds = true;
         }
 
         Rect outAppBounds = inOutConfig.windowConfiguration.getAppBounds();
@@ -2244,7 +2303,16 @@
         // the out bounds doesn't need to be restricted by the parent.
         final boolean insideParentBounds = compatInsets == null;
         if (insideParentBounds && windowingMode != WINDOWING_MODE_FREEFORM) {
-            final Rect parentAppBounds = parentConfig.windowConfiguration.getAppBounds();
+            Rect parentAppBounds;
+            if (hasOverrideBounds) {
+                // Since we overrode the bounds, restrict appBounds to display non-decor rather
+                // than parent. Otherwise, it won't match the overridden bounds.
+                final TaskDisplayArea displayArea = getDisplayArea();
+                parentAppBounds = displayArea != null
+                        ? displayArea.getConfiguration().windowConfiguration.getAppBounds() : null;
+            } else {
+                parentAppBounds = parentConfig.windowConfiguration.getAppBounds();
+            }
             if (parentAppBounds != null && !parentAppBounds.isEmpty()) {
                 outAppBounds.intersect(parentAppBounds);
             }
@@ -2291,13 +2359,13 @@
 
             if (inOutConfig.screenWidthDp == Configuration.SCREEN_WIDTH_DP_UNDEFINED) {
                 final int overrideScreenWidthDp = (int) (mTmpStableBounds.width() / density);
-                inOutConfig.screenWidthDp = insideParentBounds
+                inOutConfig.screenWidthDp = (insideParentBounds && !hasOverrideBounds)
                         ? Math.min(overrideScreenWidthDp, parentConfig.screenWidthDp)
                         : overrideScreenWidthDp;
             }
             if (inOutConfig.screenHeightDp == Configuration.SCREEN_HEIGHT_DP_UNDEFINED) {
                 final int overrideScreenHeightDp = (int) (mTmpStableBounds.height() / density);
-                inOutConfig.screenHeightDp = insideParentBounds
+                inOutConfig.screenHeightDp = (insideParentBounds && !hasOverrideBounds)
                         ? Math.min(overrideScreenHeightDp, parentConfig.screenHeightDp)
                         : overrideScreenHeightDp;
             }
@@ -2344,27 +2412,27 @@
         mTmpBounds.set(getResolvedOverrideConfiguration().windowConfiguration.getBounds());
         super.resolveOverrideConfiguration(newParentConfig);
 
-        // Resolve override windowing mode to fullscreen for home task (even on freeform
-        // display), or split-screen-secondary if in split-screen mode.
         int windowingMode =
                 getResolvedOverrideConfiguration().windowConfiguration.getWindowingMode();
+
+        // Resolve override windowing mode to fullscreen for home task (even on freeform
+        // display), or split-screen if in split-screen mode.
         if (getActivityType() == ACTIVITY_TYPE_HOME && windowingMode == WINDOWING_MODE_UNDEFINED) {
             final int parentWindowingMode = newParentConfig.windowConfiguration.getWindowingMode();
-            windowingMode = parentWindowingMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY
-                    ? WINDOWING_MODE_SPLIT_SCREEN_SECONDARY
-                    : WINDOWING_MODE_FULLSCREEN;
+            windowingMode = WindowConfiguration.isSplitScreenWindowingMode(parentWindowingMode)
+                    ? parentWindowingMode : WINDOWING_MODE_FULLSCREEN;
             getResolvedOverrideConfiguration().windowConfiguration.setWindowingMode(windowingMode);
         }
 
-        if (!isLeafTask()) {
-            // Compute configuration overrides for tasks that created by organizer, so that
-            // organizer can get the correct configuration from those tasks.
-            if (mCreatedByOrganizer) {
-                computeConfigResourceOverrides(getResolvedOverrideConfiguration(), newParentConfig);
-            }
-            return;
+        if (isLeafTask()) {
+            resolveLeafOnlyOverrideConfigs(newParentConfig);
         }
+        computeConfigResourceOverrides(getResolvedOverrideConfiguration(), newParentConfig);
+    }
 
+    void resolveLeafOnlyOverrideConfigs(Configuration newParentConfig) {
+        int windowingMode =
+                getResolvedOverrideConfiguration().windowConfiguration.getWindowingMode();
         if (windowingMode == WINDOWING_MODE_UNDEFINED) {
             windowingMode = newParentConfig.windowConfiguration.getWindowingMode();
         }
@@ -2404,7 +2472,6 @@
                 outOverrideBounds.offset(0, offsetTop);
             }
         }
-        computeConfigResourceOverrides(getResolvedOverrideConfiguration(), newParentConfig);
     }
 
     /**
@@ -2803,28 +2870,6 @@
         return boundsChange;
     }
 
-    private void updateSurfaceCrop() {
-        // Only update the crop if we are drawing shadows on the task.
-        if (mSurfaceControl == null || !mWmService.mRenderShadowsInCompositor || !isRootTask()) {
-            return;
-        }
-
-        if (inSplitScreenWindowingMode()) {
-            // inherit crop from parent
-            mTmpRect.setEmpty();
-        } else {
-            getBounds(mTmpRect);
-        }
-
-        mTmpRect.offsetTo(0, 0);
-        if (mLastSurfaceCrop.equals(mTmpRect)) {
-            return;
-        }
-
-        getPendingTransaction().setWindowCrop(mSurfaceControl, mTmpRect);
-        mLastSurfaceCrop.set(mTmpRect);
-    }
-
     @Override
     public boolean onDescendantOrientationChanged(IBinder freezeDisplayToken,
             ConfigurationContainer requestingContainer) {
@@ -2853,22 +2898,12 @@
         if (!isRootTask) {
             adjustBoundsForDisplayChangeIfNeeded(dc);
         }
-        final DisplayContent prevDc = mDisplayContent;
         super.onDisplayChanged(dc);
         if (!isRootTask) {
             final int displayId = (dc != null) ? dc.getDisplayId() : INVALID_DISPLAY;
             mWmService.mAtmService.getTaskChangeNotificationController().notifyTaskDisplayChanged(
                     mTaskId, displayId);
         }
-        if (prevDc != null && prevDc.mChangingContainers.remove(this)) {
-            // This gets called *after* this has been reparented to the new display.
-            // That reparenting resulted in this window changing modes (eg. FREEFORM -> FULLSCREEN),
-            // so this token is now "frozen" while waiting for the animation to start on prevDc
-            // (which will be cancelled since the window is no-longer a child). However, since this
-            // is no longer a child of prevDc, this won't be notified of the cancelled animation,
-            // so we need to cancel the change transition here.
-            mSurfaceFreezer.unfreeze(getPendingTransaction());
-        }
     }
 
     boolean isResizeable(boolean checkSupportsPip) {
@@ -3453,7 +3488,6 @@
             mTmpDimBoundsRect.offsetTo(0, 0);
         }
 
-        updateSurfaceCrop();
         updateShadowsRadius(isFocused(), getPendingTransaction());
 
         if (mDimmer.updateDims(getPendingTransaction(), mTmpDimBoundsRect)) {
@@ -4392,19 +4426,20 @@
         // Let the old organizer know it has lost control.
         sendTaskVanished();
         mTaskOrganizer = organizer;
-        sendTaskAppeared();
-        onTaskOrganizerChanged();
-        return true;
-    }
 
-    void taskOrganizerUnregistered() {
-        mTaskOrganizer = null;
-        mTaskAppearedSent = false;
-        mLastTaskOrganizerWindowingMode = -1;
-        onTaskOrganizerChanged();
-        if (mCreatedByOrganizer) {
-            removeImmediately();
+        if (mTaskOrganizer != null) {
+            sendTaskAppeared();
+        } else {
+            // No longer managed by any organizer.
+            mTaskAppearedSent = false;
+            mLastTaskOrganizerWindowingMode = -1;
+            setForceHidden(FLAG_FORCE_HIDDEN_FOR_TASK_ORG, false /* set */);
+            if (mCreatedByOrganizer) {
+                removeImmediately();
+            }
         }
+
+        return true;
     }
 
     /**
@@ -4441,14 +4476,6 @@
         return result;
     }
 
-    private void onTaskOrganizerChanged() {
-        if (mTaskOrganizer == null) {
-            // If this task is no longer controlled by a task organizer, then reset the force hidden
-            // state
-            setForceHidden(FLAG_FORCE_HIDDEN_FOR_TASK_ORG, false /* set */);
-        }
-    }
-
     @Override
     void setSurfaceControl(SurfaceControl sc) {
         super.setSurfaceControl(sc);
diff --git a/services/core/java/com/android/server/wm/TaskOrganizerController.java b/services/core/java/com/android/server/wm/TaskOrganizerController.java
index adc50bf..306c100 100644
--- a/services/core/java/com/android/server/wm/TaskOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskOrganizerController.java
@@ -218,18 +218,24 @@
         }
 
         void dispose() {
-            releaseTasks();
+            // Move organizer from managing specific windowing modes
             for (int i = mTaskOrganizersForWindowingMode.size() - 1; i >= 0; --i) {
                 mTaskOrganizersForWindowingMode.valueAt(i).remove(mOrganizer.getBinder());
             }
-        }
 
-        private void releaseTasks() {
-            for (int i = mOrganizedTasks.size() - 1; i >= 0; i--) {
-                final Task t = mOrganizedTasks.get(i);
-                removeTask(t);
-                t.taskOrganizerUnregistered();
+            // Update tasks currently managed by this organizer to the next one available if
+            // possible.
+            while (!mOrganizedTasks.isEmpty()) {
+                final Task t = mOrganizedTasks.get(0);
+                t.updateTaskOrganizerState(true /* forceUpdate */);
+                if (mOrganizedTasks.contains(t)) {
+                    removeTask(t);
+                }
             }
+
+            // Remove organizer state after removing tasks so we get a chance to send
+            // onTaskVanished.
+            mTaskOrganizerStates.remove(asBinder());
         }
 
         void unlinkDeath() {
@@ -313,16 +319,11 @@
                             new TaskOrganizerState(organizer, uid));
                 }
 
-                if (orgs.size() == 1) {
-                    // Only in the case where this is the root task organizer for the given
-                    // windowing mode, we add report all existing tasks in that mode to the new
-                    // task organizer.
-                    mService.mRootWindowContainer.forAllTasks((task) -> {
-                        if (task.getWindowingMode() == windowingMode) {
-                            task.updateTaskOrganizerState(true /* forceUpdate */);
-                        }
-                    });
-                }
+                mService.mRootWindowContainer.forAllTasks((task) -> {
+                    if (task.getWindowingMode() == windowingMode) {
+                        task.updateTaskOrganizerState(true /* forceUpdate */);
+                    }
+                });
             }
         } finally {
             Binder.restoreCallingIdentity(origId);
@@ -335,7 +336,7 @@
         final long origId = Binder.clearCallingIdentity();
         try {
             synchronized (mGlobalLock) {
-                final TaskOrganizerState state = mTaskOrganizerStates.remove(organizer.asBinder());
+                final TaskOrganizerState state = mTaskOrganizerStates.get(organizer.asBinder());
                 if (state == null) {
                     return;
                 }
@@ -367,7 +368,9 @@
 
     void onTaskVanished(ITaskOrganizer organizer, Task task) {
         final TaskOrganizerState state = mTaskOrganizerStates.get(organizer.asBinder());
-        state.removeTask(task);
+        if (state != null) {
+            state.removeTask(task);
+        }
     }
 
     @Override
diff --git a/services/core/java/com/android/server/wm/WallpaperWindowToken.java b/services/core/java/com/android/server/wm/WallpaperWindowToken.java
index 203ca25..5f3c633 100644
--- a/services/core/java/com/android/server/wm/WallpaperWindowToken.java
+++ b/services/core/java/com/android/server/wm/WallpaperWindowToken.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wm;
 
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
 
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_LAYERS;
@@ -45,6 +46,7 @@
             DisplayContent dc, boolean ownerCanManageAppTokens) {
         super(service, token, TYPE_WALLPAPER, explicit, dc, ownerCanManageAppTokens);
         dc.mWallpaperController.addWallpaperToken(this);
+        setWindowingMode(WINDOWING_MODE_FULLSCREEN);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index f3e2992..591bc54 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -526,6 +526,11 @@
      */
     @CallSuper
     void removeImmediately() {
+        final DisplayContent dc = getDisplayContent();
+        if (dc != null) {
+            mSurfaceFreezer.unfreeze(getPendingTransaction());
+            dc.mChangingContainers.remove(this);
+        }
         while (!mChildren.isEmpty()) {
             final E child = mChildren.peekLast();
             child.removeImmediately();
@@ -718,6 +723,10 @@
      * @param dc The display this container is on after changes.
      */
     void onDisplayChanged(DisplayContent dc) {
+        if (mDisplayContent != null && mDisplayContent.mChangingContainers.remove(this)) {
+            // Cancel any change transition queued-up for this container on the old display.
+            mSurfaceFreezer.unfreeze(getPendingTransaction());
+        }
         mDisplayContent = dc;
         if (dc != null && dc != this) {
             dc.getPendingTransaction().merge(mPendingTransaction);
@@ -2033,6 +2042,7 @@
 
     void cancelAnimation() {
         mSurfaceAnimator.cancelAnimation();
+        mSurfaceFreezer.unfreeze(getPendingTransaction());
     }
 
     @Override
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index f55a1b3..fbdea01 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -219,6 +219,7 @@
 import android.view.IPinnedStackListener;
 import android.view.IRecentsAnimationRunner;
 import android.view.IRotationWatcher;
+import android.view.IScrollCaptureController;
 import android.view.ISystemGestureExclusionListener;
 import android.view.IWallpaperVisibilityListener;
 import android.view.IWindow;
@@ -2445,17 +2446,15 @@
             if (controls != null) {
                 final int length = Math.min(controls.length, outControls.length);
                 for (int i = 0; i < length; i++) {
-                    final InsetsSourceControl control = controls[i];
-
-                    // Check if we are sending invalid leashes.
-                    final SurfaceControl leash = control != null ? control.getLeash() : null;
-                    if (leash != null && !leash.isValid()) {
-                        Slog.wtf(TAG, leash + " is not valid before sending to " + win,
-                                leash.getReleaseStack());
-                    }
-
-                    outControls[i] = win.isClientLocal() && control != null
-                            ? new InsetsSourceControl(control) : control;
+                    // We will leave the critical section before returning the leash to the client,
+                    // so we need to copy the leash to prevent others release the one that we are
+                    // about to return.
+                    // TODO: We will have an extra copy if the client is not local.
+                    //       For now, we rely on GC to release it.
+                    //       Maybe we can modify InsetsSourceControl.writeToParcel so it can release
+                    //       the extra leash as soon as possible.
+                    outControls[i] = controls[i] != null
+                            ? new InsetsSourceControl(controls[i]) : null;
                 }
             }
         }
@@ -6112,15 +6111,6 @@
                 pw.print("  mInputMethodInputTarget in display# "); pw.print(displayId);
                 pw.print(' '); pw.println(inputMethodInputTarget);
             }
-            if (mAccessibilityController != null) {
-                final Region magnificationRegion = new Region();
-                mAccessibilityController.getMagnificationRegionLocked(displayId,
-                        magnificationRegion);
-                pw.print("  mMagnificationRegion in display# ");
-                pw.print(displayId);
-                pw.print(' ');
-                pw.println(magnificationRegion);
-            }
         });
         pw.print("  mInTouchMode="); pw.println(mInTouchMode);
         pw.print("  mLastDisplayFreezeDuration=");
@@ -6136,6 +6126,9 @@
 
         mInputManagerCallback.dump(pw, "  ");
         mTaskSnapshotController.dump(pw, "  ");
+        if (mAccessibilityController != null) {
+            mAccessibilityController.dump(pw, "  ");
+        }
 
         if (dumpAll) {
             final WindowState imeWindow = mRoot.getCurrentInputMethodWindow();
@@ -6837,6 +6830,58 @@
         }
     }
 
+    /**
+     * Forwards a scroll capture request to the appropriate window, if available.
+     *
+     * @param displayId the display for the request
+     * @param behindClient token for a window, used to filter the search to windows behind it
+     * @param taskId specifies the id of a task the result must belong to or -1 to ignore task ids
+     * @param controller the controller to receive results; a call to either
+     *      {@link IScrollCaptureController#onClientConnected} or
+     *      {@link IScrollCaptureController#onClientUnavailable}.
+     */
+    public void requestScrollCapture(int displayId, @Nullable IBinder behindClient, int taskId,
+            IScrollCaptureController controller) {
+        if (!checkCallingPermission(READ_FRAME_BUFFER, "requestScrollCapture()")) {
+            throw new SecurityException("Requires READ_FRAME_BUFFER permission");
+        }
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mGlobalLock) {
+                DisplayContent dc = mRoot.getDisplayContent(displayId);
+                if (dc == null) {
+                    ProtoLog.e(WM_ERROR,
+                            "Invalid displayId for requestScrollCapture: %d", displayId);
+                    controller.onClientUnavailable();
+                    return;
+                }
+                WindowState topWindow = null;
+                if (behindClient != null) {
+                    topWindow = windowForClientLocked(null, behindClient, /* throwOnError*/ true);
+                }
+                WindowState targetWindow = dc.findScrollCaptureTargetWindow(topWindow, taskId);
+                if (targetWindow == null) {
+                    controller.onClientUnavailable();
+                    return;
+                }
+                // Forward to the window for handling.
+                try {
+                    targetWindow.mClient.requestScrollCapture(controller);
+                } catch (RemoteException e) {
+                    ProtoLog.w(WM_ERROR,
+                            "requestScrollCapture: caught exception dispatching to window."
+                                    + "token=%s", targetWindow.mClient.asBinder());
+                    controller.onClientUnavailable();
+                }
+            }
+        } catch (RemoteException e) {
+            ProtoLog.w(WM_ERROR,
+                    "requestScrollCapture: caught exception dispatching callback: %s", e);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
     @Override
     public void dontOverrideDisplayInfo(int displayId) {
         final long token = Binder.clearCallingIdentity();
@@ -7605,15 +7650,8 @@
                     Slog.w(TAG, "Cannot find window which accessibility connection is added to");
                     return;
                 }
-                try (SurfaceControl.Transaction t = new SurfaceControl.Transaction()) {
-                    t.setMetadata(
-                            state.mSurfaceControl,
-                            SurfaceControl.METADATA_ACCESSIBILITY_ID,
-                            accessibilityWindowId);
-                    t.apply();
-                } finally {
-                    SurfaceControl.closeTransaction();
-                }
+                mTransaction.setMetadata(state.mSurfaceControl,
+                        SurfaceControl.METADATA_ACCESSIBILITY_ID, accessibilityWindowId).apply();
             }
         }
 
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index 194ed3e..41bd707 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -40,6 +40,7 @@
 import static com.android.server.wm.ActivityTaskManagerService.KEY_DISPATCHING_TIMEOUT_MS;
 import static com.android.server.wm.ActivityTaskManagerService.RELAUNCH_REASON_NONE;
 
+import android.Manifest;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityThread;
@@ -50,6 +51,7 @@
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.ServiceInfo;
 import android.content.res.Configuration;
 import android.os.Build;
 import android.os.Message;
@@ -165,7 +167,8 @@
     // Thread currently set for VR scheduling
     int mVrThreadTid;
 
-    boolean mIsImeProcess;
+    // Whether this process has ever started a service with the BIND_INPUT_METHOD permission.
+    private volatile boolean mHasImeService;
 
     // all activities running in the process
     private final ArrayList<ActivityRecord> mActivities = new ArrayList<>();
@@ -187,6 +190,8 @@
     // Registered display id as a listener to override config change
     private int mDisplayId;
     private ActivityRecord mConfigActivityRecord;
+    // Whether the activity config override is allowed for this process.
+    private volatile boolean mIsActivityConfigOverrideAllowed = true;
     /**
      * Activities that hosts some UI drawn by the current process. The activities live
      * in another process. This is used to check if the process is currently showing anything
@@ -201,9 +206,6 @@
     /** Whether our process is currently running a {@link IRemoteAnimationRunner} */
     private boolean mRunningRemoteAnimation;
 
-    /** Whether this process is owned by the System UI package. */
-    final boolean mIsSysUiPackage;
-
     public WindowProcessController(@NonNull ActivityTaskManagerService atm, ApplicationInfo info,
             String name, int uid, int userId, Object owner, WindowProcessListener listener) {
         mInfo = info;
@@ -215,8 +217,13 @@
         mAtm = atm;
         mDisplayId = INVALID_DISPLAY;
 
-        mIsSysUiPackage = info.packageName.equals(
+        boolean isSysUiPackage = info.packageName.equals(
                 mAtm.getSysUiServiceComponentLocked().getPackageName());
+        if (isSysUiPackage || mUid == Process.SYSTEM_UID) {
+            // This is a system owned process and should not use an activity config.
+            // TODO(b/151161907): Remove after support for display-independent (raw) SysUi configs.
+            mIsActivityConfigOverrideAllowed = false;
+        }
 
         onConfigurationChanged(atm.getGlobalConfiguration());
     }
@@ -1095,9 +1102,7 @@
      * always track the configuration of the non-finishing activity last added to the process.
      */
     private void updateActivityConfigurationListener() {
-        if (mIsSysUiPackage || mUid == Process.SYSTEM_UID) {
-            // This is a system owned process and should not use an activity config.
-            // TODO(b/151161907): Remove after support for display-independent (raw) SysUi configs.
+        if (!mIsActivityConfigOverrideAllowed) {
             return;
         }
 
@@ -1132,7 +1137,7 @@
         final Configuration config = getConfiguration();
         if (mLastReportedConfiguration.diff(config) == 0) {
             // Nothing changed.
-            if (Build.IS_DEBUGGABLE && mIsImeProcess) {
+            if (Build.IS_DEBUGGABLE && mHasImeService) {
                 // TODO (b/135719017): Temporary log for debugging IME service.
                 Slog.w(TAG_CONFIGURATION, "Current config: " + config
                         + " unchanged for IME proc " + mName);
@@ -1156,7 +1161,7 @@
 
     private void dispatchConfigurationChange(Configuration config) {
         if (mThread == null) {
-            if (Build.IS_DEBUGGABLE && mIsImeProcess) {
+            if (Build.IS_DEBUGGABLE && mHasImeService) {
                 // TODO (b/135719017): Temporary log for debugging IME service.
                 Slog.w(TAG_CONFIGURATION, "Unable to send config for IME proc " + mName
                         + ": no app thread");
@@ -1166,7 +1171,7 @@
         if (DEBUG_CONFIGURATION) {
             Slog.v(TAG_CONFIGURATION, "Sending to proc " + mName + " new config " + config);
         }
-        if (Build.IS_DEBUGGABLE && mIsImeProcess) {
+        if (Build.IS_DEBUGGABLE && mHasImeService) {
             // TODO (b/135719017): Temporary log for debugging IME service.
             Slog.v(TAG_CONFIGURATION, "Sending to IME proc " + mName + " new config " + config);
         }
@@ -1286,6 +1291,35 @@
         }
     }
 
+    /**
+     * Called to notify {@link WindowProcessController} of a started service.
+     *
+     * @param serviceInfo information describing the started service.
+     */
+    public void onServiceStarted(ServiceInfo serviceInfo) {
+        String permission = serviceInfo.permission;
+        if (permission == null) {
+            return;
+        }
+
+        // TODO: Audit remaining services for disabling activity override (Wallpaper, Dream, etc).
+        switch (permission) {
+            case Manifest.permission.BIND_INPUT_METHOD:
+                mHasImeService = true;
+                // Fall-through
+            case Manifest.permission.BIND_ACCESSIBILITY_SERVICE:
+            case Manifest.permission.BIND_VOICE_INTERACTION:
+                // We want to avoid overriding the config of these services with that of the
+                // activity as it could lead to incorrect display metrics. For ex, IME services
+                // expect their config to match the config of the display with the IME window
+                // showing.
+                mIsActivityConfigOverrideAllowed = false;
+                break;
+            default:
+                break;
+        }
+    }
+
     @HotPath(caller = HotPath.OOM_ADJUSTMENT)
     public void onTopProcChanged() {
         synchronized (mAtm.mGlobalLockWithoutBoost) {
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 83e7ad5..ef690e1 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -5289,7 +5289,7 @@
         // to account for it. If we actually have shadows we will
         // then un-inset ourselves by the surfaceInsets.
         if (stack != null) {
-            final int outset = stack.getStackOutset();
+            final int outset = stack.getTaskOutset();
             outPoint.offset(outset, outset);
         }
 
diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java
index 9957707..a2930ab 100644
--- a/services/core/java/com/android/server/wm/WindowStateAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java
@@ -1476,6 +1476,7 @@
         if (dumpAll) {
             pw.print(prefix); pw.print("mDrawState="); pw.print(drawStateToString());
             pw.print(prefix); pw.print(" mLastHidden="); pw.println(mLastHidden);
+            pw.print(prefix); pw.print("mEnterAnimationPending=" + mEnterAnimationPending);
             pw.print(prefix); pw.print("mSystemDecorRect="); mSystemDecorRect.printShortString(pw);
             pw.print(" mLastClipRect="); mLastClipRect.printShortString(pw);
 
diff --git a/services/core/jni/com_android_server_tv_TvUinputBridge.cpp b/services/core/jni/com_android_server_tv_TvUinputBridge.cpp
index 6e2e2c5..99deab4 100644
--- a/services/core/jni/com_android_server_tv_TvUinputBridge.cpp
+++ b/services/core/jni/com_android_server_tv_TvUinputBridge.cpp
@@ -106,7 +106,7 @@
 static const GamepadAxis* getGamepadAxis(int32_t androidAxisCode) {
     std::unordered_map<int32_t, int>::iterator it =
             gamepadAndroidAxisToIndexMap.find(androidAxisCode);
-    if (it == gamepadAndroidToLinuxKeyMap.end()) {
+    if (it == gamepadAndroidAxisToIndexMap.end()) {
         return nullptr;
     }
     return &GAMEPAD_AXES[it->second];
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 2c0d4c0..5d5e424 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -161,8 +161,11 @@
 import android.content.ComponentName;
 import android.content.ContentValues;
 import android.content.Context;
+import android.content.IIntentReceiver;
+import android.content.IIntentSender;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.IntentSender;
 import android.content.PermissionChecker;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
@@ -171,6 +174,7 @@
 import android.content.pm.IPackageDataObserver;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageInfo;
+import android.content.pm.PackageInstaller;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.PackageManagerInternal;
@@ -399,11 +403,14 @@
 
     private static final long EXPIRATION_GRACE_PERIOD_MS = 5 * MS_PER_DAY; // 5 days, in ms
     private static final long MANAGED_PROFILE_MAXIMUM_TIME_OFF_THRESHOLD = 3 * MS_PER_DAY;
+    /** When to warn the user about the approaching work profile off deadline: 1 day before */
+    private static final long MANAGED_PROFILE_OFF_WARNING_PERIOD = 1 * MS_PER_DAY;
 
     private static final String ACTION_EXPIRED_PASSWORD_NOTIFICATION =
             "com.android.server.ACTION_EXPIRED_PASSWORD_NOTIFICATION";
 
-    private static final String ACTION_PROFILE_OFF_DEADLINE =
+    @VisibleForTesting
+    static final String ACTION_PROFILE_OFF_DEADLINE =
             "com.android.server.ACTION_PROFILE_OFF_DEADLINE";
 
     private static final String ATTR_PERMISSION_PROVIDER = "permission-provider";
@@ -645,6 +652,13 @@
 
     private static final boolean ENABLE_LOCK_GUARD = true;
 
+    /** Profile off deadline is not set or more than MANAGED_PROFILE_OFF_WARNING_PERIOD away. */
+    private static final int PROFILE_OFF_DEADLINE_DEFAULT = 0;
+    /** Profile off deadline is closer than MANAGED_PROFILE_OFF_WARNING_PERIOD. */
+    private static final int PROFILE_OFF_DEADLINE_WARNING = 1;
+    /** Profile off deadline reached, notify the user that personal apps blocked. */
+    private static final int PROFILE_OFF_DEADLINE_REACHED = 2;
+
     interface Stats {
         int LOCK_GUARD_GUARD = 0;
 
@@ -922,11 +936,12 @@
                     mUserData.remove(userHandle);
                 }
                 handlePackagesChanged(null /* check all admins */, userHandle);
+                updatePersonalAppsSuspensionOnUserStart(userHandle);
             } else if (Intent.ACTION_USER_STOPPED.equals(action)) {
                 sendDeviceOwnerUserCommand(DeviceAdminReceiver.ACTION_USER_STOPPED, userHandle);
                 if (isManagedProfile(userHandle)) {
                     Slog.d(LOG_TAG, "Managed profile was stopped");
-                    updatePersonalAppSuspension(userHandle, false /* profileIsOn */);
+                    updatePersonalAppsSuspension(userHandle, false /* unlocked */);
                 }
             } else if (Intent.ACTION_USER_SWITCHED.equals(action)) {
                 sendDeviceOwnerUserCommand(DeviceAdminReceiver.ACTION_USER_SWITCHED, userHandle);
@@ -936,7 +951,7 @@
                 }
                 if (isManagedProfile(userHandle)) {
                     Slog.d(LOG_TAG, "Managed profile became unlocked");
-                    updatePersonalAppSuspension(userHandle, true /* profileIsOn */);
+                    updatePersonalAppsSuspension(userHandle, true /* unlocked */);
                 }
             } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) {
                 handlePackagesChanged(null /* check all admins */, userHandle);
@@ -963,7 +978,7 @@
                 Slog.i(LOG_TAG, "Profile off deadline alarm was triggered");
                 final int userId = getManagedUserId(UserHandle.USER_SYSTEM);
                 if (userId >= 0) {
-                    updatePersonalAppSuspension(userId, mUserManager.isUserUnlocked(userId));
+                    updatePersonalAppsSuspension(userId, mUserManager.isUserUnlocked(userId));
                 } else {
                     Slog.wtf(LOG_TAG, "Got deadline alarm for nonexistent profile");
                 }
@@ -2482,6 +2497,16 @@
         public void runCryptoSelfTest() {
             CryptoTestHelper.runAndLogSelfTest();
         }
+
+        public String[] getPersonalAppsForSuspension(int userId) {
+            return new PersonalAppsSuspensionHelper(
+                    mContext.createContextAsUser(UserHandle.of(userId), 0 /* flags */))
+                    .getPersonalAppsForSuspension();
+        }
+
+        public long systemCurrentTimeMillis() {
+            return System.currentTimeMillis();
+        }
     }
 
     /**
@@ -2703,8 +2728,9 @@
         final ComponentName doAdminReceiver = doAdmin.info.getComponent();
         clearDeviceOwnerLocked(doAdmin, doUserId);
         Slog.i(LOG_TAG, "Removing admin artifacts...");
-        // TODO(b/149075700): Clean up application restrictions in UserManager.
         removeAdminArtifacts(doAdminReceiver, doUserId);
+        Slog.i(LOG_TAG, "Uninstalling the DO...");
+        uninstallOrDisablePackage(doAdminComponent.getPackageName(), doUserId);
         Slog.i(LOG_TAG, "Migration complete.");
 
         // Note: KeyChain keys are not removed and will remain accessible for the apps that have
@@ -2716,6 +2742,47 @@
                 .write();
     }
 
+    private void uninstallOrDisablePackage(String packageName, int userHandle) {
+        final ApplicationInfo appInfo;
+        try {
+            appInfo = mIPackageManager.getApplicationInfo(
+                    packageName, MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE, userHandle);
+        } catch (RemoteException e) {
+            // Shouldn't happen.
+            return;
+        }
+        if (appInfo == null) {
+            Slog.wtf(LOG_TAG, "Failed to get package info for " + packageName);
+            return;
+        }
+        if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
+            Slog.i(LOG_TAG, String.format(
+                    "Package %s is pre-installed, marking disabled until used", packageName));
+            mContext.getPackageManager().setApplicationEnabledSetting(packageName,
+                    PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED, 0 /* flags */);
+            return;
+        }
+
+        final IIntentSender.Stub mLocalSender = new IIntentSender.Stub() {
+            @Override
+            public void send(int code, Intent intent, String resolvedType, IBinder whitelistToken,
+                    IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) {
+                final int status = intent.getIntExtra(
+                        PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE);
+                if (status == PackageInstaller.STATUS_SUCCESS) {
+                    Slog.i(LOG_TAG, String.format(
+                            "Package %s uninstalled for user %d", packageName, userHandle));
+                } else {
+                    Slog.e(LOG_TAG, String.format(
+                            "Failed to uninstall %s; status: %d", packageName, status));
+                }
+            }
+        };
+
+        final PackageInstaller pi = mInjector.getPackageManager(userHandle).getPackageInstaller();
+        pi.uninstall(packageName, 0 /* flags */, new IntentSender((IIntentSender) mLocalSender));
+    }
+
     private void moveDoPoliciesToProfileParentAdmin(ActiveAdmin doAdmin, ActiveAdmin parentAdmin) {
         // The following policies can be already controlled via parent instance, skip if so.
         if (parentAdmin.mPasswordPolicy.quality == PASSWORD_QUALITY_UNSPECIFIED) {
@@ -3999,10 +4066,6 @@
                     applyManagedProfileRestrictionIfDeviceOwnerLocked();
                 }
                 maybeStartSecurityLogMonitorOnActivityManagerReady();
-                final int userId = getManagedUserId(UserHandle.USER_SYSTEM);
-                if (userId >= 0) {
-                    updatePersonalAppSuspension(userId, false /* running */);
-                }
                 break;
             case SystemService.PHASE_BOOT_COMPLETED:
                 ensureDeviceOwnerUserStarted(); // TODO Consider better place to do this.
@@ -4010,6 +4073,16 @@
         }
     }
 
+    private void updatePersonalAppsSuspensionOnUserStart(int userHandle) {
+        final int profileUserHandle = getManagedUserId(userHandle);
+        if (profileUserHandle >= 0) {
+            // Given that the parent user has just started, profile should be locked.
+            updatePersonalAppsSuspension(profileUserHandle, false /* unlocked */);
+        } else {
+            suspendPersonalAppsInternal(userHandle, false);
+        }
+    }
+
     private void onLockSettingsReady() {
         getUserData(UserHandle.USER_SYSTEM);
         loadOwners();
@@ -8766,6 +8839,8 @@
         saveSettingsLocked(UserHandle.USER_SYSTEM);
         clearUserPoliciesLocked(userId);
         clearOverrideApnUnchecked();
+        clearApplicationRestrictions(userId);
+        mInjector.getPackageManagerInternal().clearBlockUninstallForUser(userId);
 
         mOwners.clearDeviceOwner();
         mOwners.writeDeviceOwner();
@@ -8779,6 +8854,19 @@
         toggleBackupServiceActive(UserHandle.USER_SYSTEM, true);
     }
 
+    private void clearApplicationRestrictions(int userId) {
+        // Changing app restrictions involves disk IO, offload it to the background thread.
+        mBackgroundHandler.post(() -> {
+            final List<PackageInfo> installedPackageInfos = mInjector.getPackageManager(userId)
+                    .getInstalledPackages(MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE);
+            final UserHandle userHandle = UserHandle.of(userId);
+            for (final PackageInfo packageInfo : installedPackageInfos) {
+                mInjector.getUserManager().setApplicationRestrictions(
+                        packageInfo.packageName, null /* restrictions */, userHandle);
+            }
+        });
+    }
+
     @Override
     public boolean setProfileOwner(ComponentName who, String ownerName, int userHandle) {
         if (!mHasFeature) {
@@ -8898,6 +8986,7 @@
         policyData.mOwnerInstalledCaCerts.clear();
         saveSettingsLocked(userId);
         clearUserPoliciesLocked(userId);
+        clearApplicationRestrictions(userId);
         mOwners.removeProfileOwner(userId);
         mOwners.writeProfileOwner(userId);
         deleteTransferOwnershipBundleLocked(userId);
@@ -15831,11 +15920,8 @@
             }
         }
 
-        final int suspendedState = suspended
-                ? PERSONAL_APPS_SUSPENDED_EXPLICITLY
-                : PERSONAL_APPS_NOT_SUSPENDED;
-        mInjector.binderWithCleanCallingIdentity(
-                () -> applyPersonalAppsSuspension(callingUserId, suspendedState));
+        mInjector.binderWithCleanCallingIdentity(() -> updatePersonalAppsSuspension(
+                callingUserId, mUserManager.isUserUnlocked(callingUserId)));
 
         DevicePolicyEventLogger
                 .createEvent(DevicePolicyEnums.SET_PERSONAL_APPS_SUSPENDED)
@@ -15845,44 +15931,54 @@
     }
 
     /**
-     * Checks whether there is a policy that requires personal apps to be suspended and if so,
-     * applies it.
-     * @param running whether the profile is currently considered running.
+     * Checks whether personal apps should be suspended according to the policy and applies the
+     * change if needed.
+     *
+     * @param unlocked whether the profile is currently running unlocked.
      */
-    private void updatePersonalAppSuspension(int profileUserId, boolean running) {
-        final int suspensionState;
+    private void updatePersonalAppsSuspension(int profileUserId, boolean unlocked) {
+        final boolean suspended;
         synchronized (getLockObject()) {
             final ActiveAdmin profileOwner = getProfileOwnerAdminLocked(profileUserId);
             if (profileOwner != null) {
-                final boolean deadlineReached =
-                        updateProfileOffDeadlineLocked(profileUserId, profileOwner, running);
-                suspensionState = makeSuspensionReasons(
-                        profileOwner.mSuspendPersonalApps, deadlineReached);
-                Slog.d(LOG_TAG,
-                        String.format("New personal apps suspension state: %d", suspensionState));
+                final int deadlineState =
+                        updateProfileOffDeadlineLocked(profileUserId, profileOwner, unlocked);
+                suspended = profileOwner.mSuspendPersonalApps
+                        || deadlineState == PROFILE_OFF_DEADLINE_REACHED;
+                Slog.d(LOG_TAG, String.format("Personal apps suspended: %b, deadline state: %d",
+                            suspended, deadlineState));
+                updateProfileOffDeadlineNotificationLocked(profileUserId, profileOwner,
+                        unlocked ? PROFILE_OFF_DEADLINE_DEFAULT : deadlineState);
             } else {
-                suspensionState = PERSONAL_APPS_NOT_SUSPENDED;
+                suspended = false;
             }
         }
 
-        applyPersonalAppsSuspension(profileUserId, suspensionState);
+        final int parentUserId = getProfileParentId(profileUserId);
+        suspendPersonalAppsInternal(parentUserId, suspended);
     }
 
     /**
      * Checks work profile time off policy, scheduling personal apps suspension via alarm if
      * necessary.
-     * @return whether the apps should be suspended based on maximum time off policy.
+     * @return profile deadline state
      */
-    private boolean updateProfileOffDeadlineLocked(
+    private int updateProfileOffDeadlineLocked(
             int profileUserId, ActiveAdmin profileOwner, boolean unlocked) {
-        final long now = System.currentTimeMillis();
+        final long now = mInjector.systemCurrentTimeMillis();
         if (profileOwner.mProfileOffDeadline != 0 && now > profileOwner.mProfileOffDeadline) {
             // Profile off deadline is already reached.
             Slog.i(LOG_TAG, "Profile off deadline has been reached.");
-            return true;
+            return PROFILE_OFF_DEADLINE_REACHED;
         }
         boolean shouldSaveSettings = false;
-        if (profileOwner.mProfileOffDeadline != 0
+        if (profileOwner.mSuspendPersonalApps) {
+            // When explicit suspension is active, deadline shouldn't be set.
+            if (profileOwner.mProfileOffDeadline != 0) {
+                profileOwner.mProfileOffDeadline = 0;
+                shouldSaveSettings = true;
+            }
+        } else if (profileOwner.mProfileOffDeadline != 0
                 && (profileOwner.mProfileMaximumTimeOffMillis == 0 || unlocked)) {
             // There is a deadline but either there is no policy or the profile is unlocked -> clear
             // the deadline.
@@ -15898,52 +15994,51 @@
             shouldSaveSettings = true;
         }
 
-        updateProfileOffAlarm(profileOwner.mProfileOffDeadline);
-
         if (shouldSaveSettings) {
             saveSettingsLocked(profileUserId);
         }
-        return false;
-    }
 
-    private void updateProfileOffAlarm(long profileOffDeadline) {
+        final long alarmTime;
+        final int deadlineState;
+        if (profileOwner.mProfileOffDeadline == 0) {
+            alarmTime = 0;
+            deadlineState = PROFILE_OFF_DEADLINE_DEFAULT;
+        } else if (profileOwner.mProfileOffDeadline - now < MANAGED_PROFILE_OFF_WARNING_PERIOD) {
+            // The deadline is close, upon the alarm personal apps should be suspended.
+            alarmTime = profileOwner.mProfileOffDeadline;
+            deadlineState = PROFILE_OFF_DEADLINE_WARNING;
+        } else {
+            // The deadline is quite far, upon the alarm we should warn the user first, so the
+            // alarm is scheduled earlier than the actual deadline.
+            alarmTime = profileOwner.mProfileOffDeadline - MANAGED_PROFILE_OFF_WARNING_PERIOD;
+            deadlineState = PROFILE_OFF_DEADLINE_DEFAULT;
+        }
+
         final AlarmManager am = mInjector.getAlarmManager();
         final PendingIntent pi = mInjector.pendingIntentGetBroadcast(
                 mContext, REQUEST_PROFILE_OFF_DEADLINE, new Intent(ACTION_PROFILE_OFF_DEADLINE),
                 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
-        am.cancel(pi);
-        if (profileOffDeadline != 0) {
-            Slog.i(LOG_TAG, "Profile off deadline alarm is set.");
-            am.set(AlarmManager.RTC, profileOffDeadline, pi);
-        } else {
+
+        if (alarmTime == 0) {
             Slog.i(LOG_TAG, "Profile off deadline alarm is removed.");
-        }
-    }
-
-    private void applyPersonalAppsSuspension(
-            int profileUserId, @PersonalAppsSuspensionReason int suspensionState) {
-        final boolean suspended = getUserData(UserHandle.USER_SYSTEM).mAppsSuspended;
-        final boolean shouldSuspend = suspensionState != PERSONAL_APPS_NOT_SUSPENDED;
-        if (suspended != shouldSuspend) {
-            suspendPersonalAppsInternal(shouldSuspend, UserHandle.USER_SYSTEM);
-        }
-
-        if (suspensionState == PERSONAL_APPS_SUSPENDED_PROFILE_TIMEOUT) {
-            sendPersonalAppsSuspendedNotification(profileUserId);
+            am.cancel(pi);
         } else {
-            clearPersonalAppsSuspendedNotification();
+            Slog.i(LOG_TAG, "Profile off deadline alarm is set.");
+            am.set(AlarmManager.RTC, alarmTime, pi);
         }
+
+        return deadlineState;
     }
 
-    private void suspendPersonalAppsInternal(boolean suspended, int userId) {
+    private void suspendPersonalAppsInternal(int userId, boolean suspended) {
+        if (getUserData(userId).mAppsSuspended == suspended) {
+            return;
+        }
         Slog.i(LOG_TAG, String.format("%s personal apps for user %d",
                 suspended ? "Suspending" : "Unsuspending", userId));
         mInjector.binderWithCleanCallingIdentity(() -> {
             try {
-                final String[] appsToSuspend =
-                        new PersonalAppsSuspensionHelper(
-                                mContext.createContextAsUser(UserHandle.of(userId), 0 /* flags */))
-                                .getPersonalAppsForSuspension();
+                final String[] appsToSuspend = mInjector.getPersonalAppsForSuspension(userId);
                 final String[] failedPackages = mIPackageManager.setPackagesSuspendedAsUser(
                         appsToSuspend, suspended, null, null, null, PLATFORM_PACKAGE_NAME, userId);
                 if (!ArrayUtils.isEmpty(failedPackages)) {
@@ -15962,37 +16057,38 @@
         }
     }
 
-    private void clearPersonalAppsSuspendedNotification() {
-        mInjector.binderWithCleanCallingIdentity(() ->
-                mInjector.getNotificationManager().cancel(
-                        SystemMessage.NOTE_PERSONAL_APPS_SUSPENDED));
-    }
+    private void updateProfileOffDeadlineNotificationLocked(int profileUserId,
+            @Nullable ActiveAdmin profileOwner, int notificationState) {
 
-    private void sendPersonalAppsSuspendedNotification(int userId) {
-        final String profileOwnerPackageName;
-        final long maxTimeOffDays;
-        synchronized (getLockObject()) {
-            profileOwnerPackageName = mOwners.getProfileOwnerComponent(userId).getPackageName();
-            final ActiveAdmin poAdmin = getProfileOwnerAdminLocked(userId);
-            maxTimeOffDays = TimeUnit.MILLISECONDS.toDays(poAdmin.mProfileMaximumTimeOffMillis);
+        if (notificationState == PROFILE_OFF_DEADLINE_DEFAULT) {
+            mInjector.getNotificationManager().cancel(SystemMessage.NOTE_PERSONAL_APPS_SUSPENDED);
+            return;
         }
 
+        final String profileOwnerPackageName = profileOwner.info.getPackageName();
+        final long maxTimeOffDays =
+                TimeUnit.MILLISECONDS.toDays(profileOwner.mProfileMaximumTimeOffMillis);
+
         final Intent intent = new Intent(DevicePolicyManager.ACTION_CHECK_POLICY_COMPLIANCE);
         intent.setPackage(profileOwnerPackageName);
 
         final PendingIntent pendingIntent = mInjector.pendingIntentGetActivityAsUser(mContext,
-                0 /* requestCode */, intent, PendingIntent.FLAG_UPDATE_CURRENT, null /* options */,
-                UserHandle.of(userId));
+                0 /* requestCode */, intent, PendingIntent.FLAG_UPDATE_CURRENT,
+                null /* options */, UserHandle.of(profileUserId));
+
+        // TODO(b/149075510): Only the first of the notifications should be dismissible.
+        final String title = mContext.getString(
+                notificationState == PROFILE_OFF_DEADLINE_WARNING
+                ? R.string.personal_apps_suspended_tomorrow_title
+                : R.string.personal_apps_suspended_title);
 
         final Notification notification =
                 new Notification.Builder(mContext, SystemNotificationChannels.DEVICE_ADMIN)
                         .setSmallIcon(android.R.drawable.stat_sys_warning)
                         .setOngoing(true)
-                        .setContentTitle(
-                                mContext.getString(
-                                        R.string.personal_apps_suspended_title))
+                        .setContentTitle(title)
                         .setContentText(mContext.getString(
-                                R.string.personal_apps_suspended_text, maxTimeOffDays))
+                            R.string.personal_apps_suspended_text, maxTimeOffDays))
                         .setColor(mContext.getColor(R.color.system_notification_accent_color))
                         .setContentIntent(pendingIntent)
                         .build();
@@ -16024,7 +16120,7 @@
         }
 
         mInjector.binderWithCleanCallingIdentity(
-                () -> updatePersonalAppSuspension(userId, mUserManager.isUserUnlocked()));
+                () -> updatePersonalAppsSuspension(userId, mUserManager.isUserUnlocked()));
 
         DevicePolicyEventLogger
                 .createEvent(DevicePolicyEnums.SET_MANAGED_PROFILE_MAXIMUM_TIME_OFF)
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java b/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java
index d9db17e..da716ea 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java
@@ -51,6 +51,10 @@
 public class PersonalAppsSuspensionHelper {
     private static final String LOG_TAG = DevicePolicyManagerService.LOG_TAG;
 
+    // Flags to get all packages even if the user is still locked.
+    private static final int PACKAGE_QUERY_FLAGS =
+            PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
+
     private final Context mContext;
     private final PackageManager mPackageManager;
 
@@ -67,7 +71,7 @@
      */
     String[] getPersonalAppsForSuspension() {
         final List<PackageInfo> installedPackageInfos =
-                mPackageManager.getInstalledPackages(0 /* flags */);
+                mPackageManager.getInstalledPackages(PACKAGE_QUERY_FLAGS);
         final Set<String> result = new ArraySet<>();
         for (final PackageInfo packageInfo : installedPackageInfos) {
             final ApplicationInfo info = packageInfo.applicationInfo;
@@ -97,7 +101,7 @@
         final Intent intent = new Intent(Intent.ACTION_MAIN);
         intent.addCategory(Intent.CATEGORY_HOME);
         final List<ResolveInfo> matchingActivities =
-                mPackageManager.queryIntentActivities(intent, 0);
+                mPackageManager.queryIntentActivities(intent, PACKAGE_QUERY_FLAGS);
         for (final ResolveInfo resolveInfo : matchingActivities) {
             if (resolveInfo.activityInfo == null
                     || TextUtils.isEmpty(resolveInfo.activityInfo.packageName)) {
@@ -107,7 +111,7 @@
             final String packageName = resolveInfo.activityInfo.packageName;
             try {
                 final ApplicationInfo applicationInfo =
-                        mPackageManager.getApplicationInfo(packageName, 0);
+                        mPackageManager.getApplicationInfo(packageName, PACKAGE_QUERY_FLAGS);
                 if (applicationInfo.isSystemApp() || applicationInfo.isUpdatedSystemApp()) {
                     result.add(packageName);
                 }
@@ -147,7 +151,8 @@
     private String getSettingsPackageName() {
         final Intent intent = new Intent(Settings.ACTION_SETTINGS);
         intent.addCategory(Intent.CATEGORY_DEFAULT);
-        final ResolveInfo resolveInfo = mPackageManager.resolveActivity(intent, /* flags= */ 0);
+        final ResolveInfo resolveInfo =
+                mPackageManager.resolveActivity(intent, PACKAGE_QUERY_FLAGS);
         if (resolveInfo != null) {
             return resolveInfo.activityInfo.packageName;
         }
@@ -164,7 +169,7 @@
         intentToResolve.addCategory(Intent.CATEGORY_LAUNCHER);
         intentToResolve.setPackage(packageName);
         final List<ResolveInfo> resolveInfos =
-                mPackageManager.queryIntentActivities(intentToResolve, /* flags= */ 0);
+                mPackageManager.queryIntentActivities(intentToResolve, PACKAGE_QUERY_FLAGS);
         return resolveInfos != null && !resolveInfos.isEmpty();
     }
 
diff --git a/services/incremental/Android.bp b/services/incremental/Android.bp
index b13d330..b8bd3b4 100644
--- a/services/incremental/Android.bp
+++ b/services/incremental/Android.bp
@@ -62,6 +62,7 @@
     srcs: [
         "incremental_service.c",
         "IncrementalService.cpp",
+        "IncrementalServiceValidation.cpp",
         "BinderIncrementalService.cpp",
         "path.cpp",
         "ServiceWrappers.cpp",
diff --git a/services/incremental/BinderIncrementalService.cpp b/services/incremental/BinderIncrementalService.cpp
index fc8c6fe..8476674 100644
--- a/services/incremental/BinderIncrementalService.cpp
+++ b/services/incremental/BinderIncrementalService.cpp
@@ -18,6 +18,7 @@
 
 #include <android-base/logging.h>
 #include <android-base/no_destructor.h>
+#include <android/os/IVold.h>
 #include <binder/IResultReceiver.h>
 #include <binder/PermissionCache.h>
 #include <incfs.h>
@@ -117,11 +118,12 @@
 }
 
 binder::Status BinderIncrementalService::createStorage(
-        const std::string& path, const DataLoaderParamsParcel& params,
+        const std::string& path, const content::pm::DataLoaderParamsParcel& params,
         const ::android::sp<::android::content::pm::IDataLoaderStatusListener>& listener,
         int32_t createMode, int32_t* _aidl_return) {
     *_aidl_return =
-            mImpl.createStorage(path, const_cast<DataLoaderParamsParcel&&>(params), listener,
+            mImpl.createStorage(path, const_cast<content::pm::DataLoaderParamsParcel&&>(params),
+                                listener,
                                 android::incremental::IncrementalService::CreateOptions(
                                         createMode));
     return ok();
@@ -238,11 +240,8 @@
 binder::Status BinderIncrementalService::getMetadataByPath(int32_t storageId,
                                                            const std::string& path,
                                                            std::vector<uint8_t>* _aidl_return) {
-    auto fid = mImpl.nodeFor(storageId, path);
-    if (fid != kIncFsInvalidFileId) {
-        auto metadata = mImpl.getMetadata(storageId, fid);
-        _aidl_return->assign(metadata.begin(), metadata.end());
-    }
+    auto metadata = mImpl.getMetadata(storageId, path);
+    _aidl_return->assign(metadata.begin(), metadata.end());
     return ok();
 }
 
diff --git a/services/incremental/IncrementalService.cpp b/services/incremental/IncrementalService.cpp
index 79699bb..0641872 100644
--- a/services/incremental/IncrementalService.cpp
+++ b/services/incremental/IncrementalService.cpp
@@ -18,32 +18,26 @@
 
 #include "IncrementalService.h"
 
-#include <android-base/file.h>
 #include <android-base/logging.h>
 #include <android-base/no_destructor.h>
 #include <android-base/properties.h>
 #include <android-base/stringprintf.h>
-#include <android-base/strings.h>
-#include <android/content/pm/IDataLoaderStatusListener.h>
-#include <android/os/IVold.h>
-#include <binder/BinderService.h>
+#include <binder/AppOpsManager.h>
 #include <binder/Nullable.h>
-#include <binder/ParcelFileDescriptor.h>
 #include <binder/Status.h>
 #include <sys/stat.h>
 #include <uuid/uuid.h>
 
 #include <charconv>
 #include <ctime>
-#include <filesystem>
 #include <iterator>
 #include <span>
 #include <type_traits>
 
+#include "IncrementalServiceValidation.h"
 #include "Metadata.pb.h"
 
 using namespace std::literals;
-using namespace android::content::pm;
 namespace fs = std::filesystem;
 
 constexpr const char* kDataUsageStats = "android.permission.LOADER_USAGE_STATS";
@@ -51,10 +45,13 @@
 
 namespace android::incremental {
 
+using content::pm::DataLoaderParamsParcel;
+using content::pm::FileSystemControlParcel;
+using content::pm::IDataLoader;
+
 namespace {
 
-using IncrementalFileSystemControlParcel =
-        ::android::os::incremental::IncrementalFileSystemControlParcel;
+using IncrementalFileSystemControlParcel = os::incremental::IncrementalFileSystemControlParcel;
 
 struct Constants {
     static constexpr auto backing = "backing_store"sv;
@@ -105,10 +102,13 @@
     if (path::isAbsolute(path)) {
         path.remove_prefix(1);
     }
+    if (path.size() > 16) {
+        path = path.substr(0, 16);
+    }
     std::string res(path);
-    std::replace(res.begin(), res.end(), '/', '_');
-    std::replace(res.begin(), res.end(), '@', '_');
-    return std::string(constants().mountKeyPrefix) + res;
+    std::replace_if(
+            res.begin(), res.end(), [](char c) { return c == '/' || c == '@'; }, '_');
+    return std::string(constants().mountKeyPrefix) += res;
 }
 
 static std::pair<std::string, std::string> makeMountDir(std::string_view incrementalDir,
@@ -125,8 +125,26 @@
     return {};
 }
 
+template <class Map>
+typename Map::const_iterator findParentPath(const Map& map, std::string_view path) {
+    const auto nextIt = map.upper_bound(path);
+    if (nextIt == map.begin()) {
+        return map.end();
+    }
+    const auto suspectIt = std::prev(nextIt);
+    if (!path::startsWith(path, suspectIt->first)) {
+        return map.end();
+    }
+    return suspectIt;
+}
+
+static base::unique_fd dup(base::borrowed_fd fd) {
+    const auto res = fcntl(fd.get(), F_DUPFD_CLOEXEC, 0);
+    return base::unique_fd(res);
+}
+
 template <class ProtoMessage, class Control>
-static ProtoMessage parseFromIncfs(const IncFsWrapper* incfs, Control&& control,
+static ProtoMessage parseFromIncfs(const IncFsWrapper* incfs, const Control& control,
                                    std::string_view path) {
     auto md = incfs->getMetadata(control, path);
     ProtoMessage message;
@@ -155,20 +173,18 @@
 }
 } // namespace
 
-const bool IncrementalService::sEnablePerfLogging =
-        android::base::GetBoolProperty("incremental.perflogging", false);
-
 IncrementalService::IncFsMount::~IncFsMount() {
     if (dataLoaderStub) {
         dataLoaderStub->cleanupResources();
         dataLoaderStub = {};
     }
+    control.close();
     LOG(INFO) << "Unmounting and cleaning up mount " << mountId << " with root '" << root << '\'';
     for (auto&& [target, _] : bindPoints) {
-        LOG(INFO) << "\tbind: " << target;
+        LOG(INFO) << "  bind: " << target;
         incrementalService.mVold->unmountIncFs(target);
     }
-    LOG(INFO) << "\troot: " << root;
+    LOG(INFO) << "  root: " << root;
     incrementalService.mVold->unmountIncFs(path::join(root, constants().mount));
     cleanupFilesystem(root);
 }
@@ -193,8 +209,18 @@
     return storages.end();
 }
 
-static std::unique_ptr<DIR, decltype(&::closedir)> openDir(const char* path) {
-    return {::opendir(path), ::closedir};
+template <class Func>
+static auto makeCleanup(Func&& f) {
+    auto deleter = [f = std::move(f)](auto) { f(); };
+    return std::unique_ptr<Func, decltype(deleter)>(&f, std::move(deleter));
+}
+
+static std::unique_ptr<DIR, decltype(&::closedir)> openDir(const char* dir) {
+    return {::opendir(dir), ::closedir};
+}
+
+static auto openDir(std::string_view dir) {
+    return openDir(path::c_str(dir));
 }
 
 static int rmDirContent(const char* path) {
@@ -206,7 +232,7 @@
         if (entry->d_name == "."sv || entry->d_name == ".."sv) {
             continue;
         }
-        auto fullPath = android::base::StringPrintf("%s/%s", path, entry->d_name);
+        auto fullPath = base::StringPrintf("%s/%s", path, entry->d_name);
         if (entry->d_type == DT_DIR) {
             if (const auto err = rmDirContent(fullPath.c_str()); err != 0) {
                 PLOG(WARNING) << "Failed to delete " << fullPath << " content";
@@ -256,7 +282,8 @@
         runJobProcessing();
     });
 
-    mountExistingImages();
+    const auto mountedRootNames = adoptMountedInstances();
+    mountExistingImages(mountedRootNames);
 }
 
 IncrementalService::~IncrementalService() {
@@ -268,15 +295,7 @@
     mJobProcessor.join();
 }
 
-inline const char* toString(TimePoint t) {
-    using SystemClock = std::chrono::system_clock;
-    time_t time = SystemClock::to_time_t(
-            SystemClock::now() +
-            std::chrono::duration_cast<SystemClock::duration>(t - Clock::now()));
-    return std::ctime(&time);
-}
-
-inline const char* toString(IncrementalService::BindKind kind) {
+static const char* toString(IncrementalService::BindKind kind) {
     switch (kind) {
         case IncrementalService::BindKind::Temporary:
             return "Temporary";
@@ -291,38 +310,48 @@
 
     std::unique_lock l(mLock);
 
-    dprintf(fd, "Mounts (%d):\n", int(mMounts.size()));
+    dprintf(fd, "Mounts (%d): {\n", int(mMounts.size()));
     for (auto&& [id, ifs] : mMounts) {
-        const IncFsMount& mnt = *ifs.get();
-        dprintf(fd, "\t[%d]:\n", id);
-        dprintf(fd, "\t\tmountId: %d\n", mnt.mountId);
-        dprintf(fd, "\t\troot: %s\n", mnt.root.c_str());
-        dprintf(fd, "\t\tnextStorageDirNo: %d\n", mnt.nextStorageDirNo.load());
-        if (mnt.dataLoaderStub) {
-            mnt.dataLoaderStub->onDump(fd);
-        }
-        dprintf(fd, "\t\tstorages (%d):\n", int(mnt.storages.size()));
-        for (auto&& [storageId, storage] : mnt.storages) {
-            dprintf(fd, "\t\t\t[%d] -> [%s]\n", storageId, storage.name.c_str());
-        }
+        const IncFsMount& mnt = *ifs;
+        dprintf(fd, "  [%d]: {\n", id);
+        if (id != mnt.mountId) {
+            dprintf(fd, "    reference to mountId: %d\n", mnt.mountId);
+        } else {
+            dprintf(fd, "    mountId: %d\n", mnt.mountId);
+            dprintf(fd, "    root: %s\n", mnt.root.c_str());
+            dprintf(fd, "    nextStorageDirNo: %d\n", mnt.nextStorageDirNo.load());
+            if (mnt.dataLoaderStub) {
+                mnt.dataLoaderStub->onDump(fd);
+            } else {
+                dprintf(fd, "    dataLoader: null\n");
+            }
+            dprintf(fd, "    storages (%d): {\n", int(mnt.storages.size()));
+            for (auto&& [storageId, storage] : mnt.storages) {
+                dprintf(fd, "      [%d] -> [%s]\n", storageId, storage.name.c_str());
+            }
+            dprintf(fd, "    }\n");
 
-        dprintf(fd, "\t\tbindPoints (%d):\n", int(mnt.bindPoints.size()));
-        for (auto&& [target, bind] : mnt.bindPoints) {
-            dprintf(fd, "\t\t\t[%s]->[%d]:\n", target.c_str(), bind.storage);
-            dprintf(fd, "\t\t\t\tsavedFilename: %s\n", bind.savedFilename.c_str());
-            dprintf(fd, "\t\t\t\tsourceDir: %s\n", bind.sourceDir.c_str());
-            dprintf(fd, "\t\t\t\tkind: %s\n", toString(bind.kind));
+            dprintf(fd, "    bindPoints (%d): {\n", int(mnt.bindPoints.size()));
+            for (auto&& [target, bind] : mnt.bindPoints) {
+                dprintf(fd, "      [%s]->[%d]:\n", target.c_str(), bind.storage);
+                dprintf(fd, "        savedFilename: %s\n", bind.savedFilename.c_str());
+                dprintf(fd, "        sourceDir: %s\n", bind.sourceDir.c_str());
+                dprintf(fd, "        kind: %s\n", toString(bind.kind));
+            }
+            dprintf(fd, "    }\n");
         }
+        dprintf(fd, "  }\n");
     }
-
-    dprintf(fd, "Sorted binds (%d):\n", int(mBindsByPath.size()));
+    dprintf(fd, "}\n");
+    dprintf(fd, "Sorted binds (%d): {\n", int(mBindsByPath.size()));
     for (auto&& [target, mountPairIt] : mBindsByPath) {
         const auto& bind = mountPairIt->second;
-        dprintf(fd, "\t\t[%s]->[%d]:\n", target.c_str(), bind.storage);
-        dprintf(fd, "\t\t\tsavedFilename: %s\n", bind.savedFilename.c_str());
-        dprintf(fd, "\t\t\tsourceDir: %s\n", bind.sourceDir.c_str());
-        dprintf(fd, "\t\t\tkind: %s\n", toString(bind.kind));
+        dprintf(fd, "    [%s]->[%d]:\n", target.c_str(), bind.storage);
+        dprintf(fd, "      savedFilename: %s\n", bind.savedFilename.c_str());
+        dprintf(fd, "      sourceDir: %s\n", bind.sourceDir.c_str());
+        dprintf(fd, "      kind: %s\n", toString(bind.kind));
     }
+    dprintf(fd, "}\n");
 }
 
 void IncrementalService::onSystemReady() {
@@ -471,9 +500,9 @@
         metadata::Mount m;
         m.mutable_storage()->set_id(ifs->mountId);
         m.mutable_loader()->set_type((int)dataLoaderParams.type);
-        m.mutable_loader()->set_package_name(dataLoaderParams.packageName);
-        m.mutable_loader()->set_class_name(dataLoaderParams.className);
-        m.mutable_loader()->set_arguments(dataLoaderParams.arguments);
+        m.mutable_loader()->set_allocated_package_name(&dataLoaderParams.packageName);
+        m.mutable_loader()->set_allocated_class_name(&dataLoaderParams.className);
+        m.mutable_loader()->set_allocated_arguments(&dataLoaderParams.arguments);
         const auto metadata = m.SerializeAsString();
         m.mutable_loader()->release_arguments();
         m.mutable_loader()->release_class_name();
@@ -528,7 +557,7 @@
     }
 
     std::unique_lock l(mLock);
-    const auto& ifs = getIfsLocked(linkedStorage);
+    auto ifs = getIfsLocked(linkedStorage);
     if (!ifs) {
         LOG(ERROR) << "Ifs unavailable";
         return kInvalidStorageId;
@@ -552,6 +581,8 @@
                                 bk, l);
         err < 0) {
         LOG(ERROR) << "bindMount failed with error: " << err;
+        (void)mIncFs->unlink(ifs->control, storageIt->second.name);
+        ifs->storages.erase(storageIt);
         return kInvalidStorageId;
     }
 
@@ -561,15 +592,7 @@
 
 IncrementalService::BindPathMap::const_iterator IncrementalService::findStorageLocked(
         std::string_view path) const {
-    auto bindPointIt = mBindsByPath.upper_bound(path);
-    if (bindPointIt == mBindsByPath.begin()) {
-        return mBindsByPath.end();
-    }
-    --bindPointIt;
-    if (!path::startsWith(path, bindPointIt->first)) {
-        return mBindsByPath.end();
-    }
-    return bindPointIt;
+    return findParentPath(mBindsByPath, path);
 }
 
 StorageId IncrementalService::findStorageId(std::string_view path) const {
@@ -611,13 +634,12 @@
 }
 
 binder::Status IncrementalService::applyStorageParams(IncFsMount& ifs, bool enableReadLogs) {
-    using unique_fd = ::android::base::unique_fd;
-    ::android::os::incremental::IncrementalFileSystemControlParcel control;
-    control.cmd.reset(unique_fd(dup(ifs.control.cmd())));
-    control.pendingReads.reset(unique_fd(dup(ifs.control.pendingReads())));
+    os::incremental::IncrementalFileSystemControlParcel control;
+    control.cmd.reset(dup(ifs.control.cmd()));
+    control.pendingReads.reset(dup(ifs.control.pendingReads()));
     auto logsFd = ifs.control.logs();
     if (logsFd >= 0) {
-        control.log.reset(unique_fd(dup(logsFd)));
+        control.log.reset(dup(logsFd));
     }
 
     std::lock_guard l(mMountOperationLock);
@@ -664,38 +686,6 @@
     return findStorageId(path::normalize(pathInMount));
 }
 
-FileId IncrementalService::nodeFor(StorageId storage, std::string_view subpath) const {
-    const auto ifs = getIfs(storage);
-    if (!ifs) {
-        return kIncFsInvalidFileId;
-    }
-    std::unique_lock l(ifs->lock);
-    auto storageIt = ifs->storages.find(storage);
-    if (storageIt == ifs->storages.end()) {
-        return kIncFsInvalidFileId;
-    }
-    if (subpath.empty() || subpath == "."sv) {
-        return kIncFsInvalidFileId;
-    }
-    auto path = path::join(ifs->root, constants().mount, storageIt->second.name, subpath);
-    l.unlock();
-    return mIncFs->getFileId(ifs->control, path);
-}
-
-std::pair<FileId, std::string_view> IncrementalService::parentAndNameFor(
-        StorageId storage, std::string_view subpath) const {
-    auto name = path::basename(subpath);
-    if (name.empty()) {
-        return {kIncFsInvalidFileId, {}};
-    }
-    auto dir = path::dirname(subpath);
-    if (dir.empty() || dir == "/"sv) {
-        return {kIncFsInvalidFileId, {}};
-    }
-    auto id = nodeFor(storage, dir);
-    return {id, name};
-}
-
 IncrementalService::IfsMountPtr IncrementalService::getIfs(StorageId storage) const {
     std::lock_guard l(mLock);
     return getIfsLocked(storage);
@@ -704,7 +694,7 @@
 const IncrementalService::IfsMountPtr& IncrementalService::getIfsLocked(StorageId storage) const {
     auto it = mMounts.find(storage);
     if (it == mMounts.end()) {
-        static const android::base::NoDestructor<IfsMountPtr> kEmpty{};
+        static const base::NoDestructor<IfsMountPtr> kEmpty{};
         return *kEmpty;
     }
     return it->second;
@@ -713,21 +703,25 @@
 int IncrementalService::bind(StorageId storage, std::string_view source, std::string_view target,
                              BindKind kind) {
     if (!isValidMountTarget(target)) {
+        LOG(ERROR) << __func__ << ": not a valid bind target " << target;
         return -EINVAL;
     }
 
     const auto ifs = getIfs(storage);
     if (!ifs) {
+        LOG(ERROR) << __func__ << ": no ifs object for storage " << storage;
         return -EINVAL;
     }
 
     std::unique_lock l(ifs->lock);
     const auto storageInfo = ifs->storages.find(storage);
     if (storageInfo == ifs->storages.end()) {
+        LOG(ERROR) << "no storage";
         return -EINVAL;
     }
-    std::string normSource = normalizePathToStorageLocked(storageInfo, source);
+    std::string normSource = normalizePathToStorageLocked(ifs, storageInfo, source);
     if (normSource.empty()) {
+        LOG(ERROR) << "invalid source path";
         return -EINVAL;
     }
     l.unlock();
@@ -779,27 +773,32 @@
 }
 
 std::string IncrementalService::normalizePathToStorageLocked(
-        IncFsMount::StorageMap::iterator storageIt, std::string_view path) {
-    std::string normPath;
-    if (path::isAbsolute(path)) {
-        normPath = path::normalize(path);
-        if (!path::startsWith(normPath, storageIt->second.name)) {
-            return {};
-        }
-    } else {
-        normPath = path::normalize(path::join(storageIt->second.name, path));
+        const IfsMountPtr& incfs, IncFsMount::StorageMap::iterator storageIt,
+        std::string_view path) const {
+    if (!path::isAbsolute(path)) {
+        return path::normalize(path::join(storageIt->second.name, path));
     }
-    return normPath;
+    auto normPath = path::normalize(path);
+    if (path::startsWith(normPath, storageIt->second.name)) {
+        return normPath;
+    }
+    // not that easy: need to find if any of the bind points match
+    const auto bindIt = findParentPath(incfs->bindPoints, normPath);
+    if (bindIt == incfs->bindPoints.end()) {
+        return {};
+    }
+    return path::join(bindIt->second.sourceDir, path::relativize(bindIt->first, normPath));
 }
 
 std::string IncrementalService::normalizePathToStorage(const IncrementalService::IfsMountPtr& ifs,
-                                                       StorageId storage, std::string_view path) {
+                                                       StorageId storage,
+                                                       std::string_view path) const {
     std::unique_lock l(ifs->lock);
     const auto storageInfo = ifs->storages.find(storage);
     if (storageInfo == ifs->storages.end()) {
         return {};
     }
-    return normalizePathToStorageLocked(storageInfo, path);
+    return normalizePathToStorageLocked(ifs, storageInfo, path);
 }
 
 int IncrementalService::makeFile(StorageId storage, std::string_view path, int mode, FileId id,
@@ -844,7 +843,8 @@
     auto err = mIncFs->makeDir(ifs->control, normPath, mode);
     if (err == -EEXIST) {
         return 0;
-    } else if (err != -ENOENT) {
+    }
+    if (err != -ENOENT) {
         return err;
     }
     if (auto err = makeDirs(storageId, path::dirname(normPath), mode)) {
@@ -855,17 +855,22 @@
 
 int IncrementalService::link(StorageId sourceStorageId, std::string_view oldPath,
                              StorageId destStorageId, std::string_view newPath) {
-    auto ifsSrc = getIfs(sourceStorageId);
-    auto ifsDest = sourceStorageId == destStorageId ? ifsSrc : getIfs(destStorageId);
-    if (ifsSrc && ifsSrc == ifsDest) {
-        std::string normOldPath = normalizePathToStorage(ifsSrc, sourceStorageId, oldPath);
-        std::string normNewPath = normalizePathToStorage(ifsDest, destStorageId, newPath);
-        if (normOldPath.empty() || normNewPath.empty()) {
-            return -EINVAL;
-        }
-        return mIncFs->link(ifsSrc->control, normOldPath, normNewPath);
+    std::unique_lock l(mLock);
+    auto ifsSrc = getIfsLocked(sourceStorageId);
+    if (!ifsSrc) {
+        return -EINVAL;
     }
-    return -EINVAL;
+    if (sourceStorageId != destStorageId && getIfsLocked(destStorageId) != ifsSrc) {
+        return -EINVAL;
+    }
+    l.unlock();
+    std::string normOldPath = normalizePathToStorage(ifsSrc, sourceStorageId, oldPath);
+    std::string normNewPath = normalizePathToStorage(ifsSrc, destStorageId, newPath);
+    if (normOldPath.empty() || normNewPath.empty()) {
+        LOG(ERROR) << "Invalid paths in link(): " << normOldPath << " | " << normNewPath;
+        return -EINVAL;
+    }
+    return mIncFs->link(ifsSrc->control, normOldPath, normNewPath);
 }
 
 int IncrementalService::unlink(StorageId storage, std::string_view path) {
@@ -881,10 +886,12 @@
                                      std::string&& target, BindKind kind,
                                      std::unique_lock<std::mutex>& mainLock) {
     if (!isValidMountTarget(target)) {
+        LOG(ERROR) << __func__ << ": invalid mount target " << target;
         return -EINVAL;
     }
 
     std::string mdFileName;
+    std::string metadataFullPath;
     if (kind != BindKind::Temporary) {
         metadata::BindPoint bp;
         bp.set_storage_id(storage);
@@ -894,17 +901,21 @@
         bp.release_dest_path();
         bp.release_source_subdir();
         mdFileName = makeBindMdName();
-        auto node =
-                mIncFs->makeFile(ifs.control, path::join(ifs.root, constants().mount, mdFileName),
-                                 0444, idFromMetadata(metadata),
-                                 {.metadata = {metadata.data(), (IncFsSize)metadata.size()}});
+        metadataFullPath = path::join(ifs.root, constants().mount, mdFileName);
+        auto node = mIncFs->makeFile(ifs.control, metadataFullPath, 0444, idFromMetadata(metadata),
+                                     {.metadata = {metadata.data(), (IncFsSize)metadata.size()}});
         if (node) {
+            LOG(ERROR) << __func__ << ": couldn't create a mount node " << mdFileName;
             return int(node);
         }
     }
 
-    return addBindMountWithMd(ifs, storage, std::move(mdFileName), std::move(source),
-                              std::move(target), kind, mainLock);
+    const auto res = addBindMountWithMd(ifs, storage, std::move(mdFileName), std::move(source),
+                                        std::move(target), kind, mainLock);
+    if (res) {
+        mIncFs->unlink(ifs.control, metadataFullPath);
+    }
+    return res;
 }
 
 int IncrementalService::addBindMountWithMd(IncrementalService::IncFsMount& ifs, StorageId storage,
@@ -929,12 +940,31 @@
         mainLock.lock();
     }
     std::lock_guard l(ifs.lock);
+    addBindMountRecordLocked(ifs, storage, std::move(metadataName), std::move(source),
+                             std::move(target), kind);
+    return 0;
+}
+
+void IncrementalService::addBindMountRecordLocked(IncFsMount& ifs, StorageId storage,
+                                                  std::string&& metadataName, std::string&& source,
+                                                  std::string&& target, BindKind kind) {
     const auto [it, _] =
             ifs.bindPoints.insert_or_assign(target,
                                             IncFsMount::Bind{storage, std::move(metadataName),
                                                              std::move(source), kind});
     mBindsByPath[std::move(target)] = it;
-    return 0;
+}
+
+RawMetadata IncrementalService::getMetadata(StorageId storage, std::string_view path) const {
+    const auto ifs = getIfs(storage);
+    if (!ifs) {
+        return {};
+    }
+    const auto normPath = normalizePathToStorage(ifs, storage, path);
+    if (normPath.empty()) {
+        return {};
+    }
+    return mIncFs->getMetadata(ifs->control, normPath);
 }
 
 RawMetadata IncrementalService::getMetadata(StorageId storage, FileId node) const {
@@ -945,47 +975,6 @@
     return mIncFs->getMetadata(ifs->control, node);
 }
 
-std::vector<std::string> IncrementalService::listFiles(StorageId storage) const {
-    const auto ifs = getIfs(storage);
-    if (!ifs) {
-        return {};
-    }
-
-    std::unique_lock l(ifs->lock);
-    auto subdirIt = ifs->storages.find(storage);
-    if (subdirIt == ifs->storages.end()) {
-        return {};
-    }
-    auto dir = path::join(ifs->root, constants().mount, subdirIt->second.name);
-    l.unlock();
-
-    const auto prefixSize = dir.size() + 1;
-    std::vector<std::string> todoDirs{std::move(dir)};
-    std::vector<std::string> result;
-    do {
-        auto currDir = std::move(todoDirs.back());
-        todoDirs.pop_back();
-
-        auto d =
-                std::unique_ptr<DIR, decltype(&::closedir)>(::opendir(currDir.c_str()), ::closedir);
-        while (auto e = ::readdir(d.get())) {
-            if (e->d_type == DT_REG) {
-                result.emplace_back(
-                        path::join(std::string_view(currDir).substr(prefixSize), e->d_name));
-                continue;
-            }
-            if (e->d_type == DT_DIR) {
-                if (e->d_name == "."sv || e->d_name == ".."sv) {
-                    continue;
-                }
-                todoDirs.emplace_back(path::join(currDir, e->d_name));
-                continue;
-            }
-        }
-    } while (!todoDirs.empty());
-    return result;
-}
-
 bool IncrementalService::startLoading(StorageId storage) const {
     DataLoaderStubPtr dataLoaderStub;
     {
@@ -1003,16 +992,216 @@
     return true;
 }
 
-void IncrementalService::mountExistingImages() {
-    for (const auto& entry : fs::directory_iterator(mIncrementalDir)) {
-        const auto path = entry.path().u8string();
-        const auto name = entry.path().filename().u8string();
-        if (!base::StartsWith(name, constants().mountKeyPrefix)) {
+std::unordered_set<std::string_view> IncrementalService::adoptMountedInstances() {
+    std::unordered_set<std::string_view> mountedRootNames;
+    mIncFs->listExistingMounts([this, &mountedRootNames](auto root, auto backingDir, auto binds) {
+        LOG(INFO) << "Existing mount: " << backingDir << "->" << root;
+        for (auto [source, target] : binds) {
+            LOG(INFO) << "  bind: '" << source << "'->'" << target << "'";
+            LOG(INFO) << "         " << path::join(root, source);
+        }
+
+        // Ensure it's a kind of a mount that's managed by IncrementalService
+        if (path::basename(root) != constants().mount ||
+            path::basename(backingDir) != constants().backing) {
+            return;
+        }
+        const auto expectedRoot = path::dirname(root);
+        if (path::dirname(backingDir) != expectedRoot) {
+            return;
+        }
+        if (path::dirname(expectedRoot) != mIncrementalDir) {
+            return;
+        }
+        if (!path::basename(expectedRoot).starts_with(constants().mountKeyPrefix)) {
+            return;
+        }
+
+        LOG(INFO) << "Looks like an IncrementalService-owned: " << expectedRoot;
+
+        // make sure we clean up the mount if it happens to be a bad one.
+        // Note: unmounting needs to run first, so the cleanup object is created _last_.
+        auto cleanupFiles = makeCleanup([&]() {
+            LOG(INFO) << "Failed to adopt existing mount, deleting files: " << expectedRoot;
+            IncFsMount::cleanupFilesystem(expectedRoot);
+        });
+        auto cleanupMounts = makeCleanup([&]() {
+            LOG(INFO) << "Failed to adopt existing mount, cleaning up: " << expectedRoot;
+            for (auto&& [_, target] : binds) {
+                mVold->unmountIncFs(std::string(target));
+            }
+            mVold->unmountIncFs(std::string(root));
+        });
+
+        auto control = mIncFs->openMount(root);
+        if (!control) {
+            LOG(INFO) << "failed to open mount " << root;
+            return;
+        }
+
+        auto mountRecord =
+                parseFromIncfs<metadata::Mount>(mIncFs.get(), control,
+                                                path::join(root, constants().infoMdName));
+        if (!mountRecord.has_loader() || !mountRecord.has_storage()) {
+            LOG(ERROR) << "Bad mount metadata in mount at " << expectedRoot;
+            return;
+        }
+
+        auto mountId = mountRecord.storage().id();
+        mNextId = std::max(mNextId, mountId + 1);
+
+        DataLoaderParamsParcel dataLoaderParams;
+        {
+            const auto& loader = mountRecord.loader();
+            dataLoaderParams.type = (content::pm::DataLoaderType)loader.type();
+            dataLoaderParams.packageName = loader.package_name();
+            dataLoaderParams.className = loader.class_name();
+            dataLoaderParams.arguments = loader.arguments();
+        }
+
+        auto ifs = std::make_shared<IncFsMount>(std::string(expectedRoot), mountId,
+                                                std::move(control), *this);
+        cleanupFiles.release(); // ifs will take care of that now
+
+        std::vector<std::pair<std::string, metadata::BindPoint>> permanentBindPoints;
+        auto d = openDir(root);
+        while (auto e = ::readdir(d.get())) {
+            if (e->d_type == DT_REG) {
+                auto name = std::string_view(e->d_name);
+                if (name.starts_with(constants().mountpointMdPrefix)) {
+                    permanentBindPoints
+                            .emplace_back(name,
+                                          parseFromIncfs<metadata::BindPoint>(mIncFs.get(),
+                                                                              ifs->control,
+                                                                              path::join(root,
+                                                                                         name)));
+                    if (permanentBindPoints.back().second.dest_path().empty() ||
+                        permanentBindPoints.back().second.source_subdir().empty()) {
+                        permanentBindPoints.pop_back();
+                        mIncFs->unlink(ifs->control, path::join(root, name));
+                    } else {
+                        LOG(INFO) << "Permanent bind record: '"
+                                  << permanentBindPoints.back().second.source_subdir() << "'->'"
+                                  << permanentBindPoints.back().second.dest_path() << "'";
+                    }
+                }
+            } else if (e->d_type == DT_DIR) {
+                if (e->d_name == "."sv || e->d_name == ".."sv) {
+                    continue;
+                }
+                auto name = std::string_view(e->d_name);
+                if (name.starts_with(constants().storagePrefix)) {
+                    int storageId;
+                    const auto res =
+                            std::from_chars(name.data() + constants().storagePrefix.size() + 1,
+                                            name.data() + name.size(), storageId);
+                    if (res.ec != std::errc{} || *res.ptr != '_') {
+                        LOG(WARNING) << "Ignoring storage with invalid name '" << name
+                                     << "' for mount " << expectedRoot;
+                        continue;
+                    }
+                    auto [_, inserted] = mMounts.try_emplace(storageId, ifs);
+                    if (!inserted) {
+                        LOG(WARNING) << "Ignoring storage with duplicate id " << storageId
+                                     << " for mount " << expectedRoot;
+                        continue;
+                    }
+                    ifs->storages.insert_or_assign(storageId,
+                                                   IncFsMount::Storage{path::join(root, name)});
+                    mNextId = std::max(mNextId, storageId + 1);
+                }
+            }
+        }
+
+        if (ifs->storages.empty()) {
+            LOG(WARNING) << "No valid storages in mount " << root;
+            return;
+        }
+
+        // now match the mounted directories with what we expect to have in the metadata
+        {
+            std::unique_lock l(mLock, std::defer_lock);
+            for (auto&& [metadataFile, bindRecord] : permanentBindPoints) {
+                auto mountedIt = std::find_if(binds.begin(), binds.end(),
+                                              [&, bindRecord = bindRecord](auto&& bind) {
+                                                  return bind.second == bindRecord.dest_path() &&
+                                                          path::join(root, bind.first) ==
+                                                          bindRecord.source_subdir();
+                                              });
+                if (mountedIt != binds.end()) {
+                    LOG(INFO) << "Matched permanent bound " << bindRecord.source_subdir()
+                              << " to mount " << mountedIt->first;
+                    addBindMountRecordLocked(*ifs, bindRecord.storage_id(), std::move(metadataFile),
+                                             std::move(*bindRecord.mutable_source_subdir()),
+                                             std::move(*bindRecord.mutable_dest_path()),
+                                             BindKind::Permanent);
+                    if (mountedIt != binds.end() - 1) {
+                        std::iter_swap(mountedIt, binds.end() - 1);
+                    }
+                    binds = binds.first(binds.size() - 1);
+                } else {
+                    LOG(INFO) << "Didn't match permanent bound " << bindRecord.source_subdir()
+                              << ", mounting";
+                    // doesn't exist - try mounting back
+                    if (addBindMountWithMd(*ifs, bindRecord.storage_id(), std::move(metadataFile),
+                                           std::move(*bindRecord.mutable_source_subdir()),
+                                           std::move(*bindRecord.mutable_dest_path()),
+                                           BindKind::Permanent, l)) {
+                        mIncFs->unlink(ifs->control, metadataFile);
+                    }
+                }
+            }
+        }
+
+        // if anything stays in |binds| those are probably temporary binds; system restarted since
+        // they were mounted - so let's unmount them all.
+        for (auto&& [source, target] : binds) {
+            if (source.empty()) {
+                continue;
+            }
+            mVold->unmountIncFs(std::string(target));
+        }
+        cleanupMounts.release(); // ifs now manages everything
+
+        if (ifs->bindPoints.empty()) {
+            LOG(WARNING) << "No valid bind points for mount " << expectedRoot;
+            deleteStorage(*ifs);
+            return;
+        }
+
+        prepareDataLoaderLocked(*ifs, std::move(dataLoaderParams));
+        CHECK(ifs->dataLoaderStub);
+
+        mountedRootNames.insert(path::basename(ifs->root));
+
+        // not locking here at all: we're still in the constructor, no other calls can happen
+        mMounts[ifs->mountId] = std::move(ifs);
+    });
+
+    return mountedRootNames;
+}
+
+void IncrementalService::mountExistingImages(
+        const std::unordered_set<std::string_view>& mountedRootNames) {
+    auto dir = openDir(mIncrementalDir);
+    if (!dir) {
+        PLOG(WARNING) << "Couldn't open the root incremental dir " << mIncrementalDir;
+        return;
+    }
+    while (auto entry = ::readdir(dir.get())) {
+        if (entry->d_type != DT_DIR) {
+            continue;
+        }
+        std::string_view name = entry->d_name;
+        if (!name.starts_with(constants().mountKeyPrefix)) {
+            continue;
+        }
+        if (mountedRootNames.find(name) != mountedRootNames.end()) {
             continue;
         }
         const auto root = path::join(mIncrementalDir, name);
         if (!mountExistingImage(root)) {
-            IncFsMount::cleanupFilesystem(path);
+            IncFsMount::cleanupFilesystem(root);
         }
     }
 }
@@ -1049,7 +1238,7 @@
     DataLoaderParamsParcel dataLoaderParams;
     {
         const auto& loader = mount.loader();
-        dataLoaderParams.type = (android::content::pm::DataLoaderType)loader.type();
+        dataLoaderParams.type = (content::pm::DataLoaderType)loader.type();
         dataLoaderParams.packageName = loader.package_name();
         dataLoaderParams.className = loader.class_name();
         dataLoaderParams.arguments = loader.arguments();
@@ -1059,7 +1248,7 @@
     CHECK(ifs->dataLoaderStub);
 
     std::vector<std::pair<std::string, metadata::BindPoint>> bindPoints;
-    auto d = openDir(path::c_str(mountTarget));
+    auto d = openDir(mountTarget);
     while (auto e = ::readdir(d.get())) {
         if (e->d_type == DT_REG) {
             auto name = std::string_view(e->d_name);
@@ -1109,12 +1298,14 @@
     }
 
     int bindCount = 0;
-    for (auto&& bp : bindPoints) {
+    {
         std::unique_lock l(mLock, std::defer_lock);
-        bindCount += !addBindMountWithMd(*ifs, bp.second.storage_id(), std::move(bp.first),
-                                         std::move(*bp.second.mutable_source_subdir()),
-                                         std::move(*bp.second.mutable_dest_path()),
-                                         BindKind::Permanent, l);
+        for (auto&& bp : bindPoints) {
+            bindCount += !addBindMountWithMd(*ifs, bp.second.storage_id(), std::move(bp.first),
+                                             std::move(*bp.second.mutable_source_subdir()),
+                                             std::move(*bp.second.mutable_dest_path()),
+                                             BindKind::Permanent, l);
+        }
     }
 
     if (bindCount == 0) {
@@ -1123,30 +1314,35 @@
         return false;
     }
 
+    // not locking here at all: we're still in the constructor, no other calls can happen
     mMounts[ifs->mountId] = std::move(ifs);
     return true;
 }
 
 IncrementalService::DataLoaderStubPtr IncrementalService::prepareDataLoader(
-        IncrementalService::IncFsMount& ifs, DataLoaderParamsParcel&& params,
+        IncFsMount& ifs, DataLoaderParamsParcel&& params,
         const DataLoaderStatusListener* externalListener) {
     std::unique_lock l(ifs.lock);
+    prepareDataLoaderLocked(ifs, std::move(params), externalListener);
+    return ifs.dataLoaderStub;
+}
+
+void IncrementalService::prepareDataLoaderLocked(IncFsMount& ifs, DataLoaderParamsParcel&& params,
+                                                 const DataLoaderStatusListener* externalListener) {
     if (ifs.dataLoaderStub) {
         LOG(INFO) << "Skipped data loader preparation because it already exists";
-        return ifs.dataLoaderStub;
+        return;
     }
 
     FileSystemControlParcel fsControlParcel;
     fsControlParcel.incremental = aidl::make_nullable<IncrementalFileSystemControlParcel>();
-    fsControlParcel.incremental->cmd.reset(base::unique_fd(::dup(ifs.control.cmd())));
-    fsControlParcel.incremental->pendingReads.reset(
-            base::unique_fd(::dup(ifs.control.pendingReads())));
-    fsControlParcel.incremental->log.reset(base::unique_fd(::dup(ifs.control.logs())));
+    fsControlParcel.incremental->cmd.reset(dup(ifs.control.cmd()));
+    fsControlParcel.incremental->pendingReads.reset(dup(ifs.control.pendingReads()));
+    fsControlParcel.incremental->log.reset(dup(ifs.control.logs()));
     fsControlParcel.service = new IncrementalServiceConnector(*this, ifs.mountId);
 
     ifs.dataLoaderStub = new DataLoaderStub(*this, ifs.mountId, std::move(params),
                                             std::move(fsControlParcel), externalListener);
-    return ifs.dataLoaderStub;
 }
 
 template <class Duration>
@@ -1208,7 +1404,7 @@
         const auto targetLibPathAbsolute = normalizePathToStorage(ifs, storage, targetLibPath);
         // If the extract file already exists, skip
         if (access(targetLibPathAbsolute.c_str(), F_OK) == 0) {
-            if (sEnablePerfLogging) {
+            if (perfLoggingEnabled()) {
                 LOG(INFO) << "incfs: Native lib file already exists: " << targetLibPath
                           << "; skipping extraction, spent "
                           << elapsedMcs(startFileTs, Clock::now()) << "mcs";
@@ -1235,7 +1431,7 @@
 
         // If it is a zero-byte file, skip data writing
         if (entry.uncompressed_length == 0) {
-            if (sEnablePerfLogging) {
+            if (perfLoggingEnabled()) {
                 LOG(INFO) << "incfs: Extracted " << libName
                           << "(0 bytes): " << elapsedMcs(startFileTs, makeFileTs) << "mcs";
             }
@@ -1248,7 +1444,7 @@
             extractZipFile(ifs.lock(), zipFile.get(), entry, libFileId, libPath, makeFileTs);
         });
 
-        if (sEnablePerfLogging) {
+        if (perfLoggingEnabled()) {
             auto prepareJobTs = Clock::now();
             LOG(INFO) << "incfs: Processed " << libName << ": "
                       << elapsedMcs(startFileTs, prepareJobTs)
@@ -1275,7 +1471,7 @@
         mJobCondition.notify_all();
     }
 
-    if (sEnablePerfLogging) {
+    if (perfLoggingEnabled()) {
         auto end = Clock::now();
         LOG(INFO) << "incfs: configureNativeBinaries complete in " << elapsedMcs(start, end)
                   << "mcs, make dirs: " << elapsedMcs(start, mkDirsTs)
@@ -1340,7 +1536,7 @@
         return;
     }
 
-    if (sEnablePerfLogging) {
+    if (perfLoggingEnabled()) {
         auto endFileTs = Clock::now();
         LOG(INFO) << "incfs: Extracted " << libName << "(" << entry.compressed_length << " -> "
                   << entry.uncompressed_length << " bytes): " << elapsedMcs(startedTs, endFileTs)
@@ -1356,7 +1552,7 @@
     struct WaitPrinter {
         const Clock::time_point startTs = Clock::now();
         ~WaitPrinter() noexcept {
-            if (sEnablePerfLogging) {
+            if (perfLoggingEnabled()) {
                 const auto endTs = Clock::now();
                 LOG(INFO) << "incfs: waitForNativeBinariesExtraction() complete in "
                           << elapsedMcs(startTs, endTs) << "mcs";
@@ -1381,6 +1577,11 @@
     return mRunning;
 }
 
+bool IncrementalService::perfLoggingEnabled() {
+    static const bool enabled = base::GetBoolProperty("incremental.perflogging", false);
+    return enabled;
+}
+
 void IncrementalService::runJobProcessing() {
     for (;;) {
         std::unique_lock lock(mJobMutex);
@@ -1492,12 +1693,17 @@
     return setTargetStatus(IDataLoaderStatusListener::DATA_LOADER_DESTROYED);
 }
 
-bool IncrementalService::DataLoaderStub::setTargetStatus(int status) {
+bool IncrementalService::DataLoaderStub::setTargetStatus(int newStatus) {
+    int oldStatus, curStatus;
     {
         std::unique_lock lock(mStatusMutex);
-        mTargetStatus = status;
+        oldStatus = mTargetStatus;
+        mTargetStatus = newStatus;
         mTargetStatusTs = Clock::now();
+        curStatus = mCurrentStatus;
     }
+    LOG(DEBUG) << "Target status update for DataLoader " << mId << ": " << oldStatus << " -> "
+               << newStatus << " (current " << curStatus << ")";
     return fsmStep();
 }
 
@@ -1596,35 +1802,45 @@
         return binder::Status::fromServiceSpecificError(-EPERM, "Mount ID mismatch.");
     }
 
+    int targetStatus, oldStatus;
     {
         std::unique_lock lock(mStatusMutex);
         if (mCurrentStatus == newStatus) {
             return binder::Status::ok();
         }
         mCurrentStatus = newStatus;
+        oldStatus = mCurrentStatus;
+        targetStatus = mTargetStatus;
     }
 
+    LOG(DEBUG) << "Current status update for DataLoader " << mId << ": " << oldStatus << " -> "
+               << newStatus << " (target " << targetStatus << ")";
+
     if (mListener) {
         mListener->onStatusChanged(mountId, newStatus);
     }
 
     fsmStep();
 
+    mStatusCondition.notify_all();
+
     return binder::Status::ok();
 }
 
 void IncrementalService::DataLoaderStub::onDump(int fd) {
-    dprintf(fd, "\t\tdataLoader:");
-    dprintf(fd, "\t\t\tcurrentStatus: %d\n", mCurrentStatus);
-    dprintf(fd, "\t\t\ttargetStatus: %d\n", mTargetStatus);
-    dprintf(fd, "\t\t\ttargetStatusTs: %lldmcs\n",
+    dprintf(fd, "    dataLoader: {\n");
+    dprintf(fd, "      currentStatus: %d\n", mCurrentStatus);
+    dprintf(fd, "      targetStatus: %d\n", mTargetStatus);
+    dprintf(fd, "      targetStatusTs: %lldmcs\n",
             (long long)(elapsedMcs(mTargetStatusTs, Clock::now())));
     const auto& params = mParams;
-    dprintf(fd, "\t\t\tdataLoaderParams:\n");
-    dprintf(fd, "\t\t\t\ttype: %s\n", toString(params.type).c_str());
-    dprintf(fd, "\t\t\t\tpackageName: %s\n", params.packageName.c_str());
-    dprintf(fd, "\t\t\t\tclassName: %s\n", params.className.c_str());
-    dprintf(fd, "\t\t\t\targuments: %s\n", params.arguments.c_str());
+    dprintf(fd, "      dataLoaderParams: {\n");
+    dprintf(fd, "        type: %s\n", toString(params.type).c_str());
+    dprintf(fd, "        packageName: %s\n", params.packageName.c_str());
+    dprintf(fd, "        className: %s\n", params.className.c_str());
+    dprintf(fd, "        arguments: %s\n", params.arguments.c_str());
+    dprintf(fd, "      }\n");
+    dprintf(fd, "    }\n");
 }
 
 void IncrementalService::AppOpsListener::opChanged(int32_t, const String16&) {
diff --git a/services/incremental/IncrementalService.h b/services/incremental/IncrementalService.h
index bd01d77..9d40baf 100644
--- a/services/incremental/IncrementalService.h
+++ b/services/incremental/IncrementalService.h
@@ -16,13 +16,13 @@
 
 #pragma once
 
-#include <android-base/strings.h>
-#include <android-base/unique_fd.h>
+#include <android/content/pm/BnDataLoaderStatusListener.h>
 #include <android/content/pm/DataLoaderParamsParcel.h>
-#include <binder/IServiceManager.h>
+#include <android/content/pm/IDataLoaderStatusListener.h>
+#include <android/os/incremental/BnIncrementalServiceConnector.h>
+#include <binder/IAppOpsCallback.h>
 #include <utils/String16.h>
 #include <utils/StrongPointer.h>
-#include <utils/Vector.h>
 #include <ziparchive/zip_archive.h>
 
 #include <atomic>
@@ -37,21 +37,14 @@
 #include <string_view>
 #include <thread>
 #include <unordered_map>
+#include <unordered_set>
 #include <utility>
 #include <vector>
 
 #include "ServiceWrappers.h"
-#include "android/content/pm/BnDataLoaderStatusListener.h"
-#include "android/os/incremental/BnIncrementalServiceConnector.h"
 #include "incfs.h"
 #include "path.h"
 
-using namespace android::os::incremental;
-
-namespace android::os {
-class IVold;
-}
-
 namespace android::incremental {
 
 using MountId = int;
@@ -101,17 +94,14 @@
 
     void onSystemReady();
 
-    StorageId createStorage(std::string_view mountPoint, DataLoaderParamsParcel&& dataLoaderParams,
+    StorageId createStorage(std::string_view mountPoint,
+                            content::pm::DataLoaderParamsParcel&& dataLoaderParams,
                             const DataLoaderStatusListener& dataLoaderStatusListener,
                             CreateOptions options = CreateOptions::Default);
     StorageId createLinkedStorage(std::string_view mountPoint, StorageId linkedStorage,
                                   CreateOptions options = CreateOptions::Default);
     StorageId openStorage(std::string_view path);
 
-    FileId nodeFor(StorageId storage, std::string_view path) const;
-    std::pair<FileId, std::string_view> parentAndNameFor(StorageId storage,
-                                                         std::string_view path) const;
-
     int bind(StorageId storage, std::string_view source, std::string_view target, BindKind kind);
     int unbind(StorageId storage, std::string_view target);
     void deleteStorage(StorageId storage);
@@ -131,9 +121,9 @@
         return false;
     }
 
+    RawMetadata getMetadata(StorageId storage, std::string_view path) const;
     RawMetadata getMetadata(StorageId storage, FileId node) const;
 
-    std::vector<std::string> listFiles(StorageId storage) const;
     bool startLoading(StorageId storage) const;
 
     bool configureNativeBinaries(StorageId storage, std::string_view apkFullPath,
@@ -151,7 +141,7 @@
         const std::string packageName;
     };
 
-    class IncrementalServiceConnector : public BnIncrementalServiceConnector {
+    class IncrementalServiceConnector : public os::incremental::BnIncrementalServiceConnector {
     public:
         IncrementalServiceConnector(IncrementalService& incrementalService, int32_t storage)
               : incrementalService(incrementalService), storage(storage) {}
@@ -163,14 +153,13 @@
     };
 
 private:
-    static const bool sEnablePerfLogging;
-
     struct IncFsMount;
 
-    class DataLoaderStub : public android::content::pm::BnDataLoaderStatusListener {
+    class DataLoaderStub : public content::pm::BnDataLoaderStatusListener {
     public:
-        DataLoaderStub(IncrementalService& service, MountId id, DataLoaderParamsParcel&& params,
-                       FileSystemControlParcel&& control,
+        DataLoaderStub(IncrementalService& service, MountId id,
+                       content::pm::DataLoaderParamsParcel&& params,
+                       content::pm::FileSystemControlParcel&& control,
                        const DataLoaderStatusListener* externalListener);
         ~DataLoaderStub();
         // Cleans up the internal state and invalidates DataLoaderStub. Any subsequent calls will
@@ -184,7 +173,7 @@
         void onDump(int fd);
 
         MountId id() const { return mId; }
-        const DataLoaderParamsParcel& params() const { return mParams; }
+        const content::pm::DataLoaderParamsParcel& params() const { return mParams; }
 
     private:
         binder::Status onStatusChanged(MountId mount, int newStatus) final;
@@ -202,14 +191,14 @@
 
         IncrementalService& mService;
         MountId mId = kInvalidStorageId;
-        DataLoaderParamsParcel mParams;
-        FileSystemControlParcel mControl;
+        content::pm::DataLoaderParamsParcel mParams;
+        content::pm::FileSystemControlParcel mControl;
         DataLoaderStatusListener mListener;
 
         std::mutex mStatusMutex;
         std::condition_variable mStatusCondition;
-        int mCurrentStatus = IDataLoaderStatusListener::DATA_LOADER_DESTROYED;
-        int mTargetStatus = IDataLoaderStatusListener::DATA_LOADER_DESTROYED;
+        int mCurrentStatus = content::pm::IDataLoaderStatusListener::DATA_LOADER_DESTROYED;
+        int mTargetStatus = content::pm::IDataLoaderStatusListener::DATA_LOADER_DESTROYED;
         TimePoint mTargetStatusTs = {};
     };
     using DataLoaderStubPtr = sp<DataLoaderStub>;
@@ -228,7 +217,7 @@
 
         using Control = incfs::UniqueControl;
 
-        using BindMap = std::map<std::string, Bind>;
+        using BindMap = std::map<std::string, Bind, path::PathLess>;
         using StorageMap = std::unordered_map<StorageId, Storage>;
 
         mutable std::mutex lock;
@@ -260,7 +249,10 @@
     using MountMap = std::unordered_map<MountId, IfsMountPtr>;
     using BindPathMap = std::map<std::string, IncFsMount::BindMap::iterator, path::PathLess>;
 
-    void mountExistingImages();
+    static bool perfLoggingEnabled();
+
+    std::unordered_set<std::string_view> adoptMountedInstances();
+    void mountExistingImages(const std::unordered_set<std::string_view>& mountedRootNames);
     bool mountExistingImage(std::string_view root);
 
     IfsMountPtr getIfs(StorageId storage) const;
@@ -273,8 +265,14 @@
                            std::string&& source, std::string&& target, BindKind kind,
                            std::unique_lock<std::mutex>& mainLock);
 
-    DataLoaderStubPtr prepareDataLoader(IncFsMount& ifs, DataLoaderParamsParcel&& params,
+    void addBindMountRecordLocked(IncFsMount& ifs, StorageId storage, std::string&& metadataName,
+                                  std::string&& source, std::string&& target, BindKind kind);
+
+    DataLoaderStubPtr prepareDataLoader(IncFsMount& ifs,
+                                        content::pm::DataLoaderParamsParcel&& params,
                                         const DataLoaderStatusListener* externalListener = nullptr);
+    void prepareDataLoaderLocked(IncFsMount& ifs, content::pm::DataLoaderParamsParcel&& params,
+                                 const DataLoaderStatusListener* externalListener = nullptr);
 
     BindPathMap::const_iterator findStorageLocked(std::string_view path) const;
     StorageId findStorageId(std::string_view path) const;
@@ -283,9 +281,10 @@
     void deleteStorageLocked(IncFsMount& ifs, std::unique_lock<std::mutex>&& ifsLock);
     MountMap::iterator getStorageSlotLocked();
     std::string normalizePathToStorage(const IfsMountPtr& incfs, StorageId storage,
-                                       std::string_view path);
-    std::string normalizePathToStorageLocked(IncFsMount::StorageMap::iterator storageIt,
-                                             std::string_view path);
+                                       std::string_view path) const;
+    std::string normalizePathToStorageLocked(const IfsMountPtr& incfs,
+                                             IncFsMount::StorageMap::iterator storageIt,
+                                             std::string_view path) const;
 
     binder::Status applyStorageParams(IncFsMount& ifs, bool enableReadLogs);
 
diff --git a/services/incremental/IncrementalServiceValidation.cpp b/services/incremental/IncrementalServiceValidation.cpp
new file mode 100644
index 0000000..abadbbf
--- /dev/null
+++ b/services/incremental/IncrementalServiceValidation.cpp
@@ -0,0 +1,77 @@
+/*
+ * 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.
+ */
+
+#include "IncrementalServiceValidation.h"
+
+#include <android-base/stringprintf.h>
+#include <binder/IPCThreadState.h>
+#include <binder/PermissionCache.h>
+#include <binder/PermissionController.h>
+#include <errno.h>
+#include <utils/String16.h>
+
+namespace android::incremental {
+
+binder::Status Ok() {
+    return binder::Status::ok();
+}
+
+binder::Status Exception(uint32_t code, const std::string& msg) {
+    return binder::Status::fromExceptionCode(code, String8(msg.c_str()));
+}
+
+int fromBinderStatus(const binder::Status& status) {
+    return status.exceptionCode() == binder::Status::EX_SERVICE_SPECIFIC
+            ? status.serviceSpecificErrorCode() > 0
+                    ? -status.serviceSpecificErrorCode()
+                    : status.serviceSpecificErrorCode() == 0 ? -EFAULT
+                                                             : status.serviceSpecificErrorCode()
+            : -EIO;
+}
+
+binder::Status CheckPermissionForDataDelivery(const char* permission, const char* operation,
+                                              const char* package) {
+    using android::base::StringPrintf;
+
+    int32_t pid;
+    int32_t uid;
+
+    if (!PermissionCache::checkCallingPermission(String16(permission), &pid, &uid)) {
+        return Exception(binder::Status::EX_SECURITY,
+                         StringPrintf("UID %d / PID %d lacks permission %s", uid, pid, permission));
+    }
+
+    String16 packageName{package};
+
+    // Caller must also have op granted.
+    PermissionController pc;
+    if (auto packageUid = pc.getPackageUid(packageName, 0); packageUid != uid) {
+        return Exception(binder::Status::EX_SECURITY,
+                         StringPrintf("UID %d / PID %d does not own package %s", uid, pid,
+                                      package));
+    }
+    switch (auto result = pc.noteOp(String16(operation), uid, packageName); result) {
+        case PermissionController::MODE_ALLOWED:
+        case PermissionController::MODE_DEFAULT:
+            return binder::Status::ok();
+        default:
+            return Exception(binder::Status::EX_SECURITY,
+                             StringPrintf("UID %d / PID %d / package %s lacks app-op %s, error %d",
+                                          uid, pid, package, operation, result));
+    }
+}
+
+} // namespace android::incremental
diff --git a/services/incremental/IncrementalServiceValidation.h b/services/incremental/IncrementalServiceValidation.h
index 48894c6..0e50c4d 100644
--- a/services/incremental/IncrementalServiceValidation.h
+++ b/services/incremental/IncrementalServiceValidation.h
@@ -16,61 +16,17 @@
 
 #pragma once
 
-#include <android-base/stringprintf.h>
-#include <binder/IPCThreadState.h>
-#include <binder/PermissionCache.h>
-#include <binder/PermissionController.h>
 #include <binder/Status.h>
+#include <stdint.h>
+
+#include <string>
 
 namespace android::incremental {
 
-inline binder::Status Ok() {
-    return binder::Status::ok();
-}
-
-inline binder::Status Exception(uint32_t code, const std::string& msg) {
-    return binder::Status::fromExceptionCode(code, String8(msg.c_str()));
-}
-
-inline int fromBinderStatus(const binder::Status& status) {
-    return status.exceptionCode() == binder::Status::EX_SERVICE_SPECIFIC
-            ? status.serviceSpecificErrorCode() > 0 ? -status.serviceSpecificErrorCode()
-                                                    : status.serviceSpecificErrorCode() == 0
-                            ? -EFAULT
-                            : status.serviceSpecificErrorCode()
-            : -EIO;
-}
-
-inline binder::Status CheckPermissionForDataDelivery(const char* permission, const char* operation,
-                                                     const char* package) {
-    using android::base::StringPrintf;
-
-    int32_t pid;
-    int32_t uid;
-
-    if (!PermissionCache::checkCallingPermission(String16(permission), &pid, &uid)) {
-        return Exception(binder::Status::EX_SECURITY,
-                         StringPrintf("UID %d / PID %d lacks permission %s", uid, pid, permission));
-    }
-
-    String16 packageName{package};
-
-    // Caller must also have op granted.
-    PermissionController pc;
-    if (auto packageUid = pc.getPackageUid(packageName, 0); packageUid != uid) {
-        return Exception(binder::Status::EX_SECURITY,
-                         StringPrintf("UID %d / PID %d does not own package %s", uid, pid,
-                                      package));
-    }
-    switch (auto result = pc.noteOp(String16(operation), uid, packageName); result) {
-        case PermissionController::MODE_ALLOWED:
-        case PermissionController::MODE_DEFAULT:
-            return binder::Status::ok();
-        default:
-            return Exception(binder::Status::EX_SECURITY,
-                             StringPrintf("UID %d / PID %d / package %s lacks app-op %s, error %d",
-                                          uid, pid, package, operation, result));
-    }
-}
+binder::Status Ok();
+binder::Status Exception(uint32_t code, const std::string& msg);
+int fromBinderStatus(const binder::Status& status);
+binder::Status CheckPermissionForDataDelivery(const char* permission, const char* operation,
+                                              const char* package);
 
 } // namespace android::incremental
diff --git a/services/incremental/ServiceWrappers.cpp b/services/incremental/ServiceWrappers.cpp
index bf8e696..0b044c2 100644
--- a/services/incremental/ServiceWrappers.cpp
+++ b/services/incremental/ServiceWrappers.cpp
@@ -18,12 +18,18 @@
 
 #include "ServiceWrappers.h"
 
+#include <MountRegistry.h>
 #include <android-base/logging.h>
+#include <android/content/pm/IDataLoaderManager.h>
+#include <android/os/IVold.h>
+#include <binder/AppOpsManager.h>
 #include <utils/String16.h>
 
+#include "IncrementalServiceValidation.h"
+
 using namespace std::literals;
 
-namespace android::os::incremental {
+namespace android::incremental {
 
 static constexpr auto kVoldServiceName = "vold"sv;
 static constexpr auto kDataLoaderManagerName = "dataloader_manager"sv;
@@ -32,9 +38,9 @@
 public:
     RealVoldService(const sp<os::IVold> vold) : mInterface(std::move(vold)) {}
     ~RealVoldService() = default;
-    binder::Status mountIncFs(const std::string& backingPath, const std::string& targetDir,
-                              int32_t flags,
-                              IncrementalFileSystemControlParcel* _aidl_return) const final {
+    binder::Status mountIncFs(
+            const std::string& backingPath, const std::string& targetDir, int32_t flags,
+            os::incremental::IncrementalFileSystemControlParcel* _aidl_return) const final {
         return mInterface->mountIncFs(backingPath, targetDir, flags, _aidl_return);
     }
     binder::Status unmountIncFs(const std::string& dir) const final {
@@ -56,16 +62,18 @@
 
 class RealDataLoaderManager : public DataLoaderManagerWrapper {
 public:
-    RealDataLoaderManager(const sp<content::pm::IDataLoaderManager> manager)
-          : mInterface(manager) {}
+    RealDataLoaderManager(sp<content::pm::IDataLoaderManager> manager)
+          : mInterface(std::move(manager)) {}
     ~RealDataLoaderManager() = default;
-    binder::Status initializeDataLoader(MountId mountId, const DataLoaderParamsParcel& params,
-                                        const FileSystemControlParcel& control,
-                                        const sp<IDataLoaderStatusListener>& listener,
+    binder::Status initializeDataLoader(MountId mountId,
+                                        const content::pm::DataLoaderParamsParcel& params,
+                                        const content::pm::FileSystemControlParcel& control,
+                                        const sp<content::pm::IDataLoaderStatusListener>& listener,
                                         bool* _aidl_return) const final {
         return mInterface->initializeDataLoader(mountId, params, control, listener, _aidl_return);
     }
-    binder::Status getDataLoader(MountId mountId, sp<IDataLoader>* _aidl_return) const final {
+    binder::Status getDataLoader(MountId mountId,
+                                 sp<content::pm::IDataLoader>* _aidl_return) const final {
         return mInterface->getDataLoader(mountId, _aidl_return);
     }
     binder::Status destroyDataLoader(MountId mountId) const final {
@@ -109,21 +117,28 @@
 class RealIncFs : public IncFsWrapper {
 public:
     RealIncFs() = default;
-    ~RealIncFs() = default;
+    ~RealIncFs() final = default;
+    void listExistingMounts(const ExistingMountCallback& cb) const final {
+        for (auto mount : incfs::defaultMountRegistry().copyMounts()) {
+            auto binds = mount.binds(); // span() doesn't like rvalue containers, needs to save it.
+            cb(mount.root(), mount.backingDir(), binds);
+        }
+    }
+    Control openMount(std::string_view path) const final { return incfs::open(path); }
     Control createControl(IncFsFd cmd, IncFsFd pendingReads, IncFsFd logs) const final {
         return incfs::createControl(cmd, pendingReads, logs);
     }
     ErrorCode makeFile(const Control& control, std::string_view path, int mode, FileId id,
-                       NewFileParams params) const final {
+                       incfs::NewFileParams params) const final {
         return incfs::makeFile(control, path, mode, id, params);
     }
     ErrorCode makeDir(const Control& control, std::string_view path, int mode) const final {
         return incfs::makeDir(control, path, mode);
     }
-    RawMetadata getMetadata(const Control& control, FileId fileid) const final {
+    incfs::RawMetadata getMetadata(const Control& control, FileId fileid) const final {
         return incfs::getMetadata(control, fileid);
     }
-    RawMetadata getMetadata(const Control& control, std::string_view path) const final {
+    incfs::RawMetadata getMetadata(const Control& control, std::string_view path) const final {
         return incfs::getMetadata(control, path);
     }
     FileId getFileId(const Control& control, std::string_view path) const final {
@@ -138,8 +153,8 @@
     base::unique_fd openForSpecialOps(const Control& control, FileId id) const final {
         return base::unique_fd{incfs::openForSpecialOps(control, id).release()};
     }
-    ErrorCode writeBlocks(Span<const DataBlock> blocks) const final {
-        return incfs::writeBlocks(blocks);
+    ErrorCode writeBlocks(std::span<const incfs::DataBlock> blocks) const final {
+        return incfs::writeBlocks({blocks.data(), size_t(blocks.size())});
     }
 };
 
@@ -165,8 +180,9 @@
 }
 
 std::unique_ptr<DataLoaderManagerWrapper> RealServiceManager::getDataLoaderManager() {
-    sp<IDataLoaderManager> manager =
-            RealServiceManager::getRealService<IDataLoaderManager>(kDataLoaderManagerName);
+    sp<content::pm::IDataLoaderManager> manager =
+            RealServiceManager::getRealService<content::pm::IDataLoaderManager>(
+                    kDataLoaderManagerName);
     if (manager) {
         return std::make_unique<RealDataLoaderManager>(manager);
     }
@@ -239,4 +255,4 @@
     return getJavaVm(env);
 }
 
-} // namespace android::os::incremental
+} // namespace android::incremental
diff --git a/services/incremental/ServiceWrappers.h b/services/incremental/ServiceWrappers.h
index 142bf2e..39f8541 100644
--- a/services/incremental/ServiceWrappers.h
+++ b/services/incremental/ServiceWrappers.h
@@ -16,29 +16,23 @@
 
 #pragma once
 
-#include "IncrementalServiceValidation.h"
-
-#include <android-base/strings.h>
 #include <android-base/unique_fd.h>
 #include <android/content/pm/DataLoaderParamsParcel.h>
 #include <android/content/pm/FileSystemControlParcel.h>
 #include <android/content/pm/IDataLoader.h>
-#include <android/content/pm/IDataLoaderManager.h>
 #include <android/content/pm/IDataLoaderStatusListener.h>
-#include <android/os/IVold.h>
-#include <binder/AppOpsManager.h>
+#include <binder/IAppOpsCallback.h>
 #include <binder/IServiceManager.h>
+#include <binder/Status.h>
 #include <incfs.h>
 #include <jni.h>
 
 #include <memory>
+#include <span>
 #include <string>
 #include <string_view>
 
-using namespace android::incfs;
-using namespace android::content::pm;
-
-namespace android::os::incremental {
+namespace android::incremental {
 
 // --- Wrapper interfaces ---
 
@@ -47,42 +41,54 @@
 class VoldServiceWrapper {
 public:
     virtual ~VoldServiceWrapper() = default;
-    virtual binder::Status mountIncFs(const std::string& backingPath, const std::string& targetDir,
-                                      int32_t flags,
-                                      IncrementalFileSystemControlParcel* _aidl_return) const = 0;
+    virtual binder::Status mountIncFs(
+            const std::string& backingPath, const std::string& targetDir, int32_t flags,
+            os::incremental::IncrementalFileSystemControlParcel* result) const = 0;
     virtual binder::Status unmountIncFs(const std::string& dir) const = 0;
     virtual binder::Status bindMount(const std::string& sourceDir,
                                      const std::string& targetDir) const = 0;
-    virtual binder::Status setIncFsMountOptions(const ::android::os::incremental::IncrementalFileSystemControlParcel& control, bool enableReadLogs) const = 0;
+    virtual binder::Status setIncFsMountOptions(
+            const os::incremental::IncrementalFileSystemControlParcel& control,
+            bool enableReadLogs) const = 0;
 };
 
 class DataLoaderManagerWrapper {
 public:
     virtual ~DataLoaderManagerWrapper() = default;
-    virtual binder::Status initializeDataLoader(MountId mountId,
-                                                const DataLoaderParamsParcel& params,
-                                                const FileSystemControlParcel& control,
-                                                const sp<IDataLoaderStatusListener>& listener,
-                                                bool* _aidl_return) const = 0;
-    virtual binder::Status getDataLoader(MountId mountId, sp<IDataLoader>* _aidl_return) const = 0;
+    virtual binder::Status initializeDataLoader(
+            MountId mountId, const content::pm::DataLoaderParamsParcel& params,
+            const content::pm::FileSystemControlParcel& control,
+            const sp<content::pm::IDataLoaderStatusListener>& listener, bool* result) const = 0;
+    virtual binder::Status getDataLoader(MountId mountId,
+                                         sp<content::pm::IDataLoader>* result) const = 0;
     virtual binder::Status destroyDataLoader(MountId mountId) const = 0;
 };
 
 class IncFsWrapper {
 public:
+    using Control = incfs::Control;
+    using FileId = incfs::FileId;
+    using ErrorCode = incfs::ErrorCode;
+
+    using ExistingMountCallback =
+            std::function<void(std::string_view root, std::string_view backingDir,
+                               std::span<std::pair<std::string_view, std::string_view>> binds)>;
+
     virtual ~IncFsWrapper() = default;
+    virtual void listExistingMounts(const ExistingMountCallback& cb) const = 0;
+    virtual Control openMount(std::string_view path) const = 0;
     virtual Control createControl(IncFsFd cmd, IncFsFd pendingReads, IncFsFd logs) const = 0;
     virtual ErrorCode makeFile(const Control& control, std::string_view path, int mode, FileId id,
-                               NewFileParams params) const = 0;
+                               incfs::NewFileParams params) const = 0;
     virtual ErrorCode makeDir(const Control& control, std::string_view path, int mode) const = 0;
-    virtual RawMetadata getMetadata(const Control& control, FileId fileid) const = 0;
-    virtual RawMetadata getMetadata(const Control& control, std::string_view path) const = 0;
+    virtual incfs::RawMetadata getMetadata(const Control& control, FileId fileid) const = 0;
+    virtual incfs::RawMetadata getMetadata(const Control& control, std::string_view path) const = 0;
     virtual FileId getFileId(const Control& control, std::string_view path) const = 0;
     virtual ErrorCode link(const Control& control, std::string_view from,
                            std::string_view to) const = 0;
     virtual ErrorCode unlink(const Control& control, std::string_view path) const = 0;
     virtual base::unique_fd openForSpecialOps(const Control& control, FileId id) const = 0;
-    virtual ErrorCode writeBlocks(Span<const DataBlock> blocks) const = 0;
+    virtual ErrorCode writeBlocks(std::span<const incfs::DataBlock> blocks) const = 0;
 };
 
 class AppOpsManagerWrapper {
@@ -129,4 +135,4 @@
     JavaVM* const mJvm;
 };
 
-} // namespace android::os::incremental
+} // namespace android::incremental
diff --git a/services/incremental/path.cpp b/services/incremental/path.cpp
index 0d86f2a..338659d 100644
--- a/services/incremental/path.cpp
+++ b/services/incremental/path.cpp
@@ -44,7 +44,7 @@
                                         PathCharsLess());
 }
 
-static void preparePathComponent(std::string_view path, bool trimFront) {
+static void preparePathComponent(std::string_view& path, bool trimFront) {
     if (trimFront) {
         while (!path.empty() && path.front() == '/') {
             path.remove_prefix(1);
diff --git a/services/incremental/test/IncrementalServiceTest.cpp b/services/incremental/test/IncrementalServiceTest.cpp
index bfe92f4..8a06053 100644
--- a/services/incremental/test/IncrementalServiceTest.cpp
+++ b/services/incremental/test/IncrementalServiceTest.cpp
@@ -25,6 +25,7 @@
 #include <future>
 
 #include "IncrementalService.h"
+#include "IncrementalServiceValidation.h"
 #include "Metadata.pb.h"
 #include "ServiceWrappers.h"
 
@@ -262,6 +263,8 @@
 
 class MockIncFs : public IncFsWrapper {
 public:
+    MOCK_CONST_METHOD1(listExistingMounts, void(const ExistingMountCallback& cb));
+    MOCK_CONST_METHOD1(openMount, Control(std::string_view path));
     MOCK_CONST_METHOD3(createControl, Control(IncFsFd cmd, IncFsFd pendingReads, IncFsFd logs));
     MOCK_CONST_METHOD5(makeFile,
                        ErrorCode(const Control& control, std::string_view path, int mode, FileId id,
@@ -274,10 +277,13 @@
                        ErrorCode(const Control& control, std::string_view from, std::string_view to));
     MOCK_CONST_METHOD2(unlink, ErrorCode(const Control& control, std::string_view path));
     MOCK_CONST_METHOD2(openForSpecialOps, base::unique_fd(const Control& control, FileId id));
-    MOCK_CONST_METHOD1(writeBlocks, ErrorCode(Span<const DataBlock> blocks));
+    MOCK_CONST_METHOD1(writeBlocks, ErrorCode(std::span<const DataBlock> blocks));
+
+    MockIncFs() { ON_CALL(*this, listExistingMounts(_)).WillByDefault(Return()); }
 
     void makeFileFails() { ON_CALL(*this, makeFile(_, _, _, _, _)).WillByDefault(Return(-1)); }
     void makeFileSuccess() { ON_CALL(*this, makeFile(_, _, _, _, _)).WillByDefault(Return(0)); }
+
     RawMetadata getMountInfoMetadata(const Control& control, std::string_view path) {
         metadata::Mount m;
         m.mutable_storage()->set_id(100);
@@ -692,14 +698,14 @@
                                                IncrementalService::CreateOptions::CreateNew);
     std::string dir_path("test");
 
-    std::string tempPath(tempDir.path);
-    std::replace(tempPath.begin(), tempPath.end(), '/', '_');
-    std::string mount_dir = std::string(mRootDir.path) + "/MT_" + tempPath.substr(1);
-    std::string normalized_dir_path = mount_dir + "/mount/st_1_0/" + dir_path;
-
     // Expecting incfs to call makeDir on a path like:
-    // /data/local/tmp/TemporaryDir-06yixG/data_local_tmp_TemporaryDir-xwdFhT/mount/st_1_0/test
-    EXPECT_CALL(*mIncFs, makeDir(_, std::string_view(normalized_dir_path), _));
+    // <root>/*/mount/<storage>/test
+    EXPECT_CALL(*mIncFs,
+                makeDir(_, Truly([&](std::string_view arg) {
+                            return arg.starts_with(mRootDir.path) &&
+                                    arg.ends_with("/mount/st_1_0/" + dir_path);
+                        }),
+                        _));
     auto res = mIncrementalService->makeDir(storageId, dir_path, 0555);
     ASSERT_EQ(res, 0);
 }
@@ -717,29 +723,32 @@
     auto first = "first"sv;
     auto second = "second"sv;
     auto third = "third"sv;
-
-    std::string tempPath(tempDir.path);
-    std::replace(tempPath.begin(), tempPath.end(), '/', '_');
-    std::string mount_dir = std::string(mRootDir.path) + "/MT_" + tempPath.substr(1);
-
-    InSequence seq;
     auto parent_path = std::string(first) + "/" + std::string(second);
     auto dir_path = parent_path + "/" + std::string(third);
 
-    std::string normalized_first_path = mount_dir + "/mount/st_1_0/" + std::string(first);
-    std::string normalized_parent_path = mount_dir + "/mount/st_1_0/" + parent_path;
-    std::string normalized_dir_path = mount_dir + "/mount/st_1_0/" + dir_path;
+    auto checkArgFor = [&](std::string_view expected, std::string_view arg) {
+        return arg.starts_with(mRootDir.path) && arg.ends_with("/mount/st_1_0/"s += expected);
+    };
 
-    EXPECT_CALL(*mIncFs, makeDir(_, std::string_view(normalized_dir_path), _))
-            .WillOnce(Return(-ENOENT));
-    EXPECT_CALL(*mIncFs, makeDir(_, std::string_view(normalized_parent_path), _))
-            .WillOnce(Return(-ENOENT));
-    EXPECT_CALL(*mIncFs, makeDir(_, std::string_view(normalized_first_path), _))
-            .WillOnce(Return(0));
-    EXPECT_CALL(*mIncFs, makeDir(_, std::string_view(normalized_parent_path), _))
-            .WillOnce(Return(0));
-    EXPECT_CALL(*mIncFs, makeDir(_, std::string_view(normalized_dir_path), _)).WillOnce(Return(0));
-    auto res = mIncrementalService->makeDirs(storageId, normalized_dir_path, 0555);
+    {
+        InSequence seq;
+        EXPECT_CALL(*mIncFs,
+                    makeDir(_, Truly([&](auto arg) { return checkArgFor(dir_path, arg); }), _))
+                .WillOnce(Return(-ENOENT));
+        EXPECT_CALL(*mIncFs,
+                    makeDir(_, Truly([&](auto arg) { return checkArgFor(parent_path, arg); }), _))
+                .WillOnce(Return(-ENOENT));
+        EXPECT_CALL(*mIncFs,
+                    makeDir(_, Truly([&](auto arg) { return checkArgFor(first, arg); }), _))
+                .WillOnce(Return(0));
+        EXPECT_CALL(*mIncFs,
+                    makeDir(_, Truly([&](auto arg) { return checkArgFor(parent_path, arg); }), _))
+                .WillOnce(Return(0));
+        EXPECT_CALL(*mIncFs,
+                    makeDir(_, Truly([&](auto arg) { return checkArgFor(dir_path, arg); }), _))
+                .WillOnce(Return(0));
+    }
+    auto res = mIncrementalService->makeDirs(storageId, dir_path, 0555);
     ASSERT_EQ(res, 0);
 }
 } // namespace android::os::incremental
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 289d79d..0fc333f 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -2174,12 +2174,6 @@
         mPackageManagerService.systemReady();
         t.traceEnd();
 
-        if (mIncrementalServiceHandle != 0) {
-            t.traceBegin("MakeIncrementalServiceReady");
-            setIncrementalServiceSystemReady(mIncrementalServiceHandle);
-            t.traceEnd();
-        }
-
         t.traceBegin("MakeDisplayManagerServiceReady");
         try {
             // TODO: use boot phase and communicate these flags some other way
@@ -2449,6 +2443,12 @@
                 reportWtf("Notifying incident daemon running", e);
             }
             t.traceEnd();
+
+            if (mIncrementalServiceHandle != 0) {
+                t.traceBegin("MakeIncrementalServiceReady");
+                setIncrementalServiceSystemReady(mIncrementalServiceHandle);
+                t.traceEnd();
+            }
         }, t);
 
         t.traceEnd(); // startOtherServices
diff --git a/services/net/Android.bp b/services/net/Android.bp
index 9f29799..bb5409b 100644
--- a/services/net/Android.bp
+++ b/services/net/Android.bp
@@ -41,6 +41,7 @@
     sdk_version: "module_current",
     libs: [
         "unsupportedappusage",
+        "framework-wifi-util-lib",
     ],
     static_libs: [
         "dnsresolver_aidl_interface-V2-java",
diff --git a/services/people/java/com/android/server/people/data/DataManager.java b/services/people/java/com/android/server/people/data/DataManager.java
index c87ece2..763e19b 100644
--- a/services/people/java/com/android/server/people/data/DataManager.java
+++ b/services/people/java/com/android/server/people/data/DataManager.java
@@ -67,6 +67,7 @@
 import com.android.internal.telephony.SmsApplication;
 import com.android.server.LocalServices;
 import com.android.server.notification.NotificationManagerInternal;
+import com.android.server.notification.ShortcutHelper;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -497,10 +498,6 @@
                 EventStore.CATEGORY_SHORTCUT_BASED, shortcutId);
     }
 
-    private boolean isPersonShortcut(@NonNull ShortcutInfo shortcutInfo) {
-        return shortcutInfo.getPersons() != null && shortcutInfo.getPersons().length != 0;
-    }
-
     @VisibleForTesting
     @WorkerThread
     void addOrUpdateConversationInfo(@NonNull ShortcutInfo shortcutInfo) {
@@ -712,7 +709,8 @@
                 @NonNull List<ShortcutInfo> shortcuts, @NonNull UserHandle user) {
             mInjector.getBackgroundExecutor().execute(() -> {
                 for (ShortcutInfo shortcut : shortcuts) {
-                    if (isPersonShortcut(shortcut)) {
+                    if (ShortcutHelper.isConversationShortcut(
+                            shortcut, mShortcutServiceInternal, user.getIdentifier())) {
                         addOrUpdateConversationInfo(shortcut);
                     }
                 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
index 7a175ca1..2983d58 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
@@ -541,7 +541,7 @@
         doReturn(new ArrayMap<IBinder, ArrayList<ConnectionRecord>>()).when(s).getConnections();
         s.startRequested = true;
         s.lastActivity = SystemClock.uptimeMillis();
-        app.services.add(s);
+        app.startService(s);
         sService.mWakefulness = PowerManagerInternal.WAKEFULNESS_AWAKE;
         sService.mOomAdjuster.updateOomAdjLocked(app, false, OomAdjuster.OOM_ADJ_REASON_NONE);
 
@@ -585,7 +585,7 @@
         doReturn(new ArrayMap<IBinder, ArrayList<ConnectionRecord>>()).when(s).getConnections();
         s.startRequested = true;
         s.lastActivity = SystemClock.uptimeMillis();
-        app.services.add(s);
+        app.startService(s);
         sService.mWakefulness = PowerManagerInternal.WAKEFULNESS_AWAKE;
         sService.mOomAdjuster.updateOomAdjLocked(app, false, OomAdjuster.OOM_ADJ_REASON_NONE);
 
@@ -1593,7 +1593,7 @@
         s.app = app3;
         setFieldValue(ServiceRecord.class, s, "connections",
                 new ArrayMap<IBinder, ArrayList<ConnectionRecord>>());
-        app3.services.add(s);
+        app3.startService(s);
         doCallRealMethod().when(s).getConnections();
         s.startRequested = true;
         s.lastActivity = now;
@@ -1698,7 +1698,7 @@
             record.app = service;
             setFieldValue(ServiceRecord.class, record, "connections",
                     new ArrayMap<IBinder, ArrayList<ConnectionRecord>>());
-            service.services.add(record);
+            service.startService(record);
             doCallRealMethod().when(record).getConnections();
         }
         AppBindRecord binding = new AppBindRecord(record, null, client);
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java
index 3352177..064e348 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.accessibility;
 
+import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_ACCESSIBILITY_ACTIONS;
+
 import static org.hamcrest.Matchers.hasItem;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.not;
@@ -308,7 +310,7 @@
                 AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT);
         verify(mMockScreenshotHelper).takeScreenshot(
                 eq(android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN), anyBoolean(),
-                anyBoolean(), any(Handler.class), any());
+                anyBoolean(), eq(SCREENSHOT_ACCESSIBILITY_ACTIONS), any(Handler.class), any());
     }
 
     // PendingIntent is a final class and cannot be mocked. So we are using this
diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java
index f8bcff5..6fe259e 100644
--- a/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java
@@ -45,6 +45,8 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.verifyZeroInteractions;
@@ -54,9 +56,11 @@
 import android.app.AppOpsManager;
 import android.app.IApplicationThread;
 import android.app.IUidObserver;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
@@ -70,6 +74,7 @@
 import androidx.test.filters.MediumTest;
 import androidx.test.filters.SmallTest;
 
+import com.android.server.LocalServices;
 import com.android.server.am.ProcessList.IsolatedUidRange;
 import com.android.server.am.ProcessList.IsolatedUidRangeAllocator;
 import com.android.server.appop.AppOpsService;
@@ -77,6 +82,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Rule;
 import org.junit.Test;
 import org.mockito.Mock;
@@ -115,6 +121,18 @@
         UidRecord.CHANGE_ACTIVE
     };
 
+    private static PackageManagerInternal sPackageManagerInternal;
+
+    @BeforeClass
+    public static void setUpOnce() {
+        sPackageManagerInternal = mock(PackageManagerInternal.class);
+        doReturn(new ComponentName("", "")).when(sPackageManagerInternal)
+                .getSystemUiServiceComponent();
+        // Remove stale instance of PackageManagerInternal if there is any
+        LocalServices.removeServiceForTest(PackageManagerInternal.class);
+        LocalServices.addService(PackageManagerInternal.class, sPackageManagerInternal);
+    }
+
     @Rule public ServiceThreadRule mServiceThreadRule = new ServiceThreadRule();
 
     private Context mContext = getInstrumentation().getTargetContext();
@@ -258,8 +276,11 @@
         uidRec.hasInternetPermission = true;
         mAms.mProcessList.mActiveUids.put(uid, uidRec);
 
-        final ProcessRecord appRec = new ProcessRecord(mAms, new ApplicationInfo(), TAG, uid);
-        appRec.thread = Mockito.mock(IApplicationThread.class);
+        ApplicationInfo info = new ApplicationInfo();
+        info.packageName = "";
+
+        final ProcessRecord appRec = new ProcessRecord(mAms, info, TAG, uid);
+        appRec.thread = mock(IApplicationThread.class);
         mAms.mProcessList.mLruProcesses.add(appRec);
 
         return uidRec;
@@ -497,7 +518,7 @@
         };
         final IUidObserver[] observers = new IUidObserver.Stub[changesToObserve.length];
         for (int i = 0; i < observers.length; ++i) {
-            observers[i] = Mockito.mock(IUidObserver.Stub.class);
+            observers[i] = mock(IUidObserver.Stub.class);
             when(observers[i].asBinder()).thenReturn((IBinder) observers[i]);
             mAms.registerUidObserver(observers[i], changesToObserve[i] /* which */,
                     ActivityManager.PROCESS_STATE_UNKNOWN /* cutpoint */, null /* caller */);
@@ -610,7 +631,7 @@
      */
     @Test
     public void testDispatchUidChanges_procStateCutpoint() throws RemoteException {
-        final IUidObserver observer = Mockito.mock(IUidObserver.Stub.class);
+        final IUidObserver observer = mock(IUidObserver.Stub.class);
 
         when(observer.asBinder()).thenReturn((IBinder) observer);
         mAms.registerUidObserver(observer, ActivityManager.UID_OBSERVER_PROCSTATE /* which */,
@@ -704,7 +725,7 @@
         assertEquals("No observers registered, so validateUids should be empty",
                 0, mAms.mValidateUids.size());
 
-        final IUidObserver observer = Mockito.mock(IUidObserver.Stub.class);
+        final IUidObserver observer = mock(IUidObserver.Stub.class);
         when(observer.asBinder()).thenReturn((IBinder) observer);
         mAms.registerUidObserver(observer, 0, 0, null);
         // Verify that when observers are registered, then validateUids is correctly updated.
diff --git a/services/tests/servicestests/src/com/android/server/am/OomAdjusterTests.java b/services/tests/servicestests/src/com/android/server/am/OomAdjusterTests.java
index d12d804..b2d7177 100644
--- a/services/tests/servicestests/src/com/android/server/am/OomAdjusterTests.java
+++ b/services/tests/servicestests/src/com/android/server/am/OomAdjusterTests.java
@@ -22,12 +22,15 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 
 import android.app.ActivityManager;
 import android.app.usage.UsageStatsManagerInternal;
+import android.content.ComponentName;
 import android.content.Context;
+import android.content.pm.PackageManagerInternal;
 
 import com.android.server.LocalServices;
 import com.android.server.wm.ActivityTaskManagerService;
@@ -45,6 +48,7 @@
 public class OomAdjusterTests {
     private static Context sContext;
     private static ActivityManagerService sService;
+    private static PackageManagerInternal sPackageManagerInternal;
 
     private ProcessRecord mProcessRecord;
 
@@ -56,6 +60,13 @@
     public static void setUpOnce() {
         sContext = getInstrumentation().getTargetContext();
 
+        sPackageManagerInternal = mock(PackageManagerInternal.class);
+        doReturn(new ComponentName("", "")).when(sPackageManagerInternal)
+                .getSystemUiServiceComponent();
+        // Remove stale instance of PackageManagerInternal if there is any
+        LocalServices.removeServiceForTest(PackageManagerInternal.class);
+        LocalServices.addService(PackageManagerInternal.class, sPackageManagerInternal);
+
         // We need to run with dexmaker share class loader to make use of
         // ActivityTaskManagerService from wm package.
         runWithDexmakerShareClassLoader(() -> {
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceMigrationTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceMigrationTest.java
index 74e7f8c..a0b9d9d 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceMigrationTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceMigrationTest.java
@@ -62,6 +62,10 @@
 
         mContext = getContext();
 
+        // Make createContextAsUser to work.
+        mContext.packageName = "com.android.frameworks.servicestests";
+        getServices().addPackageContext(UserHandle.of(0), mContext);
+
         when(getServices().packageManager.hasSystemFeature(eq(PackageManager.FEATURE_DEVICE_ADMIN)))
                 .thenReturn(true);
     }
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
index 3e5c21c..cbe49eb 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
@@ -124,6 +124,9 @@
         // Key is a pair of uri and userId
         private final Map<Pair<Uri, Integer>, ContentObserver> mContentObservers = new ArrayMap<>();
 
+        // Used as an override when set to nonzero.
+        private long mCurrentTimeMillis = 0;
+
         public MockInjector(MockSystemServices services, DpmMockContext context) {
             super(context);
             this.services = services;
@@ -470,5 +473,19 @@
 
         @Override
         public void runCryptoSelfTest() {}
+
+        @Override
+        public String[] getPersonalAppsForSuspension(int userId) {
+            return new String[]{};
+        }
+
+        public void setSystemCurrentTimeMillis(long value) {
+            mCurrentTimeMillis = value;
+        }
+
+        @Override
+        public long systemCurrentTimeMillis() {
+            return mCurrentTimeMillis != 0 ? mCurrentTimeMillis : System.currentTimeMillis();
+        }
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index 09d1d3a..b042e77 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -44,6 +44,7 @@
 import static org.mockito.Matchers.isNull;
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.atMost;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.never;
@@ -62,6 +63,7 @@
 import android.app.Activity;
 import android.app.AppOpsManager;
 import android.app.Notification;
+import android.app.PendingIntent;
 import android.app.admin.DeviceAdminReceiver;
 import android.app.admin.DevicePolicyManager;
 import android.app.admin.DevicePolicyManagerInternal;
@@ -96,6 +98,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.R;
+import com.android.internal.messages.nano.SystemMessageProto;
 import com.android.internal.widget.LockscreenCredential;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
@@ -103,6 +106,7 @@
 
 import org.hamcrest.BaseMatcher;
 import org.hamcrest.Description;
+import org.hamcrest.Matcher;
 import org.mockito.Mockito;
 import org.mockito.internal.util.collections.Sets;
 import org.mockito.stubbing.Answer;
@@ -181,6 +185,20 @@
             "wQ==\n" +
             "-----END CERTIFICATE-----\n";
 
+    // Constants for testing setManagedProfileMaximumTimeOff:
+    // Profile maximum time off value
+    private static final long PROFILE_OFF_TIMEOUT = TimeUnit.DAYS.toMillis(5);
+    // Synthetic time at the beginning of test.
+    private static final long PROFILE_OFF_START = 1;
+    // Time when warning notification should be posted,
+    private static final long PROFILE_OFF_WARNING_TIME =
+            PROFILE_OFF_START + PROFILE_OFF_TIMEOUT - TimeUnit.DAYS.toMillis(1);
+    // Time when the apps should be suspended
+    private static final long PROFILE_OFF_DEADLINE = PROFILE_OFF_START + PROFILE_OFF_TIMEOUT;
+    // Notification titles for setManagedProfileMaximumTimeOff tests:
+    private static final String PROFILE_OFF_WARNING_TITLE = "suspended_tomorrow";
+    private static final String PROFILE_OFF_SUSPENDED_TITLE = "suspended";
+
     @Override
     protected void setUp() throws Exception {
         super.setUp();
@@ -196,6 +214,11 @@
                         anyInt(),
                         any(UserHandle.class));
 
+        // Make createContextAsUser to work.
+        mContext.packageName = "com.android.frameworks.servicestests";
+        getServices().addPackageContext(UserHandle.of(0), mContext);
+        getServices().addPackageContext(UserHandle.of(DpmMockContext.CALLER_USER_HANDLE), mContext);
+
         // By default, pretend all users are running and unlocked.
         when(getServices().userManager.isUserUnlocked(anyInt())).thenReturn(true);
 
@@ -1553,20 +1576,7 @@
         dpms.approveCaCert(fourCerts.getList().get(1), userId, true);
         // a notification should be shown saying that there are two certificates left to approve.
         verify(getServices().notificationManager, timeout(1000))
-                .notifyAsUser(anyString(), anyInt(), argThat(
-                        new BaseMatcher<Notification>() {
-                            @Override
-                            public boolean matches(Object item) {
-                                final Notification noti = (Notification) item;
-                                return TEST_STRING.equals(
-                                        noti.extras.getString(Notification.EXTRA_TITLE));
-                            }
-                            @Override
-                            public void describeTo(Description description) {
-                                description.appendText(
-                                        "Notification{title=\"" + TEST_STRING + "\"}");
-                            }
-                        }), eq(user));
+                .notifyAsUser(anyString(), anyInt(), argThat(hasTitle(TEST_STRING)), eq(user));
     }
 
     /**
@@ -6267,7 +6277,214 @@
         assertThat(dpm.getAccountTypesWithManagementDisabled()).isEmpty();
     }
 
-    // admin1 is the outgoing DPC, adminAnotherPakcage is the incoming one.
+    /**
+     * Tests the case when the user doesn't turn the profile on in time, verifies that the user is
+     * warned with a notification and then the apps get suspended.
+     */
+    public void testMaximumProfileTimeOff_profileOffTimeExceeded() throws Exception {
+        prepareMocksForSetMaximumProfileTimeOff();
+
+        mContext.binder.callingUid = DpmMockContext.CALLER_UID;
+        dpm.setManagedProfileMaximumTimeOff(admin1, PROFILE_OFF_TIMEOUT);
+
+        mContext.binder.callingUid = DpmMockContext.SYSTEM_UID;
+        // The profile is running, neither alarm nor notification should be posted.
+        verify(getServices().alarmManager, never())
+                .set(anyInt(), anyLong(), any(PendingIntent.class));
+        verify(getServices().notificationManager, never())
+                .notify(anyInt(), any(Notification.class));
+        // Apps shouldn't be suspended.
+        verifyZeroInteractions(getServices().ipackageManager);
+        clearInvocations(getServices().alarmManager);
+
+        sendUserStoppedBroadcastForProfile();
+
+        // Verify the alarm was scheduled for time when the warning should be shown.
+        verify(getServices().alarmManager, times(1))
+                .set(anyInt(), eq(PROFILE_OFF_WARNING_TIME), any());
+        // But still no notification should be posted at this point.
+        verify(getServices().notificationManager, never())
+                .notify(anyInt(), any(Notification.class));
+        // Apps shouldn't be suspended.
+        verifyZeroInteractions(getServices().ipackageManager);
+        clearInvocations(getServices().alarmManager);
+
+        // Pretend the alarm went off.
+        dpms.mMockInjector.setSystemCurrentTimeMillis(PROFILE_OFF_WARNING_TIME + 10);
+        sendProfileOffDeadlineAlarmBroadcast();
+
+        // Verify the alarm was scheduled for the actual deadline this time.
+        verify(getServices().alarmManager, times(1)).set(anyInt(), eq(PROFILE_OFF_DEADLINE), any());
+        // Now the user should see a warning notification.
+        verify(getServices().notificationManager, times(1))
+                .notify(anyInt(), argThat(hasTitle(PROFILE_OFF_WARNING_TITLE)));
+        // Apps shouldn't be suspended yet.
+        verifyZeroInteractions(getServices().ipackageManager);
+        clearInvocations(getServices().alarmManager);
+        clearInvocations(getServices().notificationManager);
+
+        // Pretend the alarm went off.
+        dpms.mMockInjector.setSystemCurrentTimeMillis(PROFILE_OFF_DEADLINE + 10);
+        sendProfileOffDeadlineAlarmBroadcast();
+
+        // Verify the alarm was not set.
+        verifyZeroInteractions(getServices().alarmManager);
+        // Now the user should see a notification about suspended apps.
+        verify(getServices().notificationManager, times(1))
+                .notify(anyInt(), argThat(hasTitle(PROFILE_OFF_SUSPENDED_TITLE)));
+        // Verify that the apps are suspended.
+        verify(getServices().ipackageManager, times(1)).setPackagesSuspendedAsUser(
+                any(), eq(true), any(), any(), any(), any(), anyInt());
+    }
+
+    /**
+     * Tests the case when the user turns the profile back on long before the deadline (> 1 day).
+     */
+    public void testMaximumProfileTimeOff_turnOnBeforeWarning() throws Exception {
+        prepareMocksForSetMaximumProfileTimeOff();
+
+        mContext.binder.callingUid = DpmMockContext.CALLER_UID;
+        dpm.setManagedProfileMaximumTimeOff(admin1, PROFILE_OFF_TIMEOUT);
+
+        mContext.binder.callingUid = DpmMockContext.SYSTEM_UID;
+        sendUserStoppedBroadcastForProfile();
+        clearInvocations(getServices().alarmManager);
+        sendUserUnlockedBroadcastForProfile();
+
+        // Verify that the alarm got discharged.
+        verify(getServices().alarmManager, times(1)).cancel((PendingIntent) null);
+    }
+
+    /**
+     * Tests the case when the user turns the profile back on after the warning notification.
+     */
+    public void testMaximumProfileTimeOff_turnOnAfterWarning() throws Exception {
+        prepareMocksForSetMaximumProfileTimeOff();
+
+        mContext.binder.callingUid = DpmMockContext.CALLER_UID;
+        dpm.setManagedProfileMaximumTimeOff(admin1, PROFILE_OFF_TIMEOUT);
+
+        mContext.binder.callingUid = DpmMockContext.SYSTEM_UID;
+        sendUserStoppedBroadcastForProfile();
+
+        // Pretend the alarm went off.
+        dpms.mMockInjector.setSystemCurrentTimeMillis(PROFILE_OFF_WARNING_TIME + 10);
+        sendProfileOffDeadlineAlarmBroadcast();
+
+        clearInvocations(getServices().alarmManager);
+        clearInvocations(getServices().notificationManager);
+        sendUserUnlockedBroadcastForProfile();
+
+        // Verify that the alarm got discharged.
+        verify(getServices().alarmManager, times(1)).cancel((PendingIntent) null);
+        // Verify that the notification is removed.
+        verify(getServices().notificationManager, times(1))
+                .cancel(eq(SystemMessageProto.SystemMessage.NOTE_PERSONAL_APPS_SUSPENDED));
+    }
+
+    /**
+     * Tests the case when the user turns the profile back on when the apps are already suspended.
+     */
+    public void testMaximumProfileTimeOff_turnOnAfterDeadline() throws Exception {
+        prepareMocksForSetMaximumProfileTimeOff();
+
+        mContext.binder.callingUid = DpmMockContext.CALLER_UID;
+        dpm.setManagedProfileMaximumTimeOff(admin1, PROFILE_OFF_TIMEOUT);
+
+        mContext.binder.callingUid = DpmMockContext.SYSTEM_UID;
+        sendUserStoppedBroadcastForProfile();
+
+        // Pretend the alarm went off after the deadline.
+        dpms.mMockInjector.setSystemCurrentTimeMillis(PROFILE_OFF_DEADLINE + 10);
+        sendProfileOffDeadlineAlarmBroadcast();
+
+        clearInvocations(getServices().alarmManager);
+        clearInvocations(getServices().notificationManager);
+        clearInvocations(getServices().ipackageManager);
+
+        sendUserUnlockedBroadcastForProfile();
+
+        // Verify that the notification is removed (at this point DPC should show it).
+        verify(getServices().notificationManager, times(1))
+                .cancel(eq(SystemMessageProto.SystemMessage.NOTE_PERSONAL_APPS_SUSPENDED));
+        // Verify that the apps are NOT unsuspeded.
+        verify(getServices().ipackageManager, never()).setPackagesSuspendedAsUser(
+                any(), eq(false), any(), any(), any(), any(), anyInt());
+    }
+
+    private void sendUserUnlockedBroadcastForProfile() throws Exception {
+        when(getServices().userManager.isUserUnlocked(eq(DpmMockContext.CALLER_USER_HANDLE)))
+                .thenReturn(true);
+        final Intent unlockedIntent = new Intent(Intent.ACTION_USER_UNLOCKED)
+                .putExtra(Intent.EXTRA_USER_HANDLE, DpmMockContext.CALLER_USER_HANDLE);
+        getServices().injectBroadcast(
+                mServiceContext, unlockedIntent, DpmMockContext.CALLER_USER_HANDLE);
+        flushTasks();
+    }
+
+
+    private void sendProfileOffDeadlineAlarmBroadcast() throws Exception {
+        final Intent deadlineAlarmIntent =
+                new Intent(DevicePolicyManagerService.ACTION_PROFILE_OFF_DEADLINE);
+        getServices().injectBroadcast(
+                mServiceContext, deadlineAlarmIntent, DpmMockContext.CALLER_USER_HANDLE);
+        flushTasks();
+    }
+
+    private void sendUserStoppedBroadcastForProfile() throws Exception {
+        when(getServices().userManager.isUserUnlocked(eq(DpmMockContext.CALLER_USER_HANDLE)))
+                .thenReturn(false);
+        final Intent stoppedIntent = new Intent(Intent.ACTION_USER_STOPPED)
+                .putExtra(Intent.EXTRA_USER_HANDLE, DpmMockContext.CALLER_USER_HANDLE);
+        getServices().injectBroadcast(mServiceContext, stoppedIntent,
+                DpmMockContext.CALLER_USER_HANDLE);
+        flushTasks();
+    }
+
+    private void prepareMocksForSetMaximumProfileTimeOff() throws Exception {
+        addManagedProfile(admin1, DpmMockContext.CALLER_UID, admin1);
+        configureProfileOwnerOfOrgOwnedDevice(admin1, DpmMockContext.CALLER_USER_HANDLE);
+
+        when(getServices().userManager.isUserUnlocked()).thenReturn(true);
+
+        // Pretend our admin handles CHECK_POLICY_COMPLIANCE intent.
+        final Intent intent = new Intent(DevicePolicyManager.ACTION_CHECK_POLICY_COMPLIANCE);
+        intent.setPackage(admin1.getPackageName());
+
+        doReturn(Collections.singletonList(new ResolveInfo()))
+                .when(getServices().packageManager).queryIntentActivitiesAsUser(
+                        any(Intent.class), anyInt(), eq(DpmMockContext.CALLER_USER_HANDLE));
+
+        dpms.mMockInjector.setSystemCurrentTimeMillis(PROFILE_OFF_START);
+        // To allow creation of Notification via Notification.Builder
+        mContext.applicationInfo = mRealTestContext.getApplicationInfo();
+
+        // Setup notification titles.
+        when(mServiceContext.resources
+                .getString(R.string.personal_apps_suspended_tomorrow_title))
+                .thenReturn(PROFILE_OFF_WARNING_TITLE);
+        when(mServiceContext.resources
+                .getString(R.string.personal_apps_suspended_title))
+                .thenReturn(PROFILE_OFF_SUSPENDED_TITLE);
+
+        clearInvocations(getServices().ipackageManager);
+    }
+
+    private static Matcher<Notification> hasTitle(String expected) {
+        return new BaseMatcher<Notification>() {
+            @Override
+            public boolean matches(Object item) {
+                final Notification notification = (Notification) item;
+                return expected.equals(notification.extras.getString(Notification.EXTRA_TITLE));
+            }
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("Notification{title=\"" + expected + "\"}");
+            }
+        };
+    }
+
+    // admin1 is the outgoing DPC, adminAnotherPackage is the incoming one.
     private void assertDeviceOwnershipRevertedWithFakeTransferMetadata() throws Exception {
         writeFakeTransferMetadataFile(UserHandle.USER_SYSTEM,
                 TransferOwnershipMetadataManager.ADMIN_TYPE_DEVICE_OWNER);
@@ -6294,7 +6511,7 @@
         mServiceContext.binder.restoreCallingIdentity(ident);
     }
 
-    // admin1 is the outgoing DPC, adminAnotherPakcage is the incoming one.
+    // admin1 is the outgoing DPC, adminAnotherPackage is the incoming one.
     private void assertProfileOwnershipRevertedWithFakeTransferMetadata() throws Exception {
         writeFakeTransferMetadataFile(DpmMockContext.CALLER_USER_HANDLE,
                 TransferOwnershipMetadataManager.ADMIN_TYPE_PROFILE_OWNER);
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
index 8625a1e..20716ab 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
@@ -460,6 +460,15 @@
     }
 
     @Override
+    public Context createContextAsUser(UserHandle user, int flags) {
+        try {
+            return mMockSystemServices.createPackageContextAsUser(packageName, flags, user);
+        } catch (PackageManager.NameNotFoundException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    @Override
     public ContentResolver getContentResolver() {
         return mMockSystemServices.contentResolver;
     }
diff --git a/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java b/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
index 7b3417a..b69cc47 100644
--- a/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
@@ -107,9 +107,10 @@
                 eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
         SensorEventListener listener = listenerCaptor.getValue();
 
-        // Set up system to return 5 as a brightness value
+        // Set up system to return 0.02f as a brightness value
         float lux1 = 100.0f;
-        float normalizedBrightness1 = 0.0158f; //float equivalent of 5 out of 255
+        // Brightness as float (from 0.0f to 1.0f)
+        float normalizedBrightness1 = 0.02f;
         when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux1))
                 .thenReturn(lux1);
         when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux1))
@@ -120,14 +121,14 @@
         // This is the important bit: When the new brightness is set, make sure the new
         // brightening threshold is beyond the maximum brightness value...so that we can test that
         // our threshold clamping works.
-        when(mScreenBrightnessThresholds.getBrighteningThreshold(5)).thenReturn(1.0f);
+        when(mScreenBrightnessThresholds.getBrighteningThreshold(normalizedBrightness1))
+                .thenReturn(1.0f);
 
         // Send new sensor value and verify
         listener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, (int) lux1));
-        assertEquals(5, controller.getAutomaticScreenBrightness());
+        assertEquals(normalizedBrightness1, controller.getAutomaticScreenBrightness(), 0.001f);
 
-
-        // Set up system to return 255 as a brightness value
+        // Set up system to return 0.0f (minimum possible brightness) as a brightness value
         float lux2 = 10.0f;
         float normalizedBrightness2 = 0.0f;
         when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux2))
@@ -139,7 +140,7 @@
 
         // Send new sensor value and verify
         listener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, (int) lux2));
-        assertEquals(1, controller.getAutomaticScreenBrightness());
+        assertEquals(normalizedBrightness2, controller.getAutomaticScreenBrightness(), 0.001f);
     }
 
     @Test
@@ -153,9 +154,9 @@
                 eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
         SensorEventListener listener = listenerCaptor.getValue();
 
-        // Set up system to return 250 as a brightness value
+        // Set up system to return 0.98f as a brightness value
         float lux1 = 100.0f;
-        float normalizedBrightness1 = 0.981f; //float equivalent of 250 out of 255
+        float normalizedBrightness1 = 0.98f;
         when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux1))
                 .thenReturn(lux1);
         when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux1))
@@ -166,14 +167,15 @@
         // This is the important bit: When the new brightness is set, make sure the new
         // brightening threshold is beyond the maximum brightness value...so that we can test that
         // our threshold clamping works.
-        when(mScreenBrightnessThresholds.getBrighteningThreshold(250)).thenReturn(260.0f);
+        when(mScreenBrightnessThresholds.getBrighteningThreshold(normalizedBrightness1))
+                .thenReturn(1.1f);
 
         // Send new sensor value and verify
         listener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, (int) lux1));
-        assertEquals(250, controller.getAutomaticScreenBrightness());
+        assertEquals(normalizedBrightness1, controller.getAutomaticScreenBrightness(), 0.001f);
 
 
-        // Set up system to return 255 as a brightness value
+        // Set up system to return 1.0f as a brightness value (brightness_max)
         float lux2 = 110.0f;
         float normalizedBrightness2 = 1.0f;
         when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux2))
@@ -185,7 +187,7 @@
 
         // Send new sensor value and verify
         listener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, (int) lux2));
-        assertEquals(255, controller.getAutomaticScreenBrightness());
+        assertEquals(normalizedBrightness2, controller.getAutomaticScreenBrightness(), 0.001f);
     }
 
     @Test
@@ -204,10 +206,10 @@
 
         // User sets brightness to 100
         controller.configure(true /* enable */, null /* configuration */,
-                100 /* brightness */, true /* userChangedBrightness */, 0 /* adjustment */,
+                0.5f /* brightness */, true /* userChangedBrightness */, 0 /* adjustment */,
                 false /* userChanged */, DisplayPowerRequest.POLICY_BRIGHT);
 
         // There should be a user data point added to the mapper.
-        verify(mBrightnessMappingStrategy).addUserDataPoint(1000f, 100);
+        verify(mBrightnessMappingStrategy).addUserDataPoint(1000f, 0.5f);
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/emergency/EmergencyAffordanceServiceTest.java b/services/tests/servicestests/src/com/android/server/emergency/EmergencyAffordanceServiceTest.java
new file mode 100644
index 0000000..d438a0e
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/emergency/EmergencyAffordanceServiceTest.java
@@ -0,0 +1,486 @@
+/*
+ * 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.server.emergency;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
+import android.telephony.TelephonyManager;
+import android.test.mock.MockContentResolver;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.internal.util.test.BroadcastInterceptingContext;
+import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.server.SystemService;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+/**
+ * Unit test for EmergencyAffordanceService (EAS for short) which determines when
+ * should we enable Emergency Affordance feature (EA for short).
+ *
+ * Please refer to https://source.android.com/devices/tech/connect/emergency-affordance
+ * to see the details of the feature.
+ */
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class EmergencyAffordanceServiceTest {
+
+    // Default country ISO that should enable EA. Value comes from resource
+    // com.android.internal.R.array.config_emergency_iso_country_codes
+    private static final String EMERGENCY_ISO_CODE = "in";
+    // Randomly picked country ISO that should not enable EA.
+    private static final String NON_EMERGENCY_ISO_CODE = "us";
+
+    // Valid values for Settings.Global.EMERGENCY_AFFORDANCE_NEEDED
+    private static final int OFF = 0; // which means feature disabled
+    private static final int ON  = 1; // which means feature enabled
+
+    private static final int ACTIVE_MODEM_COUNT = 2;
+
+    @Mock private Resources mResources;
+    @Mock private SubscriptionManager mSubscriptionManager;
+    @Mock private TelephonyManager mTelephonyManager;
+
+    private TestContext mServiceContext;
+    private MockContentResolver mContentResolver;
+    private OnSubscriptionsChangedListener mSubscriptionChangedListener;
+    private EmergencyAffordanceService mService;
+
+    // Testable Context that mocks resources, content resolver and system services
+    private class TestContext extends BroadcastInterceptingContext {
+        TestContext(Context base) {
+            super(base);
+        }
+
+        @Override
+        public ContentResolver getContentResolver() {
+            return mContentResolver;
+        }
+
+        @Override
+        public Resources getResources() {
+            return mResources;
+        }
+
+        @Override
+        public Object getSystemService(String name) {
+            switch (name) {
+                case Context.TELEPHONY_SUBSCRIPTION_SERVICE:
+                    return mSubscriptionManager;
+                case Context.TELEPHONY_SERVICE:
+                    return mTelephonyManager;
+                default:
+                    return super.getSystemService(name);
+            }
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        doReturn(new String[] { EMERGENCY_ISO_CODE }).when(mResources)
+                .getStringArray(com.android.internal.R.array.config_emergency_iso_country_codes);
+
+        final Context context = InstrumentationRegistry.getContext();
+        mServiceContext = new TestContext(context);
+        mContentResolver = new MockContentResolver(mServiceContext);
+        mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
+
+        // Initialize feature off, to have constant starting
+        Settings.Global.putInt(mContentResolver, Settings.Global.EMERGENCY_AFFORDANCE_NEEDED, 0);
+        mService = new EmergencyAffordanceService(mServiceContext);
+    }
+
+    /**
+     * Verify if the device is not voice capable, the feature should be disabled.
+     */
+    @Test
+    public void testSettings_shouldBeOff_whenVoiceCapableIsFalse() throws Exception {
+        // Given: the device is not voice capable
+        // When:  setup device and boot service
+        setUpDevice(false /* withVoiceCapable */, true /* withEmergencyIsoInSim */,
+                true /* withEmergencyIsoInCell */);
+
+        // Then: EA setting will should be 0
+        verifyEmergencyAffordanceNeededSettings(OFF);
+    }
+
+    /**
+     * Verify the voice capable device is booted up without EA-enabled cell network, with
+     * no EA-enabled SIM installed, feature should be disabled.
+     */
+    @Test
+    public void testSettings_shouldBeOff_whenWithoutEAEanbledNetworkNorSim() throws Exception {
+        // Given: the device is voice capble, no EA-enable SIM, no EA-enabled Cell
+        setUpDevice(true /* withVoiceCapable */, false /* withEmergencyIsoInSim */,
+                false /* withEmergencyIsoInCell */);
+
+        // Then: EA setting will should be 0
+        verifyEmergencyAffordanceNeededSettings(OFF);
+    }
+
+    /**
+     * Verify the voice capable device is booted up with EA-enabled SIM installed, the
+     * feature should be enabled.
+     */
+    @Test
+    public void testSettings_shouldBeOn_whenBootUpWithEAEanbledSim() throws Exception {
+        // Given: the device is voice capble, with EA-enable SIM, no EA-enabled Cell
+        setUpDevice(true /* withVoiceCapable */, true /* withEmergencyIsoInSim */,
+                false /* withEmergencyIsoInCell */);
+
+        // Then: EA setting will immediately update to 1
+        verifyEmergencyAffordanceNeededSettings(ON);
+    }
+
+    /**
+     * Verify the voice capable device is booted up with EA-enabled Cell network, the
+     * feature should be enabled.
+     */
+    @Test
+    public void testSettings_shouldBeOn_whenBootUpWithEAEanbledCell() throws Exception {
+        // Given: the device is voice capble, with EA-enable SIM, with EA-enabled Cell
+        setUpDevice(true /* withVoiceCapable */, false /* withEmergencyIsoInSim */,
+                true /* withEmergencyIsoInCell */);
+
+        // Then: EA setting will immediately update to 1
+        verifyEmergencyAffordanceNeededSettings(ON);
+    }
+
+    /**
+     * Verify when device boot up with no EA-enabled SIM, but later install one,
+     * feature should be enabled.
+     */
+    @Test
+    public void testSettings_shouldBeOn_whenSubscriptionInfoChangedWithEmergencyIso()
+            throws Exception {
+        // Given: the device is voice capable, boot up with no EA-enabled SIM, no EA-enabled Cell
+        setUpDevice(true /* withVoiceCapable */, false/* withEmergencyIsoInSim */,
+                false /* withEmergencyIsoInCell */);
+
+        // When: Insert EA-enabled SIM and get notified
+        setUpSim(true /* withEmergencyIsoInSim */);
+        mSubscriptionChangedListener.onSubscriptionsChanged();
+
+        // Then: EA Setting will update to 1
+        verifyEmergencyAffordanceNeededSettings(ON);
+    }
+
+    /**
+     * Verify when feature was on, device re-insert with no EA-enabled SIMs,
+     * feature should be disabled.
+     */
+    @Test
+    public void testSettings_shouldBeOff_whenSubscriptionInfoChangedWithoutEmergencyIso()
+            throws Exception {
+        // Given: the device is voice capable, no EA-enabled Cell, with EA-enabled SIM
+        setUpDevice(true /* withVoiceCapable */, true /* withEmergencyIsoInSim */,
+                false /* withEmergencyIsoInCell */);
+
+        // When: All SIMs are replaced with EA-disabled ones.
+        setUpSim(false /* withEmergencyIsoInSim */);
+        mSubscriptionChangedListener.onSubscriptionsChanged();
+
+        // Then: EA Setting will update to 0
+        verifyEmergencyAffordanceNeededSettings(OFF);
+    }
+
+    /**
+     * Verify when device boot up with no EA-enabled Cell, but later move into one,
+     * feature should be enabled.
+     */
+    @Test
+    public void testSettings_shouldBeOn_whenCountryIsoChangedWithEmergencyIso()
+            throws Exception {
+        // Given: the device is voice capable, boot up with no EA-enabled SIM, no EA-enabled Cell
+        setUpDevice(true /* withVoiceCapable */, false/* withEmergencyIsoInSim */,
+                false /* withEmergencyIsoInCell */);
+
+        // When: device locale change to EA-enabled Cell and get notified
+        resetCell(true /* withEmergencyIsoInSim */);
+        sendBroadcastNetworkCountryChanged(EMERGENCY_COUNTRY_ISO);
+
+        // Then: EA Setting will update to 1
+        verifyEmergencyAffordanceNeededSettings(ON);
+    }
+
+    /**
+     * Verify when device boot up with  EA-enabled Cell, but later move out of it,
+     * feature should be enabled.
+     */
+    @Test
+    public void testSettings_shouldBeOff_whenCountryIsoChangedWithoutEmergencyIso()
+            throws Exception {
+        // Given: the device is voice capable, boot up with no EA-enabled SIM, with EA-enabled Cell
+        setUpDevice(true /* withVoiceCapable */, false/* withEmergencyIsoInSim */,
+                true /* withEmergencyIsoInCell */);
+
+        // When: device locale change to no EA-enabled Cell and get notified
+        resetCell(false /* withEmergencyIsoInSim */);
+        sendBroadcastNetworkCountryChanged(NON_EMERGENCY_COUNTRY_ISO);
+
+        // Then: EA Setting will update to 0
+        verifyEmergencyAffordanceNeededSettings(OFF);
+    }
+    /**
+     * Verify if device is not in EA-enabled Mobile Network without EA-enable SIM(s) installed,
+     * when receive SubscriptionInfo change, the feature should not be enabled.
+     */
+    @Test
+    public void testSettings_shouldBeOff_whenNoEmergencyIsoInCellNorSim() throws Exception {
+        // Given: the device is voice capable, no EA-enabled Cell, no EA-enabled SIM
+        setUpDevice(true /* withVoiceCapable */, false /* withEmergencyIsoInSim */,
+                false /* withEmergencyIsoInCell */);
+
+        // When: Subscription changed event received
+        mSubscriptionChangedListener.onSubscriptionsChanged();
+
+        // Then: EA Settings should be 0
+        verifyEmergencyAffordanceNeededSettings(OFF);
+    }
+
+    /**
+     * Verify while feature was on, when device receive empty country iso change, while APM is
+     * enabled, feature status should keep on.
+     */
+    @Test
+    public void testSettings_shouldOn_whenReceiveEmptyISOWithAPMEnabled() throws Exception {
+        // Given: the device is voice capable,  no EA-enabled SIM, with EA-enabled Cell
+        setUpDevice(true /* withVoiceCapable */, false /* withEmergencyIsoInSim */,
+                true /* withEmergencyIsoInCell */);
+
+        // Then: EA Settings will update to 1
+        verifyEmergencyAffordanceNeededSettings(ON);
+
+        // When: Airplane mode is enabled, and we receive EMPTY ISO in locale change
+        setAirplaneMode(true);
+        sendBroadcastNetworkCountryChanged(EMPTY_COUNTRY_ISO);
+
+        // Then: EA Settings will keep to 1
+        verifyEmergencyAffordanceNeededSettings(ON);
+    }
+
+    /**
+     * Verify while feature was on, when device receive empty country iso change, while APM is
+     * disabled, feature should be disabled.
+     */
+    @Test
+    public void testSettings_shouldOff_whenReceiveEmptyISOWithAPMDisabled() throws Exception {
+        // Given: the device is voice capable,  no EA-enabled SIM, with EA-enabled Cell
+        setUpDevice(true /* withVoiceCapable */, false /* withEmergencyIsoInSim */,
+                true /* withEmergencyIsoInCell */);
+
+        // Then: EA Settings will update to 1
+        verifyEmergencyAffordanceNeededSettings(ON);
+
+        // When: Airplane mode is disabled, and we receive valid empty ISO in locale change
+        setUpCell(false /* withEmergencyIsoInCell */);
+        setAirplaneMode(false);
+        sendBroadcastNetworkCountryChanged(EMPTY_COUNTRY_ISO);
+
+        // Then: EA Settings will keep to 0
+        verifyEmergencyAffordanceNeededSettings(OFF);
+    }
+
+    /**
+     * Verify when airplane mode is turn on and off in cell network with EA-enabled ISO,
+     * feature should keep enabled.
+     */
+    @Test
+    public void testSettings_shouldBeOn_whenAirplaneModeOnOffWithEmergencyIsoInCell()
+            throws Exception {
+        // Given: the device is voice capable,  no EA-enabled SIM, with EA-enabled Cell
+        setUpDevice(true /* withVoiceCapable */, false /* withEmergencyIsoInSim */,
+                true /* withEmergencyIsoInCell */);
+
+        // When: Device receive locale change with EA-enabled iso
+        sendBroadcastNetworkCountryChanged(EMERGENCY_COUNTRY_ISO);
+
+        // When: Airplane mode is disabled
+        setAirplaneMode(false);
+
+        // Then: EA Settings will keep with 1
+        verifyEmergencyAffordanceNeededSettings(ON);
+
+        // When: Airplane mode is enabled
+        setAirplaneMode(true);
+
+        // Then: EA Settings is still 1
+        verifyEmergencyAffordanceNeededSettings(ON);
+    }
+
+    /**
+     * Verify when airplane mode is turn on and off with EA-enabled ISO in SIM,
+     * feature should keep enabled.
+     */
+    @Test
+    public void testSettings_shouldBeOn_whenAirplaneModeOnOffWithEmergencyIsoInSim()
+            throws Exception {
+        // Given: the device is voice capable, no EA-enabled Cell Network, with EA-enabled SIM
+        setUpDevice(true /* withVoiceCapable */, true /* withEmergencyIsoInSim */,
+                false /* withEmergencyIsoInCell */);
+
+        // When: Airplane mode is disabled
+        setAirplaneMode(false);
+
+        // Then: EA Settings will keep with 1
+        verifyEmergencyAffordanceNeededSettings(ON);
+
+        // When: Airplane mode is enabled
+        setAirplaneMode(true);
+
+        // Then: EA Settings is still 1
+        verifyEmergencyAffordanceNeededSettings(ON);
+    }
+
+    // EAS reads voice capable during boot up and cache it. To test non voice capable device,
+    // we can not put this in setUp
+    private void setUpDevice(boolean withVoiceCapable, boolean withEmergencyIsoInSim,
+            boolean withEmergencyIsoInCell) throws Exception {
+        setUpVoiceCapable(withVoiceCapable);
+
+        setUpSim(withEmergencyIsoInSim);
+
+        setUpCell(withEmergencyIsoInCell);
+
+        // bypass onStart which is used to publish binder service and need sepolicy policy update
+        // mService.onStart();
+
+        mService.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
+
+        if (!withVoiceCapable) {
+            return;
+        }
+
+        captureSubscriptionChangeListener();
+    }
+
+    private void setUpVoiceCapable(boolean voiceCapable) {
+        doReturn(voiceCapable).when(mTelephonyManager).isVoiceCapable();
+    }
+
+    private static final Supplier<String> EMPTY_COUNTRY_ISO = () -> "";
+    private static final Supplier<String> EMERGENCY_COUNTRY_ISO = () -> EMERGENCY_ISO_CODE;
+    private static final Supplier<String> NON_EMERGENCY_COUNTRY_ISO = () -> NON_EMERGENCY_ISO_CODE;
+    private void sendBroadcastNetworkCountryChanged(Supplier<String> countryIso) {
+        Intent intent = new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED);
+        intent.putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, countryIso.get());
+        SubscriptionManager.putPhoneIdAndSubIdExtra(intent, 0);
+        mServiceContext.sendBroadcastAsUser(intent, UserHandle.ALL);
+    }
+
+    private void setUpSim(boolean withEmergencyIsoInSim) {
+        List<SubscriptionInfo> subInfos = getSubscriptionInfoList(withEmergencyIsoInSim);
+        doReturn(subInfos).when(mSubscriptionManager).getActiveSubscriptionInfoList();
+    }
+
+    private void setUpCell(boolean withEmergencyIsoInCell) {
+        doReturn(ACTIVE_MODEM_COUNT).when(mTelephonyManager).getActiveModemCount();
+        doReturn(NON_EMERGENCY_ISO_CODE).when(mTelephonyManager).getNetworkCountryIso(0);
+        doReturn(withEmergencyIsoInCell ? EMERGENCY_ISO_CODE : NON_EMERGENCY_ISO_CODE)
+                .when(mTelephonyManager).getNetworkCountryIso(1);
+    }
+
+    private void resetCell(boolean withEmergencyIsoInCell) {
+        doReturn(withEmergencyIsoInCell ? EMERGENCY_ISO_CODE : NON_EMERGENCY_ISO_CODE)
+                .when(mTelephonyManager).getNetworkCountryIso(1);
+    }
+
+    private void captureSubscriptionChangeListener() {
+        final ArgumentCaptor<OnSubscriptionsChangedListener> subChangedListenerCaptor =
+                ArgumentCaptor.forClass(OnSubscriptionsChangedListener.class);
+        verify(mSubscriptionManager).addOnSubscriptionsChangedListener(
+                subChangedListenerCaptor.capture());
+        mSubscriptionChangedListener = subChangedListenerCaptor.getValue();
+    }
+
+    private void setAirplaneMode(boolean enabled) {
+        // Change the system settings
+        Settings.Global.putInt(mContentResolver, Settings.Global.AIRPLANE_MODE_ON,
+                enabled ? 1 : 0);
+
+        // Post the intent
+        final Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        intent.putExtra("state", enabled);
+        mServiceContext.sendBroadcastAsUser(intent, UserHandle.ALL);
+    }
+
+    private List<SubscriptionInfo> getSubscriptionInfoList(boolean withEmergencyIso) {
+        List<SubscriptionInfo> subInfos = new ArrayList<>(2);
+
+        // Test with Multiple SIMs. SIM1 is a non-EA SIM
+        // Only country iso is valuable, all other info are filled with dummy values
+        SubscriptionInfo subInfo = new SubscriptionInfo(1, "890126042XXXXXXXXXXX", 0, "T-mobile",
+                "T-mobile", 0, 255, "12345", 0, null,
+                "310", "226", NON_EMERGENCY_ISO_CODE, false, null, null);
+        subInfos.add(subInfo);
+
+        // SIM2 can configured to be non-EA or EA SIM according parameter withEmergencyIso
+        SubscriptionInfo subInfo2 = new SubscriptionInfo(1, "890126042XXXXXXXXXXX", 0, "Airtel",
+                "Aritel", 0, 255, "12345", 0, null, "310", "226",
+                withEmergencyIso ? EMERGENCY_ISO_CODE : NON_EMERGENCY_ISO_CODE,
+                false, null, null);
+        subInfos.add(subInfo2);
+
+        return subInfos;
+    }
+
+    // EAS has handler thread to perform heavy work, while FakeSettingProvider does not support
+    // ContentObserver. To make sure consistent result, we use a simple sleep & retry to wait for
+    // real work finished before verify result.
+    private static final int TIME_DELAY_BEFORE_VERIFY_IN_MS = 50;
+    private static final int RETRIES_BEFORE_VERIFY = 20;
+    private void verifyEmergencyAffordanceNeededSettings(int expected) throws Exception {
+        try {
+            int ct = 0;
+            int actual = -1;
+            while (ct++ < RETRIES_BEFORE_VERIFY
+                    && (actual = Settings.Global.getInt(mContentResolver,
+                    Settings.Global.EMERGENCY_AFFORDANCE_NEEDED)) != expected) {
+                Thread.sleep(TIME_DELAY_BEFORE_VERIFY_IN_MS);
+            }
+            assertEquals(expected, actual);
+        } catch (Settings.SettingNotFoundException e) {
+            fail("SettingNotFoundException thrown for Settings.Global.EMERGENCY_AFFORDANCE_NEEDED");
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java
index 728e149..e16f314 100644
--- a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java
@@ -215,6 +215,8 @@
         mDataManager = new DataManager(mContext, mInjector);
         mDataManager.initialize();
 
+        when(mShortcutServiceInternal.isSharingShortcut(anyInt(), anyString(), anyString(),
+                anyString(), anyInt(), any())).thenReturn(true);
         verify(mShortcutServiceInternal).addShortcutChangeCallback(
                 mShortcutChangeCallbackCaptor.capture());
         mShortcutChangeCallback = mShortcutChangeCallbackCaptor.getValue();
diff --git a/services/tests/servicestests/src/com/android/server/power/AttentionDetectorTest.java b/services/tests/servicestests/src/com/android/server/power/AttentionDetectorTest.java
index c4289ef..e7e8aca 100644
--- a/services/tests/servicestests/src/com/android/server/power/AttentionDetectorTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/AttentionDetectorTest.java
@@ -158,17 +158,6 @@
     }
 
     @Test
-    public void testOnUserActivity_disablesSettingIfNotSufficientPermissions() {
-        when(mPackageManager.checkPermission(any(), any())).thenReturn(
-                PackageManager.PERMISSION_DENIED);
-
-        registerAttention();
-        boolean enabled = Settings.Secure.getIntForUser(getContext().getContentResolver(),
-                Settings.Secure.ADAPTIVE_SLEEP, 0, UserHandle.USER_CURRENT) == 1;
-        assertFalse(enabled);
-    }
-
-    @Test
     public void testOnUserActivity_doesntCrashIfNoAttentionService() {
         mAttentionManagerInternal = null;
         registerAttention();
diff --git a/services/tests/servicestests/src/com/android/server/tv/tunerresourcemanager/TunerResourceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/tv/tunerresourcemanager/TunerResourceManagerServiceTest.java
index 965304f..21af356 100644
--- a/services/tests/servicestests/src/com/android/server/tv/tunerresourcemanager/TunerResourceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/tv/tunerresourcemanager/TunerResourceManagerServiceTest.java
@@ -26,6 +26,7 @@
 import android.media.tv.TvInputManager;
 import android.media.tv.TvInputService;
 import android.media.tv.tuner.frontend.FrontendSettings;
+import android.media.tv.tunerresourcemanager.CasSessionRequest;
 import android.media.tv.tunerresourcemanager.IResourcesReclaimListener;
 import android.media.tv.tunerresourcemanager.ResourceClientProfile;
 import android.media.tv.tunerresourcemanager.TunerDemuxRequest;
@@ -34,7 +35,6 @@
 import android.media.tv.tunerresourcemanager.TunerFrontendRequest;
 import android.media.tv.tunerresourcemanager.TunerLnbRequest;
 import android.media.tv.tunerresourcemanager.TunerResourceManager;
-import android.os.RemoteException;
 import android.platform.test.annotations.Presubmit;
 
 import androidx.test.InstrumentationRegistry;
@@ -236,12 +236,8 @@
         TunerFrontendRequest request =
                 new TunerFrontendRequest(0 /*clientId*/, FrontendSettings.TYPE_DVBT);
         int[] frontendHandle = new int[1];
-        try {
-            assertThat(mTunerResourceManagerService
-                    .requestFrontendInternal(request, frontendHandle)).isFalse();
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        assertThat(mTunerResourceManagerService
+                .requestFrontendInternal(request, frontendHandle)).isFalse();
         assertThat(mTunerResourceManagerService.getResourceIdFromHandle(frontendHandle[0]))
                 .isEqualTo(TunerResourceManager.INVALID_RESOURCE_HANDLE);
     }
@@ -264,12 +260,8 @@
         TunerFrontendRequest request =
                 new TunerFrontendRequest(clientId[0] /*clientId*/, FrontendSettings.TYPE_DVBT);
         int[] frontendHandle = new int[1];
-        try {
-            assertThat(mTunerResourceManagerService
-                    .requestFrontendInternal(request, frontendHandle)).isFalse();
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        assertThat(mTunerResourceManagerService
+                .requestFrontendInternal(request, frontendHandle)).isFalse();
         assertThat(mTunerResourceManagerService.getResourceIdFromHandle(frontendHandle[0]))
                 .isEqualTo(TunerResourceManager.INVALID_RESOURCE_HANDLE);
     }
@@ -296,12 +288,8 @@
         TunerFrontendRequest request =
                 new TunerFrontendRequest(clientId[0] /*clientId*/, FrontendSettings.TYPE_DVBT);
         int[] frontendHandle = new int[1];
-        try {
-            assertThat(mTunerResourceManagerService
-                    .requestFrontendInternal(request, frontendHandle)).isTrue();
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        assertThat(mTunerResourceManagerService
+                .requestFrontendInternal(request, frontendHandle)).isTrue();
         assertThat(mTunerResourceManagerService.getResourceIdFromHandle(frontendHandle[0]))
                 .isEqualTo(0);
     }
@@ -334,23 +322,15 @@
         int[] frontendHandle = new int[1];
         TunerFrontendRequest request =
                 new TunerFrontendRequest(clientId1[0] /*clientId*/, FrontendSettings.TYPE_DVBT);
-        try {
-            assertThat(mTunerResourceManagerService
-                    .requestFrontendInternal(request, frontendHandle)).isTrue();
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        assertThat(mTunerResourceManagerService
+                .requestFrontendInternal(request, frontendHandle)).isTrue();
         assertThat(mTunerResourceManagerService.getResourceIdFromHandle(frontendHandle[0]))
                 .isEqualTo(infos[0].getId());
 
         request =
                 new TunerFrontendRequest(clientId0[0] /*clientId*/, FrontendSettings.TYPE_DVBT);
-        try {
-            assertThat(mTunerResourceManagerService
-                    .requestFrontendInternal(request, frontendHandle)).isTrue();
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        assertThat(mTunerResourceManagerService
+                .requestFrontendInternal(request, frontendHandle)).isTrue();
         assertThat(mTunerResourceManagerService.getResourceIdFromHandle(frontendHandle[0]))
                 .isEqualTo(infos[1].getId());
         assertThat(mTunerResourceManagerService.getFrontendResource(infos[1].getId())
@@ -394,32 +374,20 @@
         TunerFrontendRequest request =
                 new TunerFrontendRequest(clientId0[0] /*clientId*/, FrontendSettings.TYPE_DVBT);
         int[] frontendHandle = new int[1];
-        try {
-            assertThat(mTunerResourceManagerService
-                    .requestFrontendInternal(request, frontendHandle)).isTrue();
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        assertThat(mTunerResourceManagerService
+                .requestFrontendInternal(request, frontendHandle)).isTrue();
 
         request =
                 new TunerFrontendRequest(clientId1[0] /*clientId*/, FrontendSettings.TYPE_DVBT);
-        try {
-            assertThat(mTunerResourceManagerService
-                    .requestFrontendInternal(request, frontendHandle)).isFalse();
-            assertThat(listener.isRelaimed()).isFalse();
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        assertThat(mTunerResourceManagerService
+                .requestFrontendInternal(request, frontendHandle)).isFalse();
+        assertThat(listener.isRelaimed()).isFalse();
 
         request =
                 new TunerFrontendRequest(clientId1[0] /*clientId*/, FrontendSettings.TYPE_DVBS);
-        try {
-            assertThat(mTunerResourceManagerService
-                    .requestFrontendInternal(request, frontendHandle)).isFalse();
-            assertThat(listener.isRelaimed()).isFalse();
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        assertThat(mTunerResourceManagerService
+                .requestFrontendInternal(request, frontendHandle)).isFalse();
+        assertThat(listener.isRelaimed()).isFalse();
     }
 
     @Test
@@ -456,12 +424,8 @@
         TunerFrontendRequest request =
                 new TunerFrontendRequest(clientId0[0] /*clientId*/, FrontendSettings.TYPE_DVBT);
         int[] frontendHandle = new int[1];
-        try {
-            assertThat(mTunerResourceManagerService
-                    .requestFrontendInternal(request, frontendHandle)).isTrue();
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        assertThat(mTunerResourceManagerService
+                .requestFrontendInternal(request, frontendHandle)).isTrue();
         assertThat(mTunerResourceManagerService.getResourceIdFromHandle(frontendHandle[0]))
                 .isEqualTo(infos[0].getId());
         assertThat(mTunerResourceManagerService.getClientProfile(clientId0[0])
@@ -470,12 +434,8 @@
 
         request =
                 new TunerFrontendRequest(clientId1[0] /*clientId*/, FrontendSettings.TYPE_DVBS);
-        try {
-            assertThat(mTunerResourceManagerService
-                    .requestFrontendInternal(request, frontendHandle)).isTrue();
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        assertThat(mTunerResourceManagerService
+                .requestFrontendInternal(request, frontendHandle)).isTrue();
         assertThat(mTunerResourceManagerService.getResourceIdFromHandle(frontendHandle[0]))
                 .isEqualTo(infos[1].getId());
         assertThat(mTunerResourceManagerService.getFrontendResource(infos[0].getId())
@@ -511,12 +471,8 @@
         TunerFrontendRequest request =
                 new TunerFrontendRequest(clientId[0] /*clientId*/, FrontendSettings.TYPE_DVBT);
         int[] frontendHandle = new int[1];
-        try {
-            assertThat(mTunerResourceManagerService
-                    .requestFrontendInternal(request, frontendHandle)).isTrue();
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        assertThat(mTunerResourceManagerService
+                .requestFrontendInternal(request, frontendHandle)).isTrue();
         int frontendId = mTunerResourceManagerService.getResourceIdFromHandle(frontendHandle[0]);
         assertThat(frontendId).isEqualTo(infos[0].getId());
         assertThat(mTunerResourceManagerService
@@ -534,6 +490,99 @@
     }
 
     @Test
+    public void requestCasTest_NoCasAvailable_RequestWithHigherPriority() {
+        // Register clients
+        ResourceClientProfile[] profiles = new ResourceClientProfile[2];
+        profiles[0] = new ResourceClientProfile("0" /*sessionId*/,
+                TvInputService.PRIORITY_HINT_USE_CASE_TYPE_PLAYBACK);
+        profiles[1] = new ResourceClientProfile("1" /*sessionId*/,
+                TvInputService.PRIORITY_HINT_USE_CASE_TYPE_PLAYBACK);
+        int[] clientPriorities = {100, 500};
+        int[] clientId0 = new int[1];
+        int[] clientId1 = new int[1];
+        TestResourcesReclaimListener listener = new TestResourcesReclaimListener();
+        mTunerResourceManagerService.registerClientProfileInternal(
+                profiles[0], listener, clientId0);
+        assertThat(clientId0[0]).isNotEqualTo(TunerResourceManagerService.INVALID_CLIENT_ID);
+        mTunerResourceManagerService.getClientProfile(clientId0[0])
+                .setPriority(clientPriorities[0]);
+        mTunerResourceManagerService.registerClientProfileInternal(
+                profiles[1], new TestResourcesReclaimListener(), clientId1);
+        assertThat(clientId1[0]).isNotEqualTo(TunerResourceManagerService.INVALID_CLIENT_ID);
+        mTunerResourceManagerService.getClientProfile(clientId1[0])
+                .setPriority(clientPriorities[1]);
+
+        // Init cas resources.
+        mTunerResourceManagerService.updateCasInfoInternal(1 /*casSystemId*/, 2 /*maxSessionNum*/);
+
+        CasSessionRequest request = new CasSessionRequest(clientId0[0], 1 /*casSystemId*/);
+        int[] casSessionHandle = new int[1];
+        // Request for 2 cas sessions.
+        assertThat(mTunerResourceManagerService
+                .requestCasSessionInternal(request, casSessionHandle)).isTrue();
+        assertThat(mTunerResourceManagerService
+                .requestCasSessionInternal(request, casSessionHandle)).isTrue();
+        assertThat(mTunerResourceManagerService.getResourceIdFromHandle(casSessionHandle[0]))
+                .isEqualTo(1);
+        assertThat(mTunerResourceManagerService.getClientProfile(clientId0[0])
+                .getInUseCasSystemId()).isEqualTo(1);
+        assertThat(mTunerResourceManagerService.getCasResource(1)
+                .getOwnerClientIds()).isEqualTo(new HashSet<Integer>(Arrays.asList(clientId0[0])));
+        assertThat(mTunerResourceManagerService.getCasResource(1).isFullyUsed()).isTrue();
+
+        request = new CasSessionRequest(clientId1[0], 1);
+        assertThat(mTunerResourceManagerService
+                .requestCasSessionInternal(request, casSessionHandle)).isTrue();
+        assertThat(mTunerResourceManagerService.getResourceIdFromHandle(casSessionHandle[0]))
+                .isEqualTo(1);
+        assertThat(mTunerResourceManagerService.getClientProfile(clientId1[0])
+                .getInUseCasSystemId()).isEqualTo(1);
+        assertThat(mTunerResourceManagerService.getClientProfile(clientId0[0])
+                .getInUseCasSystemId()).isEqualTo(ClientProfile.INVALID_RESOURCE_ID);
+        assertThat(mTunerResourceManagerService.getCasResource(1)
+                .getOwnerClientIds()).isEqualTo(new HashSet<Integer>(Arrays.asList(clientId1[0])));
+        assertThat(mTunerResourceManagerService.getCasResource(1).isFullyUsed()).isFalse();
+        assertThat(listener.isRelaimed()).isTrue();
+    }
+
+    @Test
+    public void releaseCasTest() {
+        // Register clients
+        ResourceClientProfile[] profiles = new ResourceClientProfile[1];
+        profiles[0] = new ResourceClientProfile("0" /*sessionId*/,
+                TvInputService.PRIORITY_HINT_USE_CASE_TYPE_PLAYBACK);
+        int[] clientId = new int[1];
+        TestResourcesReclaimListener listener = new TestResourcesReclaimListener();
+        mTunerResourceManagerService.registerClientProfileInternal(profiles[0], listener, clientId);
+        assertThat(clientId[0]).isNotEqualTo(TunerResourceManagerService.INVALID_CLIENT_ID);
+
+        // Init cas resources.
+        mTunerResourceManagerService.updateCasInfoInternal(1 /*casSystemId*/, 2 /*maxSessionNum*/);
+
+        CasSessionRequest request = new CasSessionRequest(clientId[0], 1 /*casSystemId*/);
+        int[] casSessionHandle = new int[1];
+        // Request for 1 cas sessions.
+        assertThat(mTunerResourceManagerService
+                .requestCasSessionInternal(request, casSessionHandle)).isTrue();
+        assertThat(mTunerResourceManagerService.getResourceIdFromHandle(casSessionHandle[0]))
+                .isEqualTo(1);
+        assertThat(mTunerResourceManagerService.getClientProfile(clientId[0])
+                .getInUseCasSystemId()).isEqualTo(1);
+        assertThat(mTunerResourceManagerService.getCasResource(1)
+                .getOwnerClientIds()).isEqualTo(new HashSet<Integer>(Arrays.asList(clientId[0])));
+        assertThat(mTunerResourceManagerService.getCasResource(1).isFullyUsed()).isFalse();
+
+        // Release cas
+        mTunerResourceManagerService.releaseCasSessionInternal(mTunerResourceManagerService
+                .getCasResource(1), clientId[0]);
+        assertThat(mTunerResourceManagerService.getClientProfile(clientId[0])
+                .getInUseCasSystemId()).isEqualTo(ClientProfile.INVALID_RESOURCE_ID);
+        assertThat(mTunerResourceManagerService.getCasResource(1).isFullyUsed()).isFalse();
+        assertThat(mTunerResourceManagerService.getCasResource(1)
+                .getOwnerClientIds()).isEmpty();
+    }
+
+    @Test
     public void requestLnbTest_NoLnbAvailable_RequestWithHigherPriority() {
         // Register clients
         ResourceClientProfile[] profiles = new ResourceClientProfile[2];
@@ -562,24 +611,16 @@
 
         TunerLnbRequest request = new TunerLnbRequest(clientId0[0]);
         int[] lnbHandle = new int[1];
-        try {
-            assertThat(mTunerResourceManagerService
-                    .requestLnbInternal(request, lnbHandle)).isTrue();
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        assertThat(mTunerResourceManagerService
+                .requestLnbInternal(request, lnbHandle)).isTrue();
         assertThat(mTunerResourceManagerService.getResourceIdFromHandle(lnbHandle[0]))
                 .isEqualTo(lnbIds[0]);
         assertThat(mTunerResourceManagerService.getClientProfile(clientId0[0])
                 .getInUseLnbIds()).isEqualTo(new HashSet<Integer>(Arrays.asList(lnbIds[0])));
 
         request = new TunerLnbRequest(clientId1[0]);
-        try {
-            assertThat(mTunerResourceManagerService
-                    .requestLnbInternal(request, lnbHandle)).isTrue();
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        assertThat(mTunerResourceManagerService
+                .requestLnbInternal(request, lnbHandle)).isTrue();
         assertThat(mTunerResourceManagerService.getResourceIdFromHandle(lnbHandle[0]))
                 .isEqualTo(lnbIds[0]);
         assertThat(mTunerResourceManagerService.getLnbResource(lnbIds[0])
@@ -608,12 +649,8 @@
 
         TunerLnbRequest request = new TunerLnbRequest(clientId[0]);
         int[] lnbHandle = new int[1];
-        try {
-            assertThat(mTunerResourceManagerService
-                    .requestLnbInternal(request, lnbHandle)).isTrue();
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        assertThat(mTunerResourceManagerService
+                .requestLnbInternal(request, lnbHandle)).isTrue();
         int lnbId = mTunerResourceManagerService.getResourceIdFromHandle(lnbHandle[0]);
         assertThat(lnbId).isEqualTo(lnbIds[0]);
 
@@ -647,12 +684,8 @@
         TunerFrontendRequest request =
                 new TunerFrontendRequest(clientId[0] /*clientId*/, FrontendSettings.TYPE_DVBT);
         int[] frontendHandle = new int[1];
-        try {
-            assertThat(mTunerResourceManagerService
-                    .requestFrontendInternal(request, frontendHandle)).isTrue();
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        assertThat(mTunerResourceManagerService
+                .requestFrontendInternal(request, frontendHandle)).isTrue();
         assertThat(mTunerResourceManagerService.getResourceIdFromHandle(frontendHandle[0]))
                 .isEqualTo(infos[0].getId());
         assertThat(mTunerResourceManagerService.getFrontendResource(infos[0].getId())
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index ecdd9e5..7a5e226 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -6074,6 +6074,9 @@
         // Pretend the shortcut exists
         List<ShortcutInfo> shortcutInfos = new ArrayList<>();
         ShortcutInfo info = mock(ShortcutInfo.class);
+        when(info.getPackage()).thenReturn(PKG);
+        when(info.getId()).thenReturn("someshortcutId");
+        when(info.getUserId()).thenReturn(USER_SYSTEM);
         when(info.isLongLived()).thenReturn(true);
         when(info.isEnabled()).thenReturn(true);
         shortcutInfos.add(info);
@@ -6137,6 +6140,9 @@
         // Pretend the shortcut exists
         List<ShortcutInfo> shortcutInfos = new ArrayList<>();
         ShortcutInfo info = mock(ShortcutInfo.class);
+        when(info.getPackage()).thenReturn(PKG);
+        when(info.getId()).thenReturn("someshortcutId");
+        when(info.getUserId()).thenReturn(USER_SYSTEM);
         when(info.isLongLived()).thenReturn(true);
         when(info.isEnabled()).thenReturn(true);
         shortcutInfos.add(info);
@@ -6483,6 +6489,9 @@
         when(mPreferencesHelper.getConversations(anyString(), anyInt())).thenReturn(convos);
 
         ShortcutInfo si = mock(ShortcutInfo.class);
+        when(si.getPackage()).thenReturn(PKG_P);
+        when(si.getId()).thenReturn("convo");
+        when(si.getUserId()).thenReturn(USER_SYSTEM);
         when(si.getShortLabel()).thenReturn("Hello");
         when(si.isLongLived()).thenReturn(true);
         when(si.isEnabled()).thenReturn(true);
@@ -6514,6 +6523,9 @@
         when(mPreferencesHelper.getConversations(anyString(), anyInt())).thenReturn(convos);
 
         ShortcutInfo si = mock(ShortcutInfo.class);
+        when(si.getPackage()).thenReturn(PKG_P);
+        when(si.getId()).thenReturn("convo");
+        when(si.getUserId()).thenReturn(USER_SYSTEM);
         when(si.getShortLabel()).thenReturn("Hello");
         when(si.isLongLived()).thenReturn(false);
         when(mLauncherApps.getShortcuts(any(), any())).thenReturn(Arrays.asList(si));
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ShortcutHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ShortcutHelperTest.java
index f7304bd..3095c87 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ShortcutHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ShortcutHelperTest.java
@@ -170,6 +170,9 @@
     @Test
     public void testGetValidShortcutInfo_notLongLived() {
         ShortcutInfo si = mock(ShortcutInfo.class);
+        when(si.getPackage()).thenReturn(PKG);
+        when(si.getId()).thenReturn(SHORTCUT_ID);
+        when(si.getUserId()).thenReturn(UserHandle.USER_SYSTEM);
         when(si.isLongLived()).thenReturn(false);
         when(si.isEnabled()).thenReturn(true);
         ArrayList<ShortcutInfo> shortcuts = new ArrayList<>();
@@ -184,6 +187,9 @@
     @Test
     public void testGetValidShortcutInfo_notSharingShortcut() {
         ShortcutInfo si = mock(ShortcutInfo.class);
+        when(si.getPackage()).thenReturn(PKG);
+        when(si.getId()).thenReturn(SHORTCUT_ID);
+        when(si.getUserId()).thenReturn(UserHandle.USER_SYSTEM);
         when(si.isLongLived()).thenReturn(true);
         when(si.isEnabled()).thenReturn(true);
         ArrayList<ShortcutInfo> shortcuts = new ArrayList<>();
@@ -198,6 +204,9 @@
     @Test
     public void testGetValidShortcutInfo_notEnabled() {
         ShortcutInfo si = mock(ShortcutInfo.class);
+        when(si.getPackage()).thenReturn(PKG);
+        when(si.getId()).thenReturn(SHORTCUT_ID);
+        when(si.getUserId()).thenReturn(UserHandle.USER_SYSTEM);
         when(si.isLongLived()).thenReturn(true);
         when(si.isEnabled()).thenReturn(false);
         ArrayList<ShortcutInfo> shortcuts = new ArrayList<>();
@@ -212,6 +221,9 @@
     @Test
     public void testGetValidShortcutInfo_isValid() {
         ShortcutInfo si = mock(ShortcutInfo.class);
+        when(si.getPackage()).thenReturn(PKG);
+        when(si.getId()).thenReturn(SHORTCUT_ID);
+        when(si.getUserId()).thenReturn(UserHandle.USER_SYSTEM);
         when(si.isLongLived()).thenReturn(true);
         when(si.isEnabled()).thenReturn(true);
         ArrayList<ShortcutInfo> shortcuts = new ArrayList<>();
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java
index 6ae8313..0700f9f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java
@@ -329,6 +329,7 @@
         private boolean mCreateStack = true;
 
         private ActivityStack mStack;
+        private TaskDisplayArea mTaskDisplayArea;
 
         TaskBuilder(ActivityStackSupervisor supervisor) {
             mSupervisor = supervisor;
@@ -378,9 +379,16 @@
             return this;
         }
 
+        TaskBuilder setDisplay(DisplayContent display) {
+            mTaskDisplayArea = display.getDefaultTaskDisplayArea();
+            return this;
+        }
+
         Task build() {
             if (mStack == null && mCreateStack) {
-                mStack = mSupervisor.mRootWindowContainer.getDefaultTaskDisplayArea().createStack(
+                TaskDisplayArea displayArea = mTaskDisplayArea != null ? mTaskDisplayArea
+                        : mSupervisor.mRootWindowContainer.getDefaultTaskDisplayArea();
+                mStack = displayArea.createStack(
                         WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
                 spyOn(mStack);
             }
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java
index 9b7ffd6..e8fab2b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java
@@ -37,7 +37,6 @@
 import android.view.RemoteAnimationDefinition;
 import android.view.RemoteAnimationTarget;
 
-import androidx.test.filters.FlakyTest;
 import androidx.test.filters.SmallTest;
 
 import org.junit.Test;
@@ -97,7 +96,6 @@
     }
 
     @Test
-    @FlakyTest(bugId = 131005232)
     public void testModeChangeRemoteAnimatorNoSnapshot() {
         // setup currently defaults to no snapshot.
         setUpOnDisplay(mDisplayContent);
@@ -115,7 +113,6 @@
     }
 
     @Test
-    @FlakyTest(bugId = 131005232)
     public void testCancelPendingChangeOnRemove() {
         // setup currently defaults to no snapshot.
         setUpOnDisplay(mDisplayContent);
@@ -135,8 +132,7 @@
     }
 
     @Test
-    @FlakyTest(bugId = 131005232)
-    public void testNoChangeWhenMoveDisplay() {
+    public void testNoChangeOnOldDisplayWhenMoveDisplay() {
         mDisplayContent.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
         final DisplayContent dc1 = createNewDisplay(Display.STATE_ON);
         dc1.setWindowingMode(WINDOWING_MODE_FREEFORM);
@@ -151,9 +147,8 @@
 
         assertEquals(WINDOWING_MODE_FULLSCREEN, mTask.getWindowingMode());
 
-        // Make sure we're not waiting for a change animation (no leash)
-        assertFalse(mTask.isInChangeTransition());
-        assertNull(mActivity.mSurfaceFreezer.mSnapshot);
+        // Make sure the change transition is not the old display
+        assertFalse(dc1.mChangingContainers.contains(mTask));
 
         waitUntilHandlersIdle();
         mActivity.removeImmediately();
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index daff149..80fcf2e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -41,6 +41,7 @@
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
 import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE;
+import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
 import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
 import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION;
 import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
@@ -74,6 +75,7 @@
 import static org.mockito.ArgumentMatchers.eq;
 
 import android.annotation.SuppressLint;
+import android.app.ActivityTaskManager;
 import android.app.WindowConfiguration;
 import android.content.res.Configuration;
 import android.graphics.Rect;
@@ -1207,6 +1209,31 @@
         assertNull(taskDisplayArea.getOrCreateRootHomeTask());
     }
 
+    @Test
+    public void testFindScrollCaptureTargetWindow_behindWindow() {
+        DisplayContent display = createNewDisplay();
+        ActivityStack stack = createTaskStackOnDisplay(display);
+        Task task = createTaskInStack(stack, 0 /* userId */);
+        WindowState activityWindow = createAppWindow(task, TYPE_APPLICATION, "App Window");
+        WindowState behindWindow = createWindow(null, TYPE_SCREENSHOT, display, "Screenshot");
+
+        WindowState result = display.findScrollCaptureTargetWindow(behindWindow,
+                ActivityTaskManager.INVALID_TASK_ID);
+        assertEquals(activityWindow, result);
+    }
+
+    @Test
+    public void testFindScrollCaptureTargetWindow_taskId() {
+        DisplayContent display = createNewDisplay();
+        ActivityStack stack = createTaskStackOnDisplay(display);
+        Task task = createTaskInStack(stack, 0 /* userId */);
+        WindowState window = createAppWindow(task, TYPE_APPLICATION, "App Window");
+        WindowState behindWindow = createWindow(null, TYPE_SCREENSHOT, display, "Screenshot");
+
+        WindowState result = display.findScrollCaptureTargetWindow(null, task.mTaskId);
+        assertEquals(window, result);
+    }
+
     private boolean isOptionsPanelAtRight(int displayId) {
         return (mWm.getPreferredOptionsPanelGravity(displayId) & Gravity.RIGHT) == Gravity.RIGHT;
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
index be25597..e887be0 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
@@ -33,6 +33,7 @@
 
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 
 /**
  * Build/Install/Run:
@@ -40,6 +41,7 @@
  */
 @SmallTest
 @Presubmit
+@RunWith(WindowTestRunner.class)
 @FlakyTest
 public class RefreshRatePolicyTest extends WindowTestsBase {
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskRecordTests.java
index 519ac78..dcc2ff1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskRecordTests.java
@@ -54,10 +54,7 @@
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.same;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
 
 import android.app.ActivityManager;
 import android.app.TaskInfo;
@@ -75,12 +72,10 @@
 import android.util.Xml;
 import android.view.DisplayInfo;
 
-import androidx.test.filters.FlakyTest;
 import androidx.test.filters.MediumTest;
 
 import com.android.internal.app.IVoiceInteractor;
 import com.android.server.wm.Task.TaskFactory;
-import com.android.server.wm.utils.WmDisplayCutout;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -368,25 +363,38 @@
 
     @Test
     public void testComputeConfigResourceOverrides() {
-        final Task task = new TaskBuilder(mSupervisor).build();
+        final Rect fullScreenBounds = new Rect(0, 0, 1080, 1920);
+        TestDisplayContent display = new TestDisplayContent.Builder(
+                mService, fullScreenBounds.width(), fullScreenBounds.height()).build();
+        final Task task = new TaskBuilder(mSupervisor).setDisplay(display).build();
         final Configuration inOutConfig = new Configuration();
         final Configuration parentConfig = new Configuration();
         final int longSide = 1200;
         final int shortSide = 600;
+        final Rect parentBounds = new Rect(0, 0, 250, 500);
+        parentConfig.windowConfiguration.setBounds(parentBounds);
         parentConfig.densityDpi = 400;
-        parentConfig.screenHeightDp = 200; // 200 * 400 / 160 = 500px
-        parentConfig.screenWidthDp = 100; // 100 * 400 / 160 = 250px
+        parentConfig.screenHeightDp = (parentBounds.bottom * 160) / parentConfig.densityDpi; // 200
+        parentConfig.screenWidthDp = (parentBounds.right * 160) / parentConfig.densityDpi; // 100
         parentConfig.windowConfiguration.setRotation(ROTATION_0);
 
-        // Portrait bounds.
-        inOutConfig.windowConfiguration.getBounds().set(0, 0, shortSide, longSide);
-        // By default, the parent bounds should limit the existing input bounds.
+        // By default, the input bounds will fill parent.
         task.computeConfigResourceOverrides(inOutConfig, parentConfig);
 
         assertEquals(parentConfig.screenHeightDp, inOutConfig.screenHeightDp);
         assertEquals(parentConfig.screenWidthDp, inOutConfig.screenWidthDp);
         assertEquals(Configuration.ORIENTATION_PORTRAIT, inOutConfig.orientation);
 
+        // If bounds are overridden, config properties should be made to match. Surface hierarchy
+        // will crop for policy.
+        inOutConfig.setToDefaults();
+        inOutConfig.windowConfiguration.getBounds().set(0, 0, shortSide, longSide);
+        // By default, the parent bounds should limit the existing input bounds.
+        task.computeConfigResourceOverrides(inOutConfig, parentConfig);
+
+        assertEquals(longSide, inOutConfig.screenHeightDp * parentConfig.densityDpi / 160);
+        assertEquals(shortSide, inOutConfig.screenWidthDp * parentConfig.densityDpi / 160);
+
         inOutConfig.setToDefaults();
         // Landscape bounds.
         inOutConfig.windowConfiguration.getBounds().set(0, 0, longSide, shortSide);
@@ -394,21 +402,17 @@
         // Setup the display with a top stable inset. The later assertion will ensure the inset is
         // excluded from screenHeightDp.
         final int statusBarHeight = 100;
-        final DisplayContent displayContent = task.mDisplayContent;
-        final DisplayPolicy policy = mock(DisplayPolicy.class);
+        final DisplayPolicy policy = display.getDisplayPolicy();
         doAnswer(invocationOnMock -> {
             final Rect insets = invocationOnMock.<Rect>getArgument(0);
             insets.top = statusBarHeight;
             return null;
         }).when(policy).convertNonDecorInsetsToStableInsets(any(), eq(ROTATION_0));
-        doReturn(policy).when(displayContent).getDisplayPolicy();
-        doReturn(mock(WmDisplayCutout.class)).when(displayContent)
-                .calculateDisplayCutoutForRotation(anyInt());
 
         // Without limiting to be inside the parent bounds, the out screen size should keep relative
         // to the input bounds.
         final ActivityRecord.CompatDisplayInsets compatIntsets =
-                new ActivityRecord.CompatDisplayInsets(displayContent, task);
+                new ActivityRecord.CompatDisplayInsets(display, task);
         task.computeConfigResourceOverrides(inOutConfig, parentConfig, compatIntsets);
 
         assertEquals((shortSide - statusBarHeight) * DENSITY_DEFAULT / parentConfig.densityDpi,
@@ -454,7 +458,6 @@
         parentConfig.screenWidthDp = 100; // 100 * 400 / 160 = 250px
         parentConfig.windowConfiguration.setRotation(ROTATION_0);
 
-        final float density = 2.5f; // densityDpi / DENSITY_DEFAULT_SCALE = 400 / 160.0f
         final int longSideDp = 480; // longSide / density = 1200 / 400 * 160
         final int shortSideDp = 240; // shortSide / density = 600 / 400 * 160
         final int screenLayout = parentConfig.screenLayout
@@ -463,31 +466,38 @@
                 Configuration.reduceScreenLayout(screenLayout, longSideDp, shortSideDp);
 
         // Portrait bounds overlapping with navigation bar, without insets.
-        inOutConfig.windowConfiguration.getBounds().set(0,
+        final Rect freeformBounds = new Rect(0,
                 displayHeight - 10 - longSide,
                 shortSide,
                 displayHeight - 10);
+        inOutConfig.windowConfiguration.setBounds(freeformBounds);
         // Set to freeform mode to verify bug fix.
         inOutConfig.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
 
         task.computeConfigResourceOverrides(inOutConfig, parentConfig);
 
-        assertEquals(parentConfig.screenWidthDp, inOutConfig.screenWidthDp);
-        assertEquals(parentConfig.screenHeightDp, inOutConfig.screenHeightDp);
+        // screenW/H should not be effected by parent since overridden and freeform
+        assertEquals(freeformBounds.width() * 160 / parentConfig.densityDpi,
+                inOutConfig.screenWidthDp);
+        assertEquals(freeformBounds.height() * 160 / parentConfig.densityDpi,
+                inOutConfig.screenHeightDp);
         assertEquals(reducedScreenLayout, inOutConfig.screenLayout);
 
         inOutConfig.setToDefaults();
         // Landscape bounds overlapping with navigtion bar, without insets.
-        inOutConfig.windowConfiguration.getBounds().set(0,
+        freeformBounds.set(0,
                 displayHeight - 10 - shortSide,
                 longSide,
                 displayHeight - 10);
+        inOutConfig.windowConfiguration.setBounds(freeformBounds);
         inOutConfig.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
 
         task.computeConfigResourceOverrides(inOutConfig, parentConfig);
 
-        assertEquals(parentConfig.screenWidthDp, inOutConfig.screenWidthDp);
-        assertEquals(parentConfig.screenHeightDp, inOutConfig.screenHeightDp);
+        assertEquals(freeformBounds.width() * 160 / parentConfig.densityDpi,
+                inOutConfig.screenWidthDp);
+        assertEquals(freeformBounds.height() * 160 / parentConfig.densityDpi,
+                inOutConfig.screenHeightDp);
         assertEquals(reducedScreenLayout, inOutConfig.screenLayout);
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskStackTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskStackTests.java
index 7cb5e84..f354a04 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskStackTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskStackTests.java
@@ -186,7 +186,7 @@
         final ActivityStack stack = createTaskStackOnDisplay(mDisplayContent);
         final int stackOutset = 10;
         spyOn(stack);
-        doReturn(stackOutset).when(stack).getStackOutset();
+        doReturn(stackOutset).when(stack).getTaskOutset();
         doReturn(true).when(stack).inMultiWindowMode();
 
         // Mock the resolved override windowing mode to non-fullscreen
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
index 91c3c27..e39b4bc 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
@@ -24,6 +24,7 @@
 import android.util.MergedConfiguration;
 import android.view.DisplayCutout;
 import android.view.DragEvent;
+import android.view.IScrollCaptureController;
 import android.view.IWindow;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
@@ -113,6 +114,10 @@
     }
 
     @Override
+    public void requestScrollCapture(IScrollCaptureController controller) throws RemoteException {
+    }
+
+    @Override
     public void showInsets(int types, boolean fromIme) throws RemoteException {
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
index a4f1487..520ac19 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
@@ -16,6 +16,9 @@
 
 package com.android.server.wm;
 
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
 import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
@@ -251,6 +254,22 @@
         assertEquals(otherWindowInitialZoom, wallpaperWindow.mWallpaperZoomOut, .01f);
     }
 
+    /**
+     * Tests that the windowing mode of the wallpaper window must always be fullscreen.
+     */
+    @Test
+    public void testWallpaperTokenWindowingMode() {
+        final DisplayContent dc = mWm.mRoot.getDefaultDisplay();
+        final WallpaperWindowToken token = new WallpaperWindowToken(mWm, mock(IBinder.class),
+                true, dc, true /* ownerCanManageAppTokens */);
+
+        // The wallpaper should have requested override fullscreen windowing mode, so the
+        // configuration (windowing mode) propagation from display won't change it.
+        dc.setWindowingMode(WINDOWING_MODE_FREEFORM);
+        assertEquals(WINDOWING_MODE_FULLSCREEN, token.getWindowingMode());
+        dc.setWindowingMode(WINDOWING_MODE_UNDEFINED);
+        assertEquals(WINDOWING_MODE_FULLSCREEN, token.getWindowingMode());
+    }
 
     private WindowState createWallpaperTargetWindow(DisplayContent dc) {
         final ActivityRecord homeActivity = new ActivityTestsBase.ActivityBuilder(mWm.mAtmService)
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
index 53cc09b..7d2e880 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
@@ -44,7 +44,6 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
@@ -82,7 +81,7 @@
  * Test class for {@link ITaskOrganizer} and {@link android.window.ITaskOrganizerController}.
  *
  * Build/Install/Run:
- *  atest WmTests:TaskOrganizerTests
+ *  atest WmTests:WindowOrganizerTests
  */
 @SmallTest
 @Presubmit
@@ -265,15 +264,22 @@
         // newly entering the windowing mode.
         final ITaskOrganizer organizer2 = registerMockOrganizer(WINDOWING_MODE_MULTI_WINDOW);
         stack2.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
-        verify(organizer2).onTaskAppeared(any());
+        // One each for task and task2
+        verify(organizer2, times(2)).onTaskAppeared(any());
+        verify(organizer2, times(0)).onTaskVanished(any());
+        // One for task
+        verify(organizer).onTaskVanished(any());
         assertTrue(stack2.isOrganized());
 
         // Now we unregister the second one, the first one should automatically be reregistered
         // so we verify that it's now seeing changes.
         mWm.mAtmService.mTaskOrganizerController.unregisterTaskOrganizer(organizer2);
+        verify(organizer, times(3)).onTaskAppeared(any());
+        verify(organizer2, times(2)).onTaskVanished(any());
 
         stack3.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
-        verify(organizer, times(2)).onTaskAppeared(any());
+        verify(organizer, times(4)).onTaskAppeared(any());
+        verify(organizer2, times(2)).onTaskVanished(any());
         assertTrue(stack3.isOrganized());
     }
 
@@ -407,22 +413,20 @@
                 .setWindowingMode(WINDOWING_MODE_FREEFORM).build();
         final Task task = stack.getTopMostTask();
         WindowContainerTransaction t = new WindowContainerTransaction();
-        t.setBounds(task.mRemoteToken.toWindowContainerToken(), new Rect(10, 10, 100, 100));
         mWm.mAtmService.mWindowOrganizerController.applyTransaction(t);
         final int origScreenWDp = task.getConfiguration().screenHeightDp;
         final int origScreenHDp = task.getConfiguration().screenHeightDp;
         t = new WindowContainerTransaction();
         // verify that setting config overrides on parent restricts children.
         t.setScreenSizeDp(stack.mRemoteToken
-                .toWindowContainerToken(), origScreenWDp, origScreenHDp);
-        t.setBounds(task.mRemoteToken.toWindowContainerToken(), new Rect(10, 10, 150, 200));
+                .toWindowContainerToken(), origScreenWDp, origScreenHDp / 2);
         mWm.mAtmService.mWindowOrganizerController.applyTransaction(t);
-        assertEquals(origScreenHDp, task.getConfiguration().screenHeightDp);
+        assertEquals(origScreenHDp / 2, task.getConfiguration().screenHeightDp);
         t = new WindowContainerTransaction();
         t.setScreenSizeDp(stack.mRemoteToken.toWindowContainerToken(), SCREEN_WIDTH_DP_UNDEFINED,
                 SCREEN_HEIGHT_DP_UNDEFINED);
         mWm.mAtmService.mWindowOrganizerController.applyTransaction(t);
-        assertNotEquals(origScreenHDp, task.getConfiguration().screenHeightDp);
+        assertEquals(origScreenHDp, task.getConfiguration().screenHeightDp);
     }
 
     @Test
@@ -905,12 +909,13 @@
         task.setHasBeenVisible(true);
         verify(organizer, times(1)).onTaskAppeared(any());
 
-        task.taskOrganizerUnregistered();
+        task.setTaskOrganizer(null);
+        verify(organizer, times(1)).onTaskVanished(any());
         task.setTaskOrganizer(organizer);
         verify(organizer, times(2)).onTaskAppeared(any());
 
         task.removeImmediately();
-        verify(organizer).onTaskVanished(any());
+        verify(organizer, times(2)).onTaskVanished(any());
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
index 07a6179..cdf8eb4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
@@ -28,9 +28,11 @@
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.when;
 
+import android.Manifest;
 import android.app.IApplicationThread;
 import android.content.ComponentName;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.ServiceInfo;
 import android.content.res.Configuration;
 import android.platform.test.annotations.Presubmit;
 
@@ -200,6 +202,57 @@
         assertFalse(wpc.registeredForActivityConfigChanges());
     }
 
+    @Test
+    public void testActivityNotOverridingImeProcessConfig() {
+        ServiceInfo serviceInfo = new ServiceInfo();
+        serviceInfo.permission = Manifest.permission.BIND_INPUT_METHOD;
+        // Notify WPC that this process has started an IME service.
+        mWpc.onServiceStarted(serviceInfo);
+
+        final ActivityRecord activity = new ActivityBuilder(mService)
+                .setCreateTask(true)
+                .setUseProcess(mWpc)
+                .build();
+
+        mWpc.addActivityIfNeeded(activity);
+        // IME processes should not be registered for activity config changes.
+        assertFalse(mWpc.registeredForActivityConfigChanges());
+    }
+
+    @Test
+    public void testActivityNotOverridingAllyProcessConfig() {
+        ServiceInfo serviceInfo = new ServiceInfo();
+        serviceInfo.permission = Manifest.permission.BIND_ACCESSIBILITY_SERVICE;
+        // Notify WPC that this process has started an ally service.
+        mWpc.onServiceStarted(serviceInfo);
+
+        final ActivityRecord activity = new ActivityBuilder(mService)
+                .setCreateTask(true)
+                .setUseProcess(mWpc)
+                .build();
+
+        mWpc.addActivityIfNeeded(activity);
+        // Ally processes should not be registered for activity config changes.
+        assertFalse(mWpc.registeredForActivityConfigChanges());
+    }
+
+    @Test
+    public void testActivityNotOverridingVoiceInteractionProcessConfig() {
+        ServiceInfo serviceInfo = new ServiceInfo();
+        serviceInfo.permission = Manifest.permission.BIND_VOICE_INTERACTION;
+        // Notify WPC that this process has started an voice interaction service.
+        mWpc.onServiceStarted(serviceInfo);
+
+        final ActivityRecord activity = new ActivityBuilder(mService)
+                .setCreateTask(true)
+                .setUseProcess(mWpc)
+                .build();
+
+        mWpc.addActivityIfNeeded(activity);
+        // Voice interaction service processes should not be registered for activity config changes.
+        assertFalse(mWpc.registeredForActivityConfigChanges());
+    }
+
     private TestDisplayContent createTestDisplayContentInContainer() {
         return new TestDisplayContent.Builder(mService, 1000, 1500).build();
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index e561c13..6a64d1c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -247,7 +247,7 @@
     WindowState createAppWindow(Task task, int type, String name) {
         synchronized (mWm.mGlobalLock) {
             final ActivityRecord activity =
-                    WindowTestUtils.createTestActivityRecord(mDisplayContent);
+                    WindowTestUtils.createTestActivityRecord(task.getDisplayContent());
             task.addChild(activity, 0);
             return createWindow(null, type, activity, name);
         }
diff --git a/telecomm/java/android/telecom/Connection.java b/telecomm/java/android/telecom/Connection.java
index 9dfa3ac..fa99095 100755
--- a/telecomm/java/android/telecom/Connection.java
+++ b/telecomm/java/android/telecom/Connection.java
@@ -734,6 +734,31 @@
             "android.telecom.extra.ORIGINAL_CONNECTION_ID";
 
     /**
+     * Extra key set on a {@link Connection} when it was created via a remote connection service.
+     * For example, if a connection manager requests a remote connection service to create a call
+     * using one of the remote connection service's phone account handle, this extra will be set so
+     * that Telecom knows that the wrapped remote connection originated in a remote connection
+     * service.  We stash this in the extras since connection managers will typically copy the
+     * extras from a {@link RemoteConnection} to a {@link Connection} (there is ultimately not
+     * other way to relate a {@link RemoteConnection} to a {@link Connection}.
+     * @hide
+     */
+    public static final String EXTRA_REMOTE_PHONE_ACCOUNT_HANDLE =
+            "android.telecom.extra.REMOTE_PHONE_ACCOUNT_HANDLE";
+
+    /**
+     * Extra key set from a {@link ConnectionService} when using the remote connection APIs
+     * (e.g. {@link RemoteConnectionService#createRemoteConnection(PhoneAccountHandle,
+     * ConnectionRequest, boolean)}) to create a remote connection.  Provides the receiving
+     * {@link ConnectionService} with a means to know the package name of the requesting
+     * {@link ConnectionService} so that {@link #EXTRA_REMOTE_PHONE_ACCOUNT_HANDLE} can be set for
+     * better visibility in Telecom of where a connection ultimately originated.
+     * @hide
+     */
+    public static final String EXTRA_REMOTE_CONNECTION_ORIGINATING_PACKAGE_NAME =
+            "android.telecom.extra.REMOTE_CONNECTION_ORIGINATING_PACKAGE_NAME";
+
+    /**
      * Boolean connection extra key set on the extras passed to
      * {@link Connection#sendConnectionEvent} which indicates that audio is present
      * on the RTT call when the extra value is true.
diff --git a/telecomm/java/android/telecom/ConnectionService.java b/telecomm/java/android/telecom/ConnectionService.java
index 1b60e48..a716b37 100755
--- a/telecomm/java/android/telecom/ConnectionService.java
+++ b/telecomm/java/android/telecom/ConnectionService.java
@@ -1859,9 +1859,25 @@
                     new DisconnectCause(DisconnectCause.ERROR, "IMPL_RETURNED_NULL_CONFERENCE"),
                     request.getAccountHandle());
         }
-        if (conference.getExtras() != null) {
-            conference.getExtras().putString(Connection.EXTRA_ORIGINAL_CONNECTION_ID, callId);
+
+        Bundle extras = request.getExtras();
+        Bundle newExtras = new Bundle();
+        newExtras.putString(Connection.EXTRA_ORIGINAL_CONNECTION_ID, callId);
+        if (extras != null) {
+            // If the request originated from a remote connection service, we will add some
+            // tracking information that Telecom can use to keep informed of which package
+            // made the remote request, and which remote connection service was used.
+            if (extras.containsKey(Connection.EXTRA_REMOTE_CONNECTION_ORIGINATING_PACKAGE_NAME)) {
+                newExtras.putString(
+                        Connection.EXTRA_REMOTE_CONNECTION_ORIGINATING_PACKAGE_NAME,
+                        extras.getString(
+                                Connection.EXTRA_REMOTE_CONNECTION_ORIGINATING_PACKAGE_NAME));
+                newExtras.putParcelable(Connection.EXTRA_REMOTE_PHONE_ACCOUNT_HANDLE,
+                        request.getAccountHandle());
+            }
         }
+        conference.putExtras(newExtras);
+
         mConferenceById.put(callId, conference);
         mIdByConference.put(conference, callId);
         conference.addListener(mConferenceListener);
@@ -1936,6 +1952,30 @@
             Log.i(this, "createConnection, implementation returned null connection.");
             connection = Connection.createFailedConnection(
                     new DisconnectCause(DisconnectCause.ERROR, "IMPL_RETURNED_NULL_CONNECTION"));
+        } else {
+            try {
+                Bundle extras = request.getExtras();
+                if (extras != null) {
+                    // If the request originated from a remote connection service, we will add some
+                    // tracking information that Telecom can use to keep informed of which package
+                    // made the remote request, and which remote connection service was used.
+                    if (extras.containsKey(
+                            Connection.EXTRA_REMOTE_CONNECTION_ORIGINATING_PACKAGE_NAME)) {
+                        Bundle newExtras = new Bundle();
+                        newExtras.putString(
+                                Connection.EXTRA_REMOTE_CONNECTION_ORIGINATING_PACKAGE_NAME,
+                                extras.getString(
+                                        Connection.EXTRA_REMOTE_CONNECTION_ORIGINATING_PACKAGE_NAME
+                                ));
+                        newExtras.putParcelable(Connection.EXTRA_REMOTE_PHONE_ACCOUNT_HANDLE,
+                                request.getAccountHandle());
+                        connection.putExtras(newExtras);
+                    }
+                }
+            } catch (UnsupportedOperationException ose) {
+                // Do nothing; if the ConnectionService reported a failure it will be an instance
+                // of an immutable Connection which we cannot edit, so we're out of luck.
+            }
         }
 
         boolean isSelfManaged =
diff --git a/telecomm/java/android/telecom/Logging/Session.java b/telecomm/java/android/telecom/Logging/Session.java
index d82e93f..8d3f4e1 100644
--- a/telecomm/java/android/telecom/Logging/Session.java
+++ b/telecomm/java/android/telecom/Logging/Session.java
@@ -427,7 +427,7 @@
             StringBuilder methodName = new StringBuilder();
             methodName.append(getFullMethodPath(false /*truncatePath*/));
             if (mOwnerInfo != null && !mOwnerInfo.isEmpty()) {
-                methodName.append("(InCall package: ");
+                methodName.append("(");
                 methodName.append(mOwnerInfo);
                 methodName.append(")");
             }
diff --git a/telecomm/java/android/telecom/RemoteConnectionService.java b/telecomm/java/android/telecom/RemoteConnectionService.java
index cad5b70..a083301 100644
--- a/telecomm/java/android/telecom/RemoteConnectionService.java
+++ b/telecomm/java/android/telecom/RemoteConnectionService.java
@@ -258,6 +258,9 @@
             // See comments on Connection.EXTRA_ORIGINAL_CONNECTION_ID for more information.
             Bundle newExtras = new Bundle();
             newExtras.putString(Connection.EXTRA_ORIGINAL_CONNECTION_ID, callId);
+            // Track the fact this request was relayed through the remote connection service.
+            newExtras.putParcelable(Connection.EXTRA_REMOTE_PHONE_ACCOUNT_HANDLE,
+                    parcel.getPhoneAccount());
             conference.putExtras(newExtras);
 
             conference.registerCallback(new RemoteConference.Callback() {
@@ -383,6 +386,11 @@
             RemoteConnection remoteConnection = new RemoteConnection(callId,
                     mOutgoingConnectionServiceRpc, connection, callingPackage,
                     callingTargetSdkVersion);
+            // Track that it is via a remote connection.
+            Bundle newExtras = new Bundle();
+            newExtras.putParcelable(Connection.EXTRA_REMOTE_PHONE_ACCOUNT_HANDLE,
+                    connection.getPhoneAccount());
+            remoteConnection.putExtras(newExtras);
             mConnectionById.put(callId, remoteConnection);
             remoteConnection.registerCallback(new RemoteConnection.Callback() {
                 @Override
@@ -535,10 +543,20 @@
             ConnectionRequest request,
             boolean isIncoming) {
         final String id = UUID.randomUUID().toString();
+        Bundle extras = new Bundle();
+        if (request.getExtras() != null) {
+            extras.putAll(request.getExtras());
+        }
+        // We will set the package name for the originator of the remote request; this lets the
+        // receiving ConnectionService know that the request originated from a remote connection
+        // service so that it can provide tracking information for Telecom.
+        extras.putString(Connection.EXTRA_REMOTE_CONNECTION_ORIGINATING_PACKAGE_NAME,
+                mOurConnectionServiceImpl.getApplicationContext().getOpPackageName());
+
         final ConnectionRequest newRequest = new ConnectionRequest.Builder()
                 .setAccountHandle(request.getAccountHandle())
                 .setAddress(request.getAddress())
-                .setExtras(request.getExtras())
+                .setExtras(extras)
                 .setVideoState(request.getVideoState())
                 .setRttPipeFromInCall(request.getRttPipeFromInCall())
                 .setRttPipeToInCall(request.getRttPipeToInCall())
diff --git a/telephony/java/android/telephony/ims/ImsRcsManager.java b/telephony/java/android/telephony/ims/ImsRcsManager.java
index 151fb59..ede67dd 100644
--- a/telephony/java/android/telephony/ims/ImsRcsManager.java
+++ b/telephony/java/android/telephony/ims/ImsRcsManager.java
@@ -49,7 +49,7 @@
  *
  * Use {@link ImsManager#getImsRcsManager(int)} to create an instance of this manager.
  */
-public class ImsRcsManager implements RegistrationManager {
+public class ImsRcsManager {
     private static final String TAG = "ImsRcsManager";
 
     /**
@@ -173,11 +173,11 @@
     /**
      * @hide
      */
-    @Override
+    // @Override add back to RegistrationManager interface once public.
     @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
     public void registerImsRegistrationCallback(
             @NonNull @CallbackExecutor Executor executor,
-            @NonNull RegistrationCallback c)
+            @NonNull RegistrationManager.RegistrationCallback c)
             throws ImsException {
         if (c == null) {
             throw new IllegalArgumentException("Must include a non-null RegistrationCallback.");
@@ -204,7 +204,7 @@
     /**
      * @hide
      */
-    @Override
+    // @Override add back to RegistrationManager interface once public.
     @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
     public void unregisterImsRegistrationCallback(
             @NonNull RegistrationManager.RegistrationCallback c) {
@@ -228,10 +228,10 @@
     /**
      * @hide
      */
-    @Override
+    // @Override add back to RegistrationManager interface once public.
     @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
     public void getRegistrationState(@NonNull @CallbackExecutor Executor executor,
-            @NonNull @ImsRegistrationState Consumer<Integer> stateCallback) {
+            @NonNull @RegistrationManager.ImsRegistrationState Consumer<Integer> stateCallback) {
         if (stateCallback == null) {
             throw new IllegalArgumentException("Must include a non-null stateCallback.");
         }
@@ -260,7 +260,6 @@
     /**
      * @hide
      */
-    @Override
     @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
     public void getRegistrationTransportType(@NonNull @CallbackExecutor Executor executor,
             @NonNull @AccessNetworkConstants.TransportType
@@ -347,8 +346,7 @@
      * inactive subscription, it will result in a no-op.
      * @param c The RCS {@link AvailabilityCallback} to be removed.
      * @see #registerRcsAvailabilityCallback(Executor, AvailabilityCallback)
-     * @throws ImsException if the IMS service is not available when calling this method
-     * {@link ImsRcsController#unregisterRcsAvailabilityCallback()}.
+     * @throws ImsException if the IMS service is not available when calling this method.
      * See {@link ImsException#getCode()} for more information on the error codes.
      * @hide
      */
@@ -390,8 +388,7 @@
      * rather the subscription is capable of this service over IMS.
      * @see #isAvailable(int)
      * @see android.telephony.CarrierConfigManager#KEY_USE_RCS_PRESENCE_BOOL
-     * @throws ImsException if the IMS service is not available when calling this method
-     * {@link ImsRcsController#isCapable(int, int)}.
+     * @throws ImsException if the IMS service is not available when calling this method.
      * See {@link ImsException#getCode()} for more information on the error codes.
      * @hide
      */
@@ -424,9 +421,8 @@
      * @return true if the RCS capability is currently available for the associated subscription,
      * false otherwise. If the capability is available, IMS is registered and the service is
      * currently available over IMS.
-     * @see #isCapable(int)
-     * @throws ImsException if the IMS service is not available when calling this method
-     * {@link ImsRcsController#isAvailable(int, int)}.
+     * @see #isCapable(int, int)
+     * @throws ImsException if the IMS service is not available when calling this method.
      * See {@link ImsException#getCode()} for more information on the error codes.
      * @hide
      */
diff --git a/telephony/java/com/android/internal/telephony/SmsHeader.java b/telephony/java/com/android/internal/telephony/SmsHeader.java
index ab3fdf4..2f3897b 100644
--- a/telephony/java/com/android/internal/telephony/SmsHeader.java
+++ b/telephony/java/com/android/internal/telephony/SmsHeader.java
@@ -23,6 +23,8 @@
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Objects;
 
 /**
  * SMS user data header, as specified in TS 23.040 9.2.3.24.
@@ -71,6 +73,25 @@
     public static final int PORT_WAP_PUSH = 2948;
     public static final int PORT_WAP_WSP  = 9200;
 
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        SmsHeader smsHeader = (SmsHeader) o;
+        return languageTable == smsHeader.languageTable
+                && languageShiftTable == smsHeader.languageShiftTable
+                && Objects.equals(portAddrs, smsHeader.portAddrs)
+                && Objects.equals(concatRef, smsHeader.concatRef)
+                && Objects.equals(specialSmsMsgList, smsHeader.specialSmsMsgList)
+                && Objects.equals(miscEltList, smsHeader.miscEltList);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(portAddrs, concatRef, specialSmsMsgList, miscEltList, languageTable,
+                languageShiftTable);
+    }
+
     public static class PortAddrs {
         @UnsupportedAppUsage
         public PortAddrs() {
@@ -81,6 +102,21 @@
         @UnsupportedAppUsage
         public int origPort;
         public boolean areEightBits;
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            PortAddrs portAddrs = (PortAddrs) o;
+            return destPort == portAddrs.destPort
+                    && origPort == portAddrs.origPort
+                    && areEightBits == portAddrs.areEightBits;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(destPort, origPort, areEightBits);
+        }
     }
 
     public static class ConcatRef {
@@ -95,11 +131,41 @@
         @UnsupportedAppUsage
         public int msgCount;
         public boolean isEightBits;
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            ConcatRef concatRef = (ConcatRef) o;
+            return refNumber == concatRef.refNumber
+                    && seqNumber == concatRef.seqNumber
+                    && msgCount == concatRef.msgCount
+                    && isEightBits == concatRef.isEightBits;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(refNumber, seqNumber, msgCount, isEightBits);
+        }
     }
 
     public static class SpecialSmsMsg {
         public int msgIndType;
         public int msgCount;
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            SpecialSmsMsg that = (SpecialSmsMsg) o;
+            return msgIndType == that.msgIndType
+                    && msgCount == that.msgCount;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(msgIndType, msgCount);
+        }
     }
 
     /**
@@ -109,6 +175,22 @@
     public static class MiscElt {
         public int id;
         public byte[] data;
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            MiscElt miscElt = (MiscElt) o;
+            return id == miscElt.id
+                    && Arrays.equals(data, miscElt.data);
+        }
+
+        @Override
+        public int hashCode() {
+            int result = Objects.hash(id);
+            result = 31 * result + Arrays.hashCode(data);
+            return result;
+        }
     }
 
     @UnsupportedAppUsage
diff --git a/tests/BlobStoreTestUtils/src/com/android/utils/blob/DummyBlobData.java b/tests/BlobStoreTestUtils/src/com/android/utils/blob/DummyBlobData.java
index 35a6c26..371375c 100644
--- a/tests/BlobStoreTestUtils/src/com/android/utils/blob/DummyBlobData.java
+++ b/tests/BlobStoreTestUtils/src/com/android/utils/blob/DummyBlobData.java
@@ -77,7 +77,7 @@
             return mRandomSeed;
         }
 
-        public Builder setFileSize(int fileSize) {
+        public Builder setFileSize(long fileSize) {
             mFileSize = fileSize;
             return this;
         }
diff --git a/tests/BlobStoreTestUtils/src/com/android/utils/blob/Utils.java b/tests/BlobStoreTestUtils/src/com/android/utils/blob/Utils.java
index 482b23f..6927e86 100644
--- a/tests/BlobStoreTestUtils/src/com/android/utils/blob/Utils.java
+++ b/tests/BlobStoreTestUtils/src/com/android/utils/blob/Utils.java
@@ -34,6 +34,9 @@
 public class Utils {
     public static final int BUFFER_SIZE_BYTES = 16 * 1024;
 
+    public static final long KB_IN_BYTES = 1000;
+    public static final long MB_IN_BYTES = KB_IN_BYTES * 1000;
+
     public static void copy(InputStream in, OutputStream out, long lengthBytes)
             throws IOException {
         final byte[] buffer = new byte[BUFFER_SIZE_BYTES];
diff --git a/wifi/java/android/net/wifi/WifiManager.java b/wifi/java/android/net/wifi/WifiManager.java
index 6c8dc00..7d20d0d 100644
--- a/wifi/java/android/net/wifi/WifiManager.java
+++ b/wifi/java/android/net/wifi/WifiManager.java
@@ -758,6 +758,13 @@
     @SystemApi
     public static final int SAP_CLIENT_BLOCK_REASON_CODE_NO_MORE_STAS = 1;
 
+    /**
+     * Client disconnected for unspecified reason. This could for example be because the AP is being
+     * shut down.
+     * @hide
+     */
+    public static final int SAP_CLIENT_DISCONNECT_REASON_CODE_UNSPECIFIED = 2;
+
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(prefix = {"IFACE_IP_MODE_"}, value = {
diff --git a/wifi/java/android/net/wifi/WifiNetworkSpecifier.java b/wifi/java/android/net/wifi/WifiNetworkSpecifier.java
index 737b7c7..b0213b0 100644
--- a/wifi/java/android/net/wifi/WifiNetworkSpecifier.java
+++ b/wifi/java/android/net/wifi/WifiNetworkSpecifier.java
@@ -384,7 +384,8 @@
          *
          * For example:
          * To connect to an open network with a SSID prefix of "test" and a BSSID OUI of "10:03:23":
-         * {@code
+         *
+         * <pre>{@code
          * final NetworkSpecifier specifier =
          *      new Builder()
          *      .setSsidPattern(new PatternMatcher("test", PatterMatcher.PATTERN_PREFIX))
@@ -406,7 +407,7 @@
          *      // etc.
          * };
          * connectivityManager.requestNetwork(request, networkCallback);
-         * }
+         * }</pre>
          *
          * @return Instance of {@link NetworkSpecifier}.
          * @throws IllegalStateException on invalid params set.