Merge "Add Insets dumps and IME debug logs" into rvc-dev
diff --git a/Android.bp b/Android.bp
index ee381a4..01a43b6 100644
--- a/Android.bp
+++ b/Android.bp
@@ -930,6 +930,8 @@
     srcs: [
         "core/java/android/os/incremental/IIncrementalService.aidl",
         "core/java/android/os/incremental/IncrementalNewFileParams.aidl",
+        "core/java/android/os/incremental/IStorageHealthListener.aidl",
+        "core/java/android/os/incremental/StorageHealthCheckParams.aidl",
     ],
     path: "core/java",
 }
diff --git a/ApiDocs.bp b/ApiDocs.bp
index 90df19a..a81342a 100644
--- a/ApiDocs.bp
+++ b/ApiDocs.bp
@@ -66,7 +66,7 @@
         ":opt-telephony-srcs",
         ":opt-net-voip-srcs",
         ":art-module-public-api-stubs-source",
-        ":conscrypt.module.public.api.stubs.source",
+        ":conscrypt.module.public.api{.public.stubs.source}",
         ":android_icu4j_public_api_files",
         "test-mock/src/**/*.java",
         "test-runner/src/**/*.java",
diff --git a/StubLibraries.bp b/StubLibraries.bp
index 6e6efe5..c0197c4 100644
--- a/StubLibraries.bp
+++ b/StubLibraries.bp
@@ -69,7 +69,7 @@
     name: "metalava-full-api-stubs-default",
     defaults: ["metalava-base-api-stubs-default"],
     srcs: [
-        ":conscrypt.module.public.api.stubs.source",
+        ":conscrypt.module.public.api{.public.stubs.source}",
         ":framework-updatable-sources",
     ],
     sdk_version: "core_platform",
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 b4a7cd4..9a711d3 100644
--- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java
@@ -18,7 +18,6 @@
 import static android.provider.DeviceConfig.NAMESPACE_BLOBSTORE;
 import static android.text.format.Formatter.FLAG_IEC_UNITS;
 import static android.text.format.Formatter.formatFileSize;
-import static android.util.TimeUtils.formatDuration;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -58,18 +57,24 @@
      * Job Id for idle maintenance job ({@link BlobStoreIdleJobService}).
      */
     public static final int IDLE_JOB_ID = 0xB70B1D7; // 191934935L
-    /**
-     * Max time period (in millis) between each idle maintenance job run.
-     */
-    public static final long IDLE_JOB_PERIOD_MILLIS = TimeUnit.DAYS.toMillis(1);
-
-    /**
-     * Timeout in millis after which sessions with no updates will be deleted.
-     */
-    public static final long SESSION_EXPIRY_TIMEOUT_MILLIS = TimeUnit.DAYS.toMillis(7);
 
     public static class DeviceConfigProperties {
         /**
+         * Denotes the max time period (in millis) between each idle maintenance job run.
+         */
+        public static final String KEY_IDLE_JOB_PERIOD_MS = "idle_job_period_ms";
+        public static final long DEFAULT_IDLE_JOB_PERIOD_MS = TimeUnit.DAYS.toMillis(1);
+        public static long IDLE_JOB_PERIOD_MS = DEFAULT_IDLE_JOB_PERIOD_MS;
+
+        /**
+         * Denotes the timeout in millis after which sessions with no updates will be deleted.
+         */
+        public static final String KEY_SESSION_EXPIRY_TIMEOUT_MS =
+                "session_expiry_timeout_ms";
+        public static final long DEFAULT_SESSION_EXPIRY_TIMEOUT_MS = TimeUnit.DAYS.toMillis(7);
+        public static long SESSION_EXPIRY_TIMEOUT_MS = DEFAULT_SESSION_EXPIRY_TIMEOUT_MS;
+
+        /**
          * Denotes how low the limit for the amount of data, that an app will be allowed to acquire
          * a lease on, can be.
          */
@@ -119,6 +124,13 @@
             }
             properties.getKeyset().forEach(key -> {
                 switch (key) {
+                    case KEY_IDLE_JOB_PERIOD_MS:
+                        IDLE_JOB_PERIOD_MS = properties.getLong(key, DEFAULT_IDLE_JOB_PERIOD_MS);
+                        break;
+                    case KEY_SESSION_EXPIRY_TIMEOUT_MS:
+                        SESSION_EXPIRY_TIMEOUT_MS = properties.getLong(key,
+                                DEFAULT_SESSION_EXPIRY_TIMEOUT_MS);
+                        break;
                     case KEY_TOTAL_BYTES_PER_APP_LIMIT_FLOOR:
                         TOTAL_BYTES_PER_APP_LIMIT_FLOOR = properties.getLong(key,
                                 DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FLOOR);
@@ -143,6 +155,12 @@
 
         static void dump(IndentingPrintWriter fout, Context context) {
             final String dumpFormat = "%s: [cur: %s, def: %s]";
+            fout.println(String.format(dumpFormat, KEY_IDLE_JOB_PERIOD_MS,
+                    TimeUtils.formatDuration(IDLE_JOB_PERIOD_MS),
+                    TimeUtils.formatDuration(DEFAULT_IDLE_JOB_PERIOD_MS)));
+            fout.println(String.format(dumpFormat, KEY_SESSION_EXPIRY_TIMEOUT_MS,
+                    TimeUtils.formatDuration(SESSION_EXPIRY_TIMEOUT_MS),
+                    TimeUtils.formatDuration(DEFAULT_SESSION_EXPIRY_TIMEOUT_MS)));
             fout.println(String.format(dumpFormat, KEY_TOTAL_BYTES_PER_APP_LIMIT_FLOOR,
                     formatFileSize(context, TOTAL_BYTES_PER_APP_LIMIT_FLOOR, FLAG_IEC_UNITS),
                     formatFileSize(context, DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FLOOR,
@@ -167,6 +185,22 @@
     }
 
     /**
+     * Returns the max time period (in millis) between each idle maintenance job run.
+     */
+    public static long getIdleJobPeriodMs() {
+        return DeviceConfigProperties.IDLE_JOB_PERIOD_MS;
+    }
+
+    /**
+     * Returns whether a session is expired or not. A session is considered expired if the session
+     * has not been modified in a while (i.e. SESSION_EXPIRY_TIMEOUT_MS).
+     */
+    public static boolean hasSessionExpired(long sessionLastModifiedMs) {
+        return sessionLastModifiedMs
+                < System.currentTimeMillis() - DeviceConfigProperties.SESSION_EXPIRY_TIMEOUT_MS;
+    }
+
+    /**
      * Returns the maximum amount of data that an app can acquire a lease on.
      */
     public static long getAppDataBytesLimit() {
@@ -277,9 +311,6 @@
         fout.println("XML current version: " + XML_VERSION_CURRENT);
 
         fout.println("Idle job ID: " + IDLE_JOB_ID);
-        fout.println("Idle job period: " + formatDuration(IDLE_JOB_PERIOD_MILLIS));
-
-        fout.println("Session expiry timeout: " + formatDuration(SESSION_EXPIRY_TIMEOUT_MILLIS));
 
         fout.println("Total bytes per app limit: " + formatFileSize(context,
                 getAppDataBytesLimit(), FLAG_IEC_UNITS));
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreIdleJobService.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreIdleJobService.java
index 460e776..4b0f719 100644
--- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreIdleJobService.java
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreIdleJobService.java
@@ -16,7 +16,6 @@
 package com.android.server.blob;
 
 import static com.android.server.blob.BlobStoreConfig.IDLE_JOB_ID;
-import static com.android.server.blob.BlobStoreConfig.IDLE_JOB_PERIOD_MILLIS;
 import static com.android.server.blob.BlobStoreConfig.LOGV;
 import static com.android.server.blob.BlobStoreConfig.TAG;
 
@@ -60,7 +59,7 @@
                 new ComponentName(context, BlobStoreIdleJobService.class))
                         .setRequiresDeviceIdle(true)
                         .setRequiresCharging(true)
-                        .setPeriodic(IDLE_JOB_PERIOD_MILLIS)
+                        .setPeriodic(BlobStoreConfig.getIdleJobPeriodMs())
                         .build();
         jobScheduler.schedule(job);
         if (LOGV) {
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java
index 9376198..7a27b2c 100644
--- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java
@@ -29,10 +29,10 @@
 import static android.os.UserHandle.USER_NULL;
 
 import static com.android.server.blob.BlobStoreConfig.LOGV;
-import static com.android.server.blob.BlobStoreConfig.SESSION_EXPIRY_TIMEOUT_MILLIS;
 import static com.android.server.blob.BlobStoreConfig.TAG;
 import static com.android.server.blob.BlobStoreConfig.XML_VERSION_CURRENT;
 import static com.android.server.blob.BlobStoreConfig.getAdjustedCommitTimeMs;
+import static com.android.server.blob.BlobStoreConfig.hasSessionExpired;
 import static com.android.server.blob.BlobStoreSession.STATE_ABANDONED;
 import static com.android.server.blob.BlobStoreSession.STATE_COMMITTED;
 import static com.android.server.blob.BlobStoreSession.STATE_VERIFIED_INVALID;
@@ -986,9 +986,9 @@
             userSessions.removeIf((sessionId, blobStoreSession) -> {
                 boolean shouldRemove = false;
 
+                // TODO: handle the case where no content has been written to session yet.
                 // Cleanup sessions which haven't been modified in a while.
-                if (blobStoreSession.getSessionFile().lastModified()
-                        < System.currentTimeMillis() - SESSION_EXPIRY_TIMEOUT_MILLIS) {
+                if (hasSessionExpired(blobStoreSession.getSessionFile().lastModified())) {
                     shouldRemove = true;
                 }
 
diff --git a/apex/sdkextensions/Android.bp b/apex/sdkextensions/Android.bp
deleted file mode 100644
index fdb078e..0000000
--- a/apex/sdkextensions/Android.bp
+++ /dev/null
@@ -1,84 +0,0 @@
-// 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 {
-    default_visibility: [":__subpackages__"],
-}
-
-apex {
-    name: "com.android.sdkext",
-    defaults: [ "com.android.sdkext-defaults" ],
-    binaries: [ "derive_sdk" ],
-    prebuilts: [ "cur_sdkinfo" ],
-    manifest: "manifest.json",
-    min_sdk_version: "current",
-}
-
-apex_defaults {
-    name: "com.android.sdkext-defaults",
-    updatable: true,
-    min_sdk_version: "R",
-    java_libs: [ "framework-sdkextensions" ],
-    prebuilts: [
-        "derive_sdk.rc",
-    ],
-    key: "com.android.sdkext.key",
-    certificate: ":com.android.sdkext.certificate",
-}
-
-sdk {
-    name: "sdkextensions-sdk",
-    java_sdk_libs: [ "framework-sdkextensions" ],
-}
-
-apex_key {
-    name: "com.android.sdkext.key",
-    public_key: "com.android.sdkext.avbpubkey",
-    private_key: "com.android.sdkext.pem",
-}
-
-android_app_certificate {
-    name: "com.android.sdkext.certificate",
-    certificate: "com.android.sdkext",
-}
-
-python_binary_host {
-    name: "gen_sdkinfo",
-    srcs: [
-        "sdk.proto",
-        "gen_sdkinfo.py",
-    ],
-    proto: {
-        canonical_path_from_root: false,
-    },
-    version: {
-        py3: {
-            embedded_launcher: true,
-        },
-    },
-}
-
-gensrcs {
-    name: "cur_sdkinfo_src",
-    srcs: [""],
-    tools: [ "gen_sdkinfo" ],
-    cmd: "$(location) -v 0 -o $(out)",
-}
-
-prebuilt_etc {
-    name: "cur_sdkinfo",
-    src: ":cur_sdkinfo_src",
-    filename: "sdkinfo.binarypb",
-    installable: false,
-}
diff --git a/apex/sdkextensions/OWNERS b/apex/sdkextensions/OWNERS
deleted file mode 100644
index a6e5522..0000000
--- a/apex/sdkextensions/OWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
[email protected]
[email protected]
diff --git a/apex/sdkextensions/TEST_MAPPING b/apex/sdkextensions/TEST_MAPPING
deleted file mode 100644
index 3dc1b9f..0000000
--- a/apex/sdkextensions/TEST_MAPPING
+++ /dev/null
@@ -1,10 +0,0 @@
-{
-  "presubmit": [
-    {
-      "name": "CtsSdkExtensionsTestCases"
-    },
-    {
-      "name": "sdkextensions_e2e_tests"
-    }
-  ]
-}
diff --git a/apex/sdkextensions/com.android.sdkext.avbpubkey b/apex/sdkextensions/com.android.sdkext.avbpubkey
deleted file mode 100644
index 8f47741..0000000
--- a/apex/sdkextensions/com.android.sdkext.avbpubkey
+++ /dev/null
Binary files differ
diff --git a/apex/sdkextensions/com.android.sdkext.pem b/apex/sdkextensions/com.android.sdkext.pem
deleted file mode 100644
index 8164601..0000000
--- a/apex/sdkextensions/com.android.sdkext.pem
+++ /dev/null
@@ -1,51 +0,0 @@
------BEGIN RSA PRIVATE KEY-----
-MIIJKQIBAAKCAgEAr72pTSavrziDP54AtQZlRclDxJf9HXRZwFRbYx9hWZ4z7ZtO
-pNBDPvPJCiAOVUsILgCQhBUolz2dyLob25Fd0PVp0n9ibIPEQYjTfHjOK40qb57N
-LhEp2ceGiAfsywPSi0TH1PQ6JgbCe/RM4TefI/sj3gYJPka3ksMvylhMIgUVLgME
-kYizhzwHqlLMspB858SioREZdGwcdZrMMIajGFU69Q9ZRDBzhPvxyKhYoObcOtk1
-uVaiE/fNoi3wKGJz2l2vhUuNrQW7MWlVMag+Qes4YACUTk9LZrOVFEJFjWc8xGUi
-ABtfKGs5JgNr/sWnhvifLn8lJuf0/BJmjD+L5QwXYs2cS7gcZJtTM12J94r0Twgw
-wF2lNmIxAE9sYqj5Rh3dIlTPE5vMUECmQEGjIBB/hzT65VxVqSjU/IlS506JTg3p
-IOQtZ15cUzTBpda4jrvqcq6RNVvgBCu2bV5D8Z4z9FUlPyvD+Zq/6lcoJfLtznAs
-G2463hyPAHTGBIcZ5p5bTuGxoAb6ivyqo4b9Qi4yYA6je9HJmuy8T3Mn5JROoeu9
-BH1K54r/mpT4TQPwuKUvRRtBAV2OPHjo+zp0Gd4Y6rxDYxEIdfEae7pQr/QExSPB
-q/QCr9RhixR1mO373LHuja+MxdAxIxugb2HTS61PQo+PbYrhJMcVuxTwJOECAwEA
-AQKCAgAH7ToRrMkH0ji5SdsmTx+KQkW4PFLCXVke/68PjX7KmAQnl3W4oVwnHr/W
-oROEbVn1GTlre7jU+YaAY0SWZrwgjLE1OWGrG1ZizlUbrCdAd6GOX09J4KROml1L
-DXB0x7tbZMLOrCVjSbLD/ITrM6MN8Gnxvbv0/yOQjxU8vzbP4gLOjHxMRCo001RV
-Ll7lPvcjTQ84zJilU6sE8vJ6zdfVZSK/ou2X0cekG+kP7+fvefo8/UcbEPlGhUrV
-IdVPPQGUu90K2hmN0FBdLi8Vik0klAN68Qu/bHwuKbNzsnmIoztucFFUR+fG3u84
-87aPS0L/J3+mjT2Tv6qhJANUGBmrK/h7MkelpKXlRTCITJLX9xP7hfSbJ4f6aLVq
-ZYPPciGxSBbUDgAwvPtOlMDzccg7YsYyiBBO28wh8MN97rePmc0z6nGmjeXhcbCC
-QktG50VYFCyqp5muKgqQmRfRjHFHLWs8GEqgxMeEL3U3HjYfCYr+6E8Sr5OnOBeH
-3buCi1+zgnNYCvbamgY/OJmW7f9h5O31hxmTplc2E1ZuxUGQZthabt1rN3bmNkyf
-KUmPwnIYkDkWBSV5lzyQExfS3/EVvj0EnHhx8faamimNrGo8xCcfnLT3c0WEFVmo
-yIyVRX3EpXJFM2JkeJ21/IEZXTzHSoNxk12CBG8i8lLSflWSMQKCAQEA2ZqVnOuV
-SZfLCUYUUh8Hvhc5zONstfq7ma1Zsttsdaj9t68nLRiBDvLOGnMjDkYZzoSd4fyl
-oy+YqWGBqcqa5kg1NOCH0I46p9d8RcWAfDnB4sqbLgWh70qsvni6igRijmsMDvkA
-U9HeEdPaLCjQ4UXw7GQvN5rRxuRt+OSqV3tV/Pk9JYyYjz7faC8dmbKDrWHHuOvm
-/9y6Xy+L5IgftykNlUeddSCIoMOAadM7BiRjsrHnOYBQ8xBcn0OYafpIswItrgVi
-IrsPJaBFidx8QYK4MVibyka6U0cm28OocDSPtSk/4jrnCEEhLjFUnWwuMXuBGlrd
-W7wP/muoJqb1VwKCAQEAzsAT90kkOCvAcrfGRE3KkUjwWAsQyP8u2+27JIQPqrpW
-GfWAzJXFt80TSp0Zf/Lrq3/SQ9n4AaL4K9dcMoreedoQN9C9JI7zPtZAWNrJVUcV
-dq2gZjBQ78+oK7uQgvFNWxga5D+Nh+Y+9Tp537fc5HIh0Y13PgsxxPk2OnZJTvLX
-HM5H7Aua9ssmqChsrKihuUsDSPozfBz+H7FNHEdKMqVLqJJSK6m0uMxuLovdVfka
-5S7iBMjEGZc46Iz3ckE0pdOiQLooNqfEQqFe5Uou/KZxxKI1NW25rEEBTVyQWt+2
-BNUCfKP7noZ45u5sUY3eJrgI7BrPEDbNS81WYaLchwKCAQA8Q4mHyd6wYO+EA/qA
-u8NDK9+AFMP4qhXme5HJ7Obetwx9IG7zGEQ1xZy6yoQ84cEn5qZq/bNJvFbFIhHs
-2gWIHRtPJ5e1dI5eCVmLYSUyQjSmAIJ1fm3YfY/VuE3BB3HcC11tkBw9GnQr78YO
-UMd4fAw7C4vgFGpgcMbcFUfvrmKkCsqaaZOeqETq75F9DWlWTSwo1HxHA/RBhENz
-6RcPfLkcTJcY5wevrjUUGcHQ86cAyDBHRngkuLVODkRZpU0Y9lN8TFVfVPre6sIX
-ag6nffJRCD8tB+V2RtBGMKunV4ctHt1oY/Oz34W260aJynoIjjG1ANEpJK4xQdNx
-0O9FAoIBAQCz2AGGKemHswdEwveEkuaSWpA3Bekj7lYkmTchHH9EU7JyAkx3qhDD
-QXB2hxGXawf1tsqAmypQwiJ+gGeCz6mW9UkGRF1DX9XX4yc2I5rew2a4RXAxc/Xz
-pP70i8O5I43Wn7FEusOyY2aAis1Y/eb4EQ+56QTAw5wXa3DwidRbCIJ2XDnT6oRy
-CWUnAYMG7ek/9TB2Wq5OWCn2B5S79IdmZsLZb+5qbMT3u1xcwO1Xy8jJc27IGpv6
-ZsDqCTV1/aJ+XQnWpBg28tiV3Sle6pjUzTRJh5AhWcEZRbKMSOiJI/CBY4k2Qq6t
-xuuEdgFjL7T+mTepqehUglcyiPuLEtAhAoIBAQDDQ5pTFOlunfYzm+CIvvImAgy7
-vrEabJYABATytAbXRMMbrKoEdU2ApEDyEW7PgysDnYLAZ+icObnJlBTYvNdlWFly
-XiviGVfpjFWDT9U/gUwFQu2lfEjoiivoGS92USHUy4UMVHiiznnm20VLLkgd3xna
-HUNSDdHIEgzOTmDsKJwMfA9zGckx23JJimPR5ekv6vr6QllYtECs4lTC1gVQAp2f
-5daxHRbkmO6gw1RgQADLkAnYz3aI1jNuHm5VyAZGt/d3JCtZ3Wwwejll8uJ4J09G
-oEtqyY9RVeHK50bLO4lyAXFiE+J6qqXjsGC20cpxeZYW5llMY/dhA6WV4YXV
------END RSA PRIVATE KEY-----
diff --git a/apex/sdkextensions/com.android.sdkext.pk8 b/apex/sdkextensions/com.android.sdkext.pk8
deleted file mode 100644
index ccc0bf4..0000000
--- a/apex/sdkextensions/com.android.sdkext.pk8
+++ /dev/null
Binary files differ
diff --git a/apex/sdkextensions/com.android.sdkext.x509.pem b/apex/sdkextensions/com.android.sdkext.x509.pem
deleted file mode 100644
index 45d2ade..0000000
--- a/apex/sdkextensions/com.android.sdkext.x509.pem
+++ /dev/null
@@ -1,35 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIGIzCCBAugAwIBAgIUXuDL7QvzQh7S6rihWz2KRvCFVT0wDQYJKoZIhvcNAQEL
-BQAwgZ8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH
-DA1Nb3VudGFpbiBWaWV3MRAwDgYDVQQKDAdBbmRyb2lkMRAwDgYDVQQLDAdBbmRy
-b2lkMRswGQYDVQQDDBJjb20uYW5kcm9pZC5zZGtleHQxIjAgBgkqhkiG9w0BCQEW
-E2FuZHJvaWRAYW5kcm9pZC5jb20wIBcNMTkxMjAyMTQyNDM0WhgPNDc1NzEwMjgx
-NDI0MzRaMIGfMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQG
-A1UEBwwNTW91bnRhaW4gVmlldzEQMA4GA1UECgwHQW5kcm9pZDEQMA4GA1UECwwH
-QW5kcm9pZDEbMBkGA1UEAwwSY29tLmFuZHJvaWQuc2RrZXh0MSIwIAYJKoZIhvcN
-AQkBFhNhbmRyb2lkQGFuZHJvaWQuY29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A
-MIICCgKCAgEAxFvZZ6ES1oqAu1K74/ZxnC3SOhHnLISLBgJEe7DqtdpuNFAwvdVO
-RL/HULhDbjYlOhpU2x3SavDIZZ2lRfiS9Q+M25WftxTRHVjBcpgwbV77TVxPKlAa
-tVN2lUVOY+s4QAVMNIXjC4kCKK/pCQtacH715EtdV47fWdg/Nx4iP/Aord8k3KGI
-9iI2ZOUjaugTRxu5lKRNDrv0bw5rEzyYmDyMud+kR/iS3/5oog57wPE0ffAkZXWE
-p3L2Cejre3ekCizsvVh6EmH6ForKLtL6f0z5Zir1f4R9+YcENspTlJR3pDhg7y3I
-uTQT/iDCtV0l+g2PjGZPEeAQHND3+kDQR7Sno/WC1Nhws6vcu1MdrC+kIh1ewx4y
-8moy/yqb5M98PJDzTSi/AOTB/OiqLXo/T8rjLBmirs9y3fTT6gJ6qXxOWgt8dle9
-7TBfa84Xi8uVY61c+A+YI0nLal7QDPsP3RPv5sJSQ9x9YnweVfD9Q0EOi52sSNu+
-QuN/kcUrMgPgha20VhfH/CkkPDyIp6aZyHHM69MIl+cYEm8vPa5uy3dosuRomT0f
-I4HOBjULDIuj+rIi+Rg3qHvmpuejwZXI/FBNWIhLEUG3ytgksjMaBoYAYflGdrcj
-BQexuF3EO+j4uo7JGjNcaT7wRoCH9gt29VHckDg2qz6VWXrlpmME4UkCAwEAAaNT
-MFEwHQYDVR0OBBYEFISN2nmUHllgPZMZ62U7mU3ZxzlXMB8GA1UdIwQYMBaAFISN
-2nmUHllgPZMZ62U7mU3ZxzlXMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL
-BQADggIBAFHIwyBNIVyHXUsDUdcjxfojXQsF/BCL9ehE3pgdkvDfQanaIREWn0nc
-oCFDFkYMRqaXOGC5TKq4OCjXOLsdfODt8HQ3F9J1B0ghQ5tfOdw7xDugNAszqP/Q
-h7kpvqLTycjrqOeZ5KjxEEYtP/KlUmALgOKcTcSH+XhWyxhjF4j24T9F2yJRr3/A
-r1NGU/djH953bHKC8OpJ2teUpDLA4TxVp/EhslH2eVigF80c/w74QPLEWkD9zv/4
-YeRg/R5N83zHs99NtlWMIeHfK6fUbzMyaSZtvm+jK20tkByQb/OQRed+drk25MtL
-68IRvxqri367qRScdpTZbu0ByLO4X8gFdubRUWr+tcO4pZX+DJRVriExbOkU2xhS
-Vtslq23V/hwTuUNm1CXjR70mPS13BTmHrIQDqLoIw/WTQlGh+vxnlAFRIHM3KB2c
-OdzUBu+NcB4aZEd0KKtct600A0DKPr1MQPb5jDq9wEtPSQYwMF0nRFNnXltbrXMd
-4hhwnhKr74fVMUmb+7eQP56XE/Nk4D+npMO54vv1pav+DI2/nxCND9BOLBweY38p
-Tvd2RjesMok0zXuVXiCIu4GEpwo7WkSnv25xrb0Ey2M8QWnGNnCcX7Kv6ip3RdWy
-HiN0G8RJrs/yNEVSDRx8ZhtwTpXVPQxbARbmhNF4/fnolElkmrMP
------END CERTIFICATE-----
diff --git a/apex/sdkextensions/derive_sdk/Android.bp b/apex/sdkextensions/derive_sdk/Android.bp
deleted file mode 100644
index 41eae09..0000000
--- a/apex/sdkextensions/derive_sdk/Android.bp
+++ /dev/null
@@ -1,55 +0,0 @@
-// 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.
-
-cc_defaults {
-    name: "derive_sdk-defaults",
-    srcs: [
-        "derive_sdk.cpp",
-        "sdk.proto",
-    ],
-    proto: {
-        type: "lite",
-        static: true,
-    },
-    min_sdk_version: "current",
-    shared_libs: ["liblog"],
-    // static c++/libbase for smaller size
-    stl: "c++_static",
-    static_libs: ["libbase"],
-}
-
-cc_binary {
-    name: "derive_sdk",
-    defaults: [ "derive_sdk-defaults" ],
-    apex_available: [ "com.android.sdkext" ],
-    visibility: [ "//frameworks/base/apex/sdkextensions" ]
-}
-
-// Work around testing using a 64-bit test suite on 32-bit test device by
-// using a prefer32 version of derive_sdk in testing.
-cc_binary {
-    name: "derive_sdk_prefer32",
-    defaults: [ "derive_sdk-defaults" ],
-    compile_multilib: "prefer32",
-    stem: "derive_sdk",
-    apex_available: [ "test_com.android.sdkext" ],
-    visibility: [ "//frameworks/base/apex/sdkextensions/testing" ],
-    installable: false,
-}
-
-prebuilt_etc {
-    name: "derive_sdk.rc",
-    src: "derive_sdk.rc",
-    installable: false,
-}
diff --git a/apex/sdkextensions/derive_sdk/derive_sdk.cpp b/apex/sdkextensions/derive_sdk/derive_sdk.cpp
deleted file mode 100644
index 900193a..0000000
--- a/apex/sdkextensions/derive_sdk/derive_sdk.cpp
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * 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.
- */
-
-#define LOG_TAG "derive_sdk"
-
-#include <algorithm>
-#include <dirent.h>
-#include <iostream>
-#include <sys/stat.h>
-#include <vector>
-
-#include <android-base/file.h>
-#include <android-base/logging.h>
-#include <android-base/properties.h>
-
-#include "frameworks/base/apex/sdkextensions/derive_sdk/sdk.pb.h"
-
-using com::android::sdkext::proto::SdkVersion;
-
-int main(int, char**) {
-    std::unique_ptr<DIR, decltype(&closedir)> apex(opendir("/apex"), closedir);
-    if (!apex) {
-        LOG(ERROR) << "Could not read /apex";
-        return EXIT_FAILURE;
-    }
-    struct dirent* de;
-    std::vector<std::string> paths;
-    while ((de = readdir(apex.get()))) {
-        std::string name = de->d_name;
-        if (name[0] == '.' || name.find('@') != std::string::npos) {
-            // Skip <name>@<ver> dirs, as they are bind-mounted to <name>
-            continue;
-        }
-        std::string path = "/apex/" + name + "/etc/sdkinfo.binarypb";
-        struct stat statbuf;
-        if (stat(path.c_str(), &statbuf) == 0) {
-            paths.push_back(path);
-        }
-    }
-
-    std::vector<int> versions;
-    for (const auto& path : paths) {
-        std::string contents;
-        if (!android::base::ReadFileToString(path, &contents, true)) {
-            LOG(ERROR) << "failed to read " << path;
-            continue;
-        }
-        SdkVersion sdk_version;
-        if (!sdk_version.ParseFromString(contents)) {
-            LOG(ERROR) << "failed to parse " << path;
-            continue;
-        }
-        LOG(INFO) << "Read version " << sdk_version.version() << " from " << path;
-        versions.push_back(sdk_version.version());
-    }
-    auto itr = std::min_element(versions.begin(), versions.end());
-    std::string prop_value = itr == versions.end() ? "0" : std::to_string(*itr);
-
-    if (!android::base::SetProperty("build.version.extensions.r", prop_value)) {
-        LOG(ERROR) << "failed to set sdk_info prop";
-        return EXIT_FAILURE;
-    }
-
-    LOG(INFO) << "R extension version is " << prop_value;
-    return EXIT_SUCCESS;
-}
diff --git a/apex/sdkextensions/derive_sdk/derive_sdk.rc b/apex/sdkextensions/derive_sdk/derive_sdk.rc
deleted file mode 100644
index 18f021c..0000000
--- a/apex/sdkextensions/derive_sdk/derive_sdk.rc
+++ /dev/null
@@ -1,5 +0,0 @@
-service derive_sdk /apex/com.android.sdkext/bin/derive_sdk
-    user nobody
-    group nobody
-    oneshot
-    disabled
diff --git a/apex/sdkextensions/derive_sdk/sdk.proto b/apex/sdkextensions/derive_sdk/sdk.proto
deleted file mode 100644
index d15b935..0000000
--- a/apex/sdkextensions/derive_sdk/sdk.proto
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * 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.
- */
-
-syntax = "proto3";
-package com.android.sdkext.proto;
-
-option java_outer_classname = "SdkProto";
-option optimize_for = LITE_RUNTIME;
-
-message SdkVersion {
-  int32 version = 1;
-}
diff --git a/apex/sdkextensions/framework/Android.bp b/apex/sdkextensions/framework/Android.bp
deleted file mode 100644
index b8aad7d..0000000
--- a/apex/sdkextensions/framework/Android.bp
+++ /dev/null
@@ -1,49 +0,0 @@
-// 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 {
-    default_visibility: [ ":__pkg__" ]
-}
-
-filegroup {
-    name: "framework-sdkextensions-sources",
-    srcs: [
-        "java/**/*.java",
-    ],
-    path: "java",
-    visibility: [ "//frameworks/base" ] // For the "global" stubs.
-}
-
-java_sdk_library {
-    name: "framework-sdkextensions",
-    srcs: [ ":framework-sdkextensions-sources" ],
-    defaults: ["framework-module-defaults"],
-
-    // TODO(b/155480189) - Remove naming_scheme once references have been resolved.
-    // Temporary java_sdk_library component naming scheme to use to ease the transition from separate
-    // modules to java_sdk_library.
-    naming_scheme: "framework-modules",
-
-    permitted_packages: [ "android.os.ext" ],
-    installable: true,
-    visibility: [
-        "//frameworks/base/apex/sdkextensions",
-        "//frameworks/base/apex/sdkextensions/testing",
-    ],
-    hostdex: true, // for hiddenapi check
-    apex_available: [
-        "com.android.sdkext",
-        "test_com.android.sdkext",
-    ],
-}
diff --git a/apex/sdkextensions/framework/api/current.txt b/apex/sdkextensions/framework/api/current.txt
deleted file mode 100644
index d802177..0000000
--- a/apex/sdkextensions/framework/api/current.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 2.0
diff --git a/apex/sdkextensions/framework/api/module-lib-current.txt b/apex/sdkextensions/framework/api/module-lib-current.txt
deleted file mode 100644
index d802177..0000000
--- a/apex/sdkextensions/framework/api/module-lib-current.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 2.0
diff --git a/apex/sdkextensions/framework/api/module-lib-removed.txt b/apex/sdkextensions/framework/api/module-lib-removed.txt
deleted file mode 100644
index d802177..0000000
--- a/apex/sdkextensions/framework/api/module-lib-removed.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 2.0
diff --git a/apex/sdkextensions/framework/api/removed.txt b/apex/sdkextensions/framework/api/removed.txt
deleted file mode 100644
index d802177..0000000
--- a/apex/sdkextensions/framework/api/removed.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 2.0
diff --git a/apex/sdkextensions/framework/api/system-current.txt b/apex/sdkextensions/framework/api/system-current.txt
deleted file mode 100644
index bbff4c5..0000000
--- a/apex/sdkextensions/framework/api/system-current.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-// Signature format: 2.0
-package android.os.ext {
-
-  public class SdkExtensions {
-    method public static int getExtensionVersion(int);
-  }
-
-}
-
diff --git a/apex/sdkextensions/framework/api/system-removed.txt b/apex/sdkextensions/framework/api/system-removed.txt
deleted file mode 100644
index d802177..0000000
--- a/apex/sdkextensions/framework/api/system-removed.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 2.0
diff --git a/apex/sdkextensions/framework/java/android/os/ext/SdkExtensions.java b/apex/sdkextensions/framework/java/android/os/ext/SdkExtensions.java
deleted file mode 100644
index 6c25f28..0000000
--- a/apex/sdkextensions/framework/java/android/os/ext/SdkExtensions.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.os.ext;
-
-import android.annotation.IntDef;
-import android.annotation.SystemApi;
-import android.os.Build.VERSION_CODES;
-import android.os.SystemProperties;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/**
- * Methods for interacting with the extension SDK.
- *
- * This class provides information about the extension SDK version present
- * on this device. Use the {@link #getExtensionVersion(int) getExtension} to
- * query for the extension version for the given SDK version.
-
- * @hide
- */
-@SystemApi
-public class SdkExtensions {
-
-    private static final int R_EXTENSION_INT;
-    static {
-        // Note: when adding more extension versions, the logic that records current
-        // extension versions when saving a rollback must also be updated.
-        // At the time of writing this is in RollbackManagerServiceImpl#getExtensionVersions()
-        R_EXTENSION_INT = SystemProperties.getInt("build.version.extensions.r", 0);
-    }
-
-    /**
-     * Values suitable as parameters for {@link #getExtensionVersion(int)}.
-     * @hide
-     */
-    @IntDef(value = { VERSION_CODES.R })
-    @Retention(RetentionPolicy.SOURCE)
-    public @interface SdkVersion {}
-
-    private SdkExtensions() { }
-
-    /**
-     * Return the version of the extension to the given SDK.
-     *
-     * @param sdk the SDK version to get the extension version of.
-     * @see SdkVersion
-     * @throws IllegalArgumentException if sdk is not an sdk version with extensions
-     */
-    public static int getExtensionVersion(@SdkVersion int sdk) {
-        if (sdk < VERSION_CODES.R) {
-            throw new IllegalArgumentException(String.valueOf(sdk) + " does not have extensions");
-        }
-
-        if (sdk == VERSION_CODES.R) {
-            return R_EXTENSION_INT;
-        }
-        return 0;
-    }
-
-}
diff --git a/apex/sdkextensions/framework/java/android/os/ext/package.html b/apex/sdkextensions/framework/java/android/os/ext/package.html
deleted file mode 100644
index 34c1697..0000000
--- a/apex/sdkextensions/framework/java/android/os/ext/package.html
+++ /dev/null
@@ -1,5 +0,0 @@
-<HTML>
-<BODY>
-Provides APIs to interface with the SDK extensions.
-</BODY>
-</HTML>
diff --git a/apex/sdkextensions/gen_sdkinfo.py b/apex/sdkextensions/gen_sdkinfo.py
deleted file mode 100644
index 5af478b..0000000
--- a/apex/sdkextensions/gen_sdkinfo.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sdk_pb2
-import sys
-
-if __name__ == '__main__':
-  argv = sys.argv[1:]
-  if not len(argv) == 4 or sorted([argv[0], argv[2]]) != ['-o', '-v']:
-    print('usage: gen_sdkinfo -v <version> -o <output-file>')
-    sys.exit(1)
-
-  for i in range(len(argv)):
-    if sys.argv[i] == '-o':
-      filename = sys.argv[i+1]
-    if sys.argv[i] == '-v':
-      version = int(sys.argv[i+1])
-
-  proto = sdk_pb2.SdkVersion()
-  proto.version = version
-  with open(filename, 'wb') as f:
-    f.write(proto.SerializeToString())
diff --git a/apex/sdkextensions/manifest.json b/apex/sdkextensions/manifest.json
deleted file mode 100644
index deeb29e..0000000
--- a/apex/sdkextensions/manifest.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
-  "name": "com.android.sdkext",
-  "version": 300000000
-}
diff --git a/apex/sdkextensions/sdk.proto b/apex/sdkextensions/sdk.proto
deleted file mode 100644
index d15b935..0000000
--- a/apex/sdkextensions/sdk.proto
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * 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.
- */
-
-syntax = "proto3";
-package com.android.sdkext.proto;
-
-option java_outer_classname = "SdkProto";
-option optimize_for = LITE_RUNTIME;
-
-message SdkVersion {
-  int32 version = 1;
-}
diff --git a/apex/sdkextensions/testing/Android.bp b/apex/sdkextensions/testing/Android.bp
deleted file mode 100644
index f2f5b32..0000000
--- a/apex/sdkextensions/testing/Android.bp
+++ /dev/null
@@ -1,46 +0,0 @@
-// 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.
-
-apex_test {
-    name: "test_com.android.sdkext",
-    visibility: [ "//system/apex/tests" ],
-    defaults: ["com.android.sdkext-defaults"],
-    manifest: "test_manifest.json",
-    prebuilts: [ "sdkinfo_45" ],
-    file_contexts: ":com.android.sdkext-file_contexts",
-    installable: false, // Should never be installed on the systemimage
-    multilib: {
-        prefer32: {
-            binaries: ["derive_sdk_prefer32"],
-        },
-    },
-    // The automated test infra ends up building this apex for 64+32-bit and
-    // then installs it on a 32-bit-only device. Work around this weirdness
-    // by preferring 32-bit.
-    compile_multilib: "prefer32",
-}
-
-genrule {
-    name: "sdkinfo_45_src",
-    out: [ "sdkinfo.binarypb" ],
-    tools: [ "gen_sdkinfo" ],
-    cmd: "$(location) -v 45 -o $(out)",
-}
-
-prebuilt_etc {
-    name: "sdkinfo_45",
-    src: ":sdkinfo_45_src",
-    filename: "sdkinfo.binarypb",
-    installable: false,
-}
diff --git a/apex/sdkextensions/testing/test_manifest.json b/apex/sdkextensions/testing/test_manifest.json
deleted file mode 100644
index 1b4a2b0..0000000
--- a/apex/sdkextensions/testing/test_manifest.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
-  "name": "com.android.sdkext",
-  "version": 2147483647
-}
diff --git a/api/test-current.txt b/api/test-current.txt
index 5dc7bdb..a163dea 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -1728,6 +1728,15 @@
 
 }
 
+package android.media.tv {
+
+  public final class TvInputManager {
+    method public void addHardwareDevice(int);
+    method public void removeHardwareDevice(int);
+  }
+
+}
+
 package android.metrics {
 
   public class LogMaker {
diff --git a/cmds/bootanimation/BootAnimation.cpp b/cmds/bootanimation/BootAnimation.cpp
index ecb95bd..3bcabe5 100644
--- a/cmds/bootanimation/BootAnimation.cpp
+++ b/cmds/bootanimation/BootAnimation.cpp
@@ -48,6 +48,7 @@
 #include <ui/Region.h>
 
 #include <gui/ISurfaceComposer.h>
+#include <gui/DisplayEventReceiver.h>
 #include <gui/Surface.h>
 #include <gui/SurfaceComposerClient.h>
 
@@ -113,8 +114,8 @@
 // ---------------------------------------------------------------------------
 
 BootAnimation::BootAnimation(sp<Callbacks> callbacks)
-        : Thread(false), mClockEnabled(true), mTimeIsAccurate(false),
-        mTimeFormat12Hour(false), mTimeCheckThread(nullptr), mCallbacks(callbacks) {
+        : Thread(false), mClockEnabled(true), mTimeIsAccurate(false), mTimeFormat12Hour(false),
+        mTimeCheckThread(nullptr), mCallbacks(callbacks), mLooper(new Looper(false)) {
     mSession = new SurfaceComposerClient();
 
     std::string powerCtl = android::base::GetProperty("sys.powerctl", "");
@@ -154,8 +155,7 @@
     return mSession;
 }
 
-void BootAnimation::binderDied(const wp<IBinder>&)
-{
+void BootAnimation::binderDied(const wp<IBinder>&) {
     // woah, surfaceflinger died!
     SLOGD("SurfaceFlinger died, exiting...");
 
@@ -219,8 +219,7 @@
     return NO_ERROR;
 }
 
-status_t BootAnimation::initTexture(FileMap* map, int* width, int* height)
-{
+status_t BootAnimation::initTexture(FileMap* map, int* width, int* height) {
     SkBitmap bitmap;
     sk_sp<SkData> data = SkData::MakeWithoutCopy(map->getDataPtr(),
             map->getDataLength());
@@ -278,6 +277,78 @@
     return NO_ERROR;
 }
 
+class BootAnimation::DisplayEventCallback : public LooperCallback {
+    BootAnimation* mBootAnimation;
+
+public:
+    DisplayEventCallback(BootAnimation* bootAnimation) {
+        mBootAnimation = bootAnimation;
+    }
+
+    int handleEvent(int /* fd */, int events, void* /* data */) {
+        if (events & (Looper::EVENT_ERROR | Looper::EVENT_HANGUP)) {
+            ALOGE("Display event receiver pipe was closed or an error occurred. events=0x%x",
+                    events);
+            return 0; // remove the callback
+        }
+
+        if (!(events & Looper::EVENT_INPUT)) {
+            ALOGW("Received spurious callback for unhandled poll event.  events=0x%x", events);
+            return 1; // keep the callback
+        }
+
+        constexpr int kBufferSize = 100;
+        DisplayEventReceiver::Event buffer[kBufferSize];
+        ssize_t numEvents;
+        do {
+            numEvents = mBootAnimation->mDisplayEventReceiver->getEvents(buffer, kBufferSize);
+            for (size_t i = 0; i < static_cast<size_t>(numEvents); i++) {
+                const auto& event = buffer[i];
+                if (event.header.type == DisplayEventReceiver::DISPLAY_EVENT_HOTPLUG) {
+                    SLOGV("Hotplug received");
+
+                    if (!event.hotplug.connected) {
+                        // ignore hotplug disconnect
+                        continue;
+                    }
+                    auto token = SurfaceComposerClient::getPhysicalDisplayToken(
+                        event.header.displayId);
+
+                    if (token != mBootAnimation->mDisplayToken) {
+                        // ignore hotplug of a secondary display
+                        continue;
+                    }
+
+                    DisplayConfig displayConfig;
+                    const status_t error = SurfaceComposerClient::getActiveDisplayConfig(
+                        mBootAnimation->mDisplayToken, &displayConfig);
+                    if (error != NO_ERROR) {
+                        SLOGE("Can't get active display configuration.");
+                    }
+                    mBootAnimation->resizeSurface(displayConfig.resolution.getWidth(),
+                        displayConfig.resolution.getHeight());
+                }
+            }
+        } while (numEvents > 0);
+
+        return 1;  // keep the callback
+    }
+};
+
+EGLConfig BootAnimation::getEglConfig(const EGLDisplay& display) {
+    const EGLint attribs[] = {
+        EGL_RED_SIZE,   8,
+        EGL_GREEN_SIZE, 8,
+        EGL_BLUE_SIZE,  8,
+        EGL_DEPTH_SIZE, 0,
+        EGL_NONE
+    };
+    EGLint numConfigs;
+    EGLConfig config;
+    eglChooseConfig(display, attribs, &config, 1, &numConfigs);
+    return config;
+}
+
 status_t BootAnimation::readyToRun() {
     mAssets.addDefaultAssets();
 
@@ -346,25 +417,12 @@
     sp<Surface> s = control->getSurface();
 
     // initialize opengl and egl
-    const EGLint attribs[] = {
-            EGL_RED_SIZE,   8,
-            EGL_GREEN_SIZE, 8,
-            EGL_BLUE_SIZE,  8,
-            EGL_DEPTH_SIZE, 0,
-            EGL_NONE
-    };
-    EGLint w, h;
-    EGLint numConfigs;
-    EGLConfig config;
-    EGLSurface surface;
-    EGLContext context;
-
     EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
-
     eglInitialize(display, nullptr, nullptr);
-    eglChooseConfig(display, attribs, &config, 1, &numConfigs);
-    surface = eglCreateWindowSurface(display, config, s.get(), nullptr);
-    context = eglCreateContext(display, config, nullptr, nullptr);
+    EGLConfig config = getEglConfig(display);
+    EGLSurface surface = eglCreateWindowSurface(display, config, s.get(), nullptr);
+    EGLContext context = eglCreateContext(display, config, nullptr, nullptr);
+    EGLint w, h;
     eglQuerySurface(display, surface, EGL_WIDTH, &w);
     eglQuerySurface(display, surface, EGL_HEIGHT, &h);
 
@@ -380,9 +438,46 @@
     mFlingerSurface = s;
     mTargetInset = -1;
 
+    // Register a display event receiver
+    mDisplayEventReceiver = std::make_unique<DisplayEventReceiver>();
+    status_t status = mDisplayEventReceiver->initCheck();
+    SLOGE_IF(status != NO_ERROR, "Initialization of DisplayEventReceiver failed with status: %d",
+            status);
+    mLooper->addFd(mDisplayEventReceiver->getFd(), 0, Looper::EVENT_INPUT,
+            new DisplayEventCallback(this), nullptr);
+
     return NO_ERROR;
 }
 
+void BootAnimation::resizeSurface(int newWidth, int newHeight) {
+    // We assume this function is called on the animation thread.
+    if (newWidth == mWidth && newHeight == mHeight) {
+        return;
+    }
+    SLOGV("Resizing the boot animation surface to %d %d", newWidth, newHeight);
+
+    eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
+    eglDestroySurface(mDisplay, mSurface);
+
+    mWidth = newWidth;
+    mHeight = newHeight;
+
+    SurfaceComposerClient::Transaction t;
+    t.setSize(mFlingerSurfaceControl, mWidth, mHeight);
+    t.apply();
+
+    EGLConfig config = getEglConfig(mDisplay);
+    EGLSurface surface = eglCreateWindowSurface(mDisplay, config, mFlingerSurface.get(), nullptr);
+    if (eglMakeCurrent(mDisplay, surface, surface, mContext) == EGL_FALSE) {
+        SLOGE("Can't make the new surface current. Error %d", eglGetError());
+        return;
+    }
+    glViewport(0, 0, mWidth, mHeight);
+    glScissor(0, 0, mWidth, mHeight);
+
+    mSurface = surface;
+}
+
 bool BootAnimation::preloadAnimation() {
     findBootAnimationFile();
     if (!mZipFileName.isEmpty()) {
@@ -443,15 +538,14 @@
     }
 }
 
-bool BootAnimation::threadLoop()
-{
-    bool r;
+bool BootAnimation::threadLoop() {
+    bool result;
     // We have no bootanimation file, so we use the stock android logo
     // animation.
     if (mZipFileName.isEmpty()) {
-        r = android();
+        result = android();
     } else {
-        r = movie();
+        result = movie();
     }
 
     eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
@@ -462,11 +556,10 @@
     eglTerminate(mDisplay);
     eglReleaseThread();
     IPCThreadState::self()->stopProcess();
-    return r;
+    return result;
 }
 
-bool BootAnimation::android()
-{
+bool BootAnimation::android() {
     SLOGD("%sAnimationShownTiming start time: %" PRId64 "ms", mShuttingDown ? "Shutdown" : "Boot",
             elapsedRealtime());
     initTexture(&mAndroid[0], mAssets, "images/android-logo-mask.png");
@@ -485,19 +578,19 @@
     glEnable(GL_TEXTURE_2D);
     glTexEnvx(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
 
-    const GLint xc = (mWidth  - mAndroid[0].w) / 2;
-    const GLint yc = (mHeight - mAndroid[0].h) / 2;
-    const Rect updateRect(xc, yc, xc + mAndroid[0].w, yc + mAndroid[0].h);
-
-    glScissor(updateRect.left, mHeight - updateRect.bottom, updateRect.width(),
-            updateRect.height());
-
     // Blend state
     glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
     glTexEnvx(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
 
     const nsecs_t startTime = systemTime();
     do {
+        processDisplayEvents();
+        const GLint xc = (mWidth  - mAndroid[0].w) / 2;
+        const GLint yc = (mHeight - mAndroid[0].h) / 2;
+        const Rect updateRect(xc, yc, xc + mAndroid[0].w, yc + mAndroid[0].h);
+        glScissor(updateRect.left, mHeight - updateRect.bottom, updateRect.width(),
+                updateRect.height());
+
         nsecs_t now = systemTime();
         double time = now - startTime;
         float t = 4.0f * float(time / us2ns(16667)) / mAndroid[1].w;
@@ -612,8 +705,7 @@
 }
 
 
-static bool readFile(ZipFileRO* zip, const char* name, String8& outString)
-{
+static bool readFile(ZipFileRO* zip, const char* name, String8& outString) {
     ZipEntryRO entry = zip->findEntryByName(name);
     SLOGE_IF(!entry, "couldn't find %s", name);
     if (!entry) {
@@ -734,8 +826,7 @@
     drawText(out, font, false, &x, &y);
 }
 
-bool BootAnimation::parseAnimationDesc(Animation& animation)
-{
+bool BootAnimation::parseAnimationDesc(Animation& animation)  {
     String8 desString;
 
     if (!readFile(animation.zip, "desc.txt", desString)) {
@@ -802,8 +893,7 @@
     return true;
 }
 
-bool BootAnimation::preloadZip(Animation& animation)
-{
+bool BootAnimation::preloadZip(Animation& animation) {
     // read all the data structures
     const size_t pcount = animation.parts.size();
     void *cookie = nullptr;
@@ -900,8 +990,7 @@
     return true;
 }
 
-bool BootAnimation::movie()
-{
+bool BootAnimation::movie() {
     if (mAnimation == nullptr) {
         mAnimation = loadAnimation(mZipFileName);
     }
@@ -987,12 +1076,9 @@
     return false;
 }
 
-bool BootAnimation::playAnimation(const Animation& animation)
-{
+bool BootAnimation::playAnimation(const Animation& animation) {
     const size_t pcount = animation.parts.size();
     nsecs_t frameDuration = s2ns(1) / animation.fps;
-    const int animationX = (mWidth - animation.width) / 2;
-    const int animationY = (mHeight - animation.height) / 2;
 
     SLOGD("%sAnimationShownTiming start time: %" PRId64 "ms", mShuttingDown ? "Shutdown" : "Boot",
             elapsedRealtime());
@@ -1023,6 +1109,11 @@
                     1.0f);
 
             for (size_t j=0 ; j<fcount && (!exitPending() || part.playUntilComplete) ; j++) {
+                processDisplayEvents();
+
+                const int animationX = (mWidth - animation.width) / 2;
+                const int animationY = (mHeight - animation.height) / 2;
+
                 const Animation::Frame& frame(part.frames[j]);
                 nsecs_t lastFrame = systemTime();
 
@@ -1106,6 +1197,12 @@
     return true;
 }
 
+void BootAnimation::processDisplayEvents() {
+    // This will poll mDisplayEventReceiver and if there are new events it'll call
+    // displayEventCallback synchronously.
+    mLooper->pollOnce(0);
+}
+
 void BootAnimation::handleViewport(nsecs_t timestep) {
     if (mShuttingDown || !mFlingerSurfaceControl || mTargetInset == 0) {
         return;
@@ -1148,8 +1245,7 @@
     mCurrentInset += delta;
 }
 
-void BootAnimation::releaseAnimation(Animation* animation) const
-{
+void BootAnimation::releaseAnimation(Animation* animation) const {
     for (Vector<Animation::Part>::iterator it = animation->parts.begin(),
          e = animation->parts.end(); it != e; ++it) {
         if (it->animation)
@@ -1160,8 +1256,7 @@
     delete animation;
 }
 
-BootAnimation::Animation* BootAnimation::loadAnimation(const String8& fn)
-{
+BootAnimation::Animation* BootAnimation::loadAnimation(const String8& fn) {
     if (mLoadedFiles.indexOf(fn) >= 0) {
         SLOGE("File \"%s\" is already loaded. Cyclic ref is not allowed",
             fn.string());
@@ -1323,5 +1418,4 @@
 
 // ---------------------------------------------------------------------------
 
-}
-; // namespace android
+} // namespace android
diff --git a/cmds/bootanimation/BootAnimation.h b/cmds/bootanimation/BootAnimation.h
index 574d65e..36cd91b 100644
--- a/cmds/bootanimation/BootAnimation.h
+++ b/cmds/bootanimation/BootAnimation.h
@@ -18,11 +18,14 @@
 #define ANDROID_BOOTANIMATION_H
 
 #include <vector>
+#include <queue>
 
 #include <stdint.h>
 #include <sys/types.h>
 
 #include <androidfw/AssetManager.h>
+#include <gui/DisplayEventReceiver.h>
+#include <utils/Looper.h>
 #include <utils/Thread.h>
 #include <binder/IBinder.h>
 
@@ -145,6 +148,11 @@
         BootAnimation* mBootAnimation;
     };
 
+    // Display event handling
+    class DisplayEventCallback;
+    int displayEventCallback(int fd, int events, void* data);
+    void processDisplayEvents();
+
     status_t initTexture(Texture* texture, AssetManager& asset, const char* name);
     status_t initTexture(FileMap* map, int* width, int* height);
     status_t initFont(Font* font, const char* fallback);
@@ -161,6 +169,8 @@
     void findBootAnimationFile();
     bool findBootAnimationFileInternal(const std::vector<std::string>& files);
     bool preloadAnimation();
+    EGLConfig getEglConfig(const EGLDisplay&);
+    void resizeSurface(int newWidth, int newHeight);
 
     void checkExit();
 
@@ -189,6 +199,8 @@
     sp<TimeCheckThread> mTimeCheckThread = nullptr;
     sp<Callbacks> mCallbacks;
     Animation* mAnimation = nullptr;
+    std::unique_ptr<DisplayEventReceiver> mDisplayEventReceiver;
+    sp<Looper> mLooper;
 };
 
 // ---------------------------------------------------------------------------
diff --git a/cmds/statsd/Android.bp b/cmds/statsd/Android.bp
index f30ed17c..3dbe413 100644
--- a/cmds/statsd/Android.bp
+++ b/cmds/statsd/Android.bp
@@ -292,6 +292,7 @@
     name: "statsd_test",
     defaults: ["statsd_defaults"],
     test_suites: ["device-tests", "mts"],
+    test_config: "statsd_test.xml",
 
     //TODO(b/153588990): Remove when the build system properly separates
     //32bit and 64bit architectures.
@@ -299,7 +300,10 @@
     multilib: {
         lib64: {
             suffix: "64",
-        }
+        },
+        lib32: {
+            suffix: "32",
+        },
     },
 
     cflags: [
diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto
index c6e9e01..62e1567 100644
--- a/cmds/statsd/src/atoms.proto
+++ b/cmds/statsd/src/atoms.proto
@@ -6713,11 +6713,11 @@
  * Logs when a data stall event occurs.
  *
  * Log from:
- *     frameworks/base/services/core/java/com/android/server/connectivity/NetworkMonitor.java
+ *     packages/modules/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java
  */
 message DataStallEvent {
     // Data stall evaluation type.
-    // See frameworks/base/services/core/java/com/android/server/connectivity/NetworkMonitor.java
+    // See packages/modules/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java
     // Refer to the definition of DATA_STALL_EVALUATION_TYPE_*.
     optional int32 evaluation_type = 1;
     // See definition in data_stall_event.proto.
@@ -6730,6 +6730,10 @@
     optional com.android.server.connectivity.CellularData cell_info = 5 [(log_mode) = MODE_BYTES];
     // See definition in data_stall_event.proto.
     optional com.android.server.connectivity.DnsEvent dns_event = 6 [(log_mode) = MODE_BYTES];
+    // The tcp packets fail rate from the latest tcp polling.
+    optional int32 tcp_fail_rate = 7;
+    // Number of packets sent since the last received packet.
+    optional int32 tcp_sent_since_last_recv = 8;
 }
 
 /*
diff --git a/cmds/statsd/src/metrics/MetricProducer.h b/cmds/statsd/src/metrics/MetricProducer.h
index e86fdf0..673f668 100644
--- a/cmds/statsd/src/metrics/MetricProducer.h
+++ b/cmds/statsd/src/metrics/MetricProducer.h
@@ -82,7 +82,9 @@
     DIMENSION_GUARDRAIL_REACHED = 6,
     MULTIPLE_BUCKETS_SKIPPED = 7,
     // Not an invalid bucket case, but the bucket is dropped.
-    BUCKET_TOO_SMALL = 8
+    BUCKET_TOO_SMALL = 8,
+    // Not an invalid bucket case, but the bucket is skipped.
+    NO_DATA = 9
 };
 
 struct Activation {
diff --git a/cmds/statsd/src/metrics/ValueMetricProducer.cpp b/cmds/statsd/src/metrics/ValueMetricProducer.cpp
index f03ce45..dbec24b 100644
--- a/cmds/statsd/src/metrics/ValueMetricProducer.cpp
+++ b/cmds/statsd/src/metrics/ValueMetricProducer.cpp
@@ -108,7 +108,7 @@
       mSkipZeroDiffOutput(metric.skip_zero_diff_output()),
       mUseZeroDefaultBase(metric.use_zero_default_base()),
       mHasGlobalBase(false),
-      mCurrentBucketIsInvalid(false),
+      mCurrentBucketIsSkipped(false),
       mMaxPullDelayNs(metric.max_pull_delay_sec() > 0 ? metric.max_pull_delay_sec() * NS_PER_SEC
                                                       : StatsdStats::kPullMaxDelayNs),
       mSplitBucketForAppUpgrade(metric.split_bucket_for_app_upgrade()),
@@ -383,15 +383,12 @@
 
 void ValueMetricProducer::invalidateCurrentBucketWithoutResetBase(const int64_t dropTimeNs,
                                                                   const BucketDropReason reason) {
-    if (!mCurrentBucketIsInvalid) {
+    if (!mCurrentBucketIsSkipped) {
         // Only report to StatsdStats once per invalid bucket.
         StatsdStats::getInstance().noteInvalidatedBucket(mMetricId);
     }
 
-    if (!maxDropEventsReached()) {
-        mCurrentSkippedBucket.dropEvents.emplace_back(buildDropEvent(dropTimeNs, reason));
-    }
-    mCurrentBucketIsInvalid = true;
+    skipCurrentBucket(dropTimeNs, reason);
 }
 
 void ValueMetricProducer::invalidateCurrentBucket(const int64_t dropTimeNs,
@@ -400,6 +397,14 @@
     resetBase();
 }
 
+void ValueMetricProducer::skipCurrentBucket(const int64_t dropTimeNs,
+                                            const BucketDropReason reason) {
+    if (!maxDropEventsReached()) {
+        mCurrentSkippedBucket.dropEvents.emplace_back(buildDropEvent(dropTimeNs, reason));
+    }
+    mCurrentBucketIsSkipped = true;
+}
+
 void ValueMetricProducer::resetBase() {
     for (auto& slice : mCurrentBaseInfo) {
         for (auto& baseInfo : slice.second) {
@@ -961,12 +966,10 @@
     int64_t conditionTrueDuration = mConditionTimer.newBucketStart(bucketEndTime);
     bool isBucketLargeEnough = bucketEndTime - mCurrentBucketStartTimeNs >= mMinBucketSizeNs;
     if (!isBucketLargeEnough) {
-        if (!maxDropEventsReached()) {
-            mCurrentSkippedBucket.dropEvents.emplace_back(
-                    buildDropEvent(eventTimeNs, BucketDropReason::BUCKET_TOO_SMALL));
-        }
+        skipCurrentBucket(eventTimeNs, BucketDropReason::BUCKET_TOO_SMALL);
     }
-    if (isBucketLargeEnough && !mCurrentBucketIsInvalid) {
+    bool bucketHasData = false;
+    if (!mCurrentBucketIsSkipped) {
         // The current bucket is large enough to keep.
         for (const auto& slice : mCurrentSlicedBucket) {
             ValueBucket bucket = buildPartialBucket(bucketEndTime, slice.second);
@@ -975,14 +978,33 @@
             if (bucket.valueIndex.size() > 0) {
                 auto& bucketList = mPastBuckets[slice.first];
                 bucketList.push_back(bucket);
+                bucketHasData = true;
             }
         }
-    } else {
+    }
+
+    if (!bucketHasData && !mCurrentBucketIsSkipped) {
+        skipCurrentBucket(eventTimeNs, BucketDropReason::NO_DATA);
+    }
+
+    if (mCurrentBucketIsSkipped) {
         mCurrentSkippedBucket.bucketStartTimeNs = mCurrentBucketStartTimeNs;
-        mCurrentSkippedBucket.bucketEndTimeNs = bucketEndTime;
+        // Fill in the gap if we skipped multiple buckets.
+        mCurrentSkippedBucket.bucketEndTimeNs =
+                numBucketsForward > 1 ? nextBucketStartTimeNs : bucketEndTime;
         mSkippedBuckets.emplace_back(mCurrentSkippedBucket);
     }
 
+    // This means that the current bucket was not flushed before a forced bucket split.
+    if (bucketEndTime < nextBucketStartTimeNs && numBucketsForward <= 1) {
+        SkippedBucket bucketInGap;
+        bucketInGap.bucketStartTimeNs = bucketEndTime;
+        bucketInGap.bucketEndTimeNs = nextBucketStartTimeNs;
+        bucketInGap.dropEvents.emplace_back(
+                buildDropEvent(eventTimeNs, BucketDropReason::NO_DATA));
+        mSkippedBuckets.emplace_back(bucketInGap);
+    }
+
     appendToFullBucket(eventTimeNs, fullBucketEndTimeNs);
     initCurrentSlicedBucket(nextBucketStartTimeNs);
     // Update the condition timer again, in case we skipped buckets.
@@ -1036,13 +1058,13 @@
         // TODO: remove mCurrentBaseInfo entries when obsolete
     }
 
-    mCurrentBucketIsInvalid = false;
+    mCurrentBucketIsSkipped = false;
     mCurrentSkippedBucket.reset();
 
     // If we do not have a global base when the condition is true,
     // we will have incomplete bucket for the next bucket.
     if (mUseDiff && !mHasGlobalBase && mCondition) {
-        mCurrentBucketIsInvalid = false;
+        mCurrentBucketIsSkipped = false;
     }
     mCurrentBucketStartTimeNs = nextBucketStartTimeNs;
     VLOG("metric %lld: new bucket start time: %lld", (long long)mMetricId,
@@ -1051,7 +1073,7 @@
 
 void ValueMetricProducer::appendToFullBucket(int64_t eventTimeNs, int64_t fullBucketEndTimeNs) {
     bool isFullBucketReached = eventTimeNs > fullBucketEndTimeNs;
-    if (mCurrentBucketIsInvalid) {
+    if (mCurrentBucketIsSkipped) {
         if (isFullBucketReached) {
             // If the bucket is invalid, we ignore the full bucket since it contains invalid data.
             mCurrentFullBucket.clear();
diff --git a/cmds/statsd/src/metrics/ValueMetricProducer.h b/cmds/statsd/src/metrics/ValueMetricProducer.h
index 751fef2..bb4a661 100644
--- a/cmds/statsd/src/metrics/ValueMetricProducer.h
+++ b/cmds/statsd/src/metrics/ValueMetricProducer.h
@@ -144,6 +144,10 @@
     void invalidateCurrentBucket(const int64_t dropTimeNs, const BucketDropReason reason);
     void invalidateCurrentBucketWithoutResetBase(const int64_t dropTimeNs,
                                                  const BucketDropReason reason);
+    // Skips the current bucket without notifying StatsdStats of the skipped bucket.
+    // This should only be called from #flushCurrentBucketLocked. Otherwise, a future event that
+    // causes the bucket to be invalidated will not notify StatsdStats.
+    void skipCurrentBucket(const int64_t dropTimeNs, const BucketDropReason reason);
 
     const int mWhatMatcherIndex;
 
@@ -250,11 +254,9 @@
     // diff against.
     bool mHasGlobalBase;
 
-    // Invalid bucket. There was a problem in collecting data in the current bucket so we cannot
-    // trust any of the data in this bucket.
-    //
-    // For instance, one pull failed.
-    bool mCurrentBucketIsInvalid;
+    // This is to track whether or not the bucket is skipped for any of the reasons listed in
+    // BucketDropReason, many of which make the bucket potentially invalid.
+    bool mCurrentBucketIsSkipped;
 
     const int64_t mMaxPullDelayNs;
 
diff --git a/cmds/statsd/src/stats_log.proto b/cmds/statsd/src/stats_log.proto
index 1121392..6bfa267 100644
--- a/cmds/statsd/src/stats_log.proto
+++ b/cmds/statsd/src/stats_log.proto
@@ -212,6 +212,8 @@
       MULTIPLE_BUCKETS_SKIPPED = 7;
       // Not an invalid bucket case, but the bucket is dropped.
       BUCKET_TOO_SMALL = 8;
+      // Not an invalid bucket case, but the bucket is skipped.
+      NO_DATA = 9;
   };
 
   message DropEvent {
diff --git a/cmds/statsd/statsd_test.xml b/cmds/statsd/statsd_test.xml
new file mode 100644
index 0000000..8f9bb1c
--- /dev/null
+++ b/cmds/statsd/statsd_test.xml
@@ -0,0 +1,37 @@
+<?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.
+-->
+<configuration description="Runs statsd_test.">
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="apct-native" />
+    <option name="test-suite-tag" value="mts" />
+
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+       <option name="cleanup" value="true" />
+       <option name="push" value="statsd_test->/data/local/tmp/statsd_test" />
+       <option name="append-bitness" value="true" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.GTest" >
+        <option name="native-test-device-path" value="/data/local/tmp" />
+        <option name="module-name" value="statsd_test" />
+    </test>
+
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <option name="mainline-module-package-name" value="com.google.android.os.statsd" />
+    </object>
+</configuration>
diff --git a/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp b/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp
index 474aa22..14246cab 100644
--- a/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp
+++ b/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp
@@ -1115,13 +1115,21 @@
     EXPECT_EQ(false, curInterval.hasValue);
     assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {12}, {bucketSizeNs},
                                     {bucket2StartTimeNs}, {bucket3StartTimeNs});
+    // The 1st bucket is dropped because of no data
     // 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(2, valueProducer->mSkippedBuckets.size());
+
+    EXPECT_EQ(bucketStartTimeNs, valueProducer->mSkippedBuckets[0].bucketStartTimeNs);
+    EXPECT_EQ(bucket2StartTimeNs, 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);
+    EXPECT_EQ(NO_DATA, valueProducer->mSkippedBuckets[0].dropEvents[0].reason);
+    EXPECT_EQ(bucket2StartTimeNs, valueProducer->mSkippedBuckets[0].dropEvents[0].dropTimeNs);
+
+    EXPECT_EQ(bucket3StartTimeNs, valueProducer->mSkippedBuckets[1].bucketStartTimeNs);
+    EXPECT_EQ(bucket6StartTimeNs, valueProducer->mSkippedBuckets[1].bucketEndTimeNs);
+    ASSERT_EQ(1, valueProducer->mSkippedBuckets[1].dropEvents.size());
+    EXPECT_EQ(MULTIPLE_BUCKETS_SKIPPED, valueProducer->mSkippedBuckets[1].dropEvents[0].reason);
+    EXPECT_EQ(bucket6StartTimeNs, valueProducer->mSkippedBuckets[1].dropEvents[0].dropTimeNs);
 }
 
 /*
@@ -2214,7 +2222,7 @@
     valueProducer->mCondition = ConditionState::kFalse;
 
     valueProducer->onConditionChanged(true, bucketStartTimeNs + 2);
-    EXPECT_EQ(true, valueProducer->mCurrentBucketIsInvalid);
+    EXPECT_EQ(true, valueProducer->mCurrentBucketIsSkipped);
     ASSERT_EQ(0UL, valueProducer->mCurrentSlicedBucket.size());
     ASSERT_EQ(0UL, valueProducer->mSkippedBuckets.size());
 
@@ -2629,13 +2637,17 @@
 
     vector<shared_ptr<LogEvent>> allData;
     allData.push_back(CreateRepeatedValueLogEvent(tagId, bucket3StartTimeNs + 1, 4));
+    // Pull fails and arrives late.
     valueProducer->onDataPulled(allData, /** fails */ false, bucket3StartTimeNs + 1);
     assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {9},
                                     {partialBucketSplitTimeNs - bucketStartTimeNs},
                                     {bucketStartTimeNs}, {partialBucketSplitTimeNs});
     ASSERT_EQ(1, valueProducer->mSkippedBuckets.size());
+    ASSERT_EQ(2, valueProducer->mSkippedBuckets[0].dropEvents.size());
+    EXPECT_EQ(PULL_FAILED, valueProducer->mSkippedBuckets[0].dropEvents[0].reason);
+    EXPECT_EQ(MULTIPLE_BUCKETS_SKIPPED, valueProducer->mSkippedBuckets[0].dropEvents[1].reason);
     EXPECT_EQ(partialBucketSplitTimeNs, valueProducer->mSkippedBuckets[0].bucketStartTimeNs);
-    EXPECT_EQ(bucket2StartTimeNs, valueProducer->mSkippedBuckets[0].bucketEndTimeNs);
+    EXPECT_EQ(bucket3StartTimeNs, valueProducer->mSkippedBuckets[0].bucketEndTimeNs);
     ASSERT_EQ(0UL, valueProducer->mCurrentFullBucket.size());
 }
 
@@ -3464,26 +3476,41 @@
     // Condition change event that skips forward by three buckets.
     valueProducer->onConditionChanged(false, bucket4StartTimeNs + 10);
 
+    int64_t dumpTimeNs = bucket4StartTimeNs + 1000;
+
     // Check dump report.
     ProtoOutputStream output;
     std::set<string> strSet;
-    valueProducer->onDumpReport(bucket4StartTimeNs + 1000, true /* include recent buckets */, true,
+    valueProducer->onDumpReport(dumpTimeNs, true /* include current buckets */, true,
                                 NO_TIME_CONSTRAINTS /* dumpLatency */, &strSet, &output);
 
     StatsLogReport report = outputStreamToProto(&output);
     EXPECT_TRUE(report.has_value_metrics());
     ASSERT_EQ(0, report.value_metrics().data_size());
-    ASSERT_EQ(1, report.value_metrics().skipped_size());
+    ASSERT_EQ(2, report.value_metrics().skipped_size());
 
     EXPECT_EQ(NanoToMillis(bucketStartTimeNs),
               report.value_metrics().skipped(0).start_bucket_elapsed_millis());
-    EXPECT_EQ(NanoToMillis(bucket2StartTimeNs),
+    EXPECT_EQ(NanoToMillis(bucket4StartTimeNs),
               report.value_metrics().skipped(0).end_bucket_elapsed_millis());
     ASSERT_EQ(1, report.value_metrics().skipped(0).drop_event_size());
 
     auto dropEvent = report.value_metrics().skipped(0).drop_event(0);
     EXPECT_EQ(BucketDropReason::MULTIPLE_BUCKETS_SKIPPED, dropEvent.drop_reason());
     EXPECT_EQ(NanoToMillis(bucket4StartTimeNs + 10), dropEvent.drop_time_millis());
+
+    // This bucket is skipped because a dumpReport with include current buckets is called.
+    // This creates a new bucket from bucket4StartTimeNs to dumpTimeNs in which we have no data
+    // since the condition is false for the entire bucket interval.
+    EXPECT_EQ(NanoToMillis(bucket4StartTimeNs),
+              report.value_metrics().skipped(1).start_bucket_elapsed_millis());
+    EXPECT_EQ(NanoToMillis(dumpTimeNs),
+              report.value_metrics().skipped(1).end_bucket_elapsed_millis());
+    ASSERT_EQ(1, report.value_metrics().skipped(1).drop_event_size());
+
+    dropEvent = report.value_metrics().skipped(1).drop_event(0);
+    EXPECT_EQ(BucketDropReason::NO_DATA, dropEvent.drop_reason());
+    EXPECT_EQ(NanoToMillis(dumpTimeNs), dropEvent.drop_time_millis());
 }
 
 /*
@@ -3544,6 +3571,89 @@
 }
 
 /*
+ * Test that NO_DATA dump reason is logged when a flushed bucket contains no data.
+ */
+TEST(ValueMetricProducerTest_BucketDrop, TestBucketDropWhenDataUnavailable) {
+    ValueMetric metric = ValueMetricProducerTestHelper::createMetricWithCondition();
+
+    sp<MockStatsPullerManager> pullerManager = new StrictMock<MockStatsPullerManager>();
+
+    sp<ValueMetricProducer> valueProducer =
+            ValueMetricProducerTestHelper::createValueProducerWithCondition(pullerManager, metric);
+
+    // Check dump report.
+    ProtoOutputStream output;
+    std::set<string> strSet;
+    int64_t dumpReportTimeNs = bucketStartTimeNs + 10000000000; // 10 seconds
+    valueProducer->onDumpReport(dumpReportTimeNs, true /* include current bucket */, true,
+                                NO_TIME_CONSTRAINTS /* dumpLatency */, &strSet, &output);
+
+    StatsLogReport report = outputStreamToProto(&output);
+    EXPECT_TRUE(report.has_value_metrics());
+    ASSERT_EQ(0, report.value_metrics().data_size());
+    ASSERT_EQ(1, report.value_metrics().skipped_size());
+
+    EXPECT_EQ(NanoToMillis(bucketStartTimeNs),
+              report.value_metrics().skipped(0).start_bucket_elapsed_millis());
+    EXPECT_EQ(NanoToMillis(dumpReportTimeNs),
+              report.value_metrics().skipped(0).end_bucket_elapsed_millis());
+    ASSERT_EQ(1, report.value_metrics().skipped(0).drop_event_size());
+
+    auto dropEvent = report.value_metrics().skipped(0).drop_event(0);
+    EXPECT_EQ(BucketDropReason::NO_DATA, dropEvent.drop_reason());
+    EXPECT_EQ(NanoToMillis(dumpReportTimeNs), dropEvent.drop_time_millis());
+}
+
+/*
+ * Test that a skipped bucket is logged when a forced bucket split occurs when the previous bucket
+ * was not flushed in time.
+ */
+TEST(ValueMetricProducerTest_BucketDrop, TestBucketDropWhenForceBucketSplitBeforeBucketFlush) {
+    ValueMetric metric = ValueMetricProducerTestHelper::createMetricWithCondition();
+
+    sp<MockStatsPullerManager> pullerManager = new StrictMock<MockStatsPullerManager>();
+
+    sp<ValueMetricProducer> valueProducer =
+            ValueMetricProducerTestHelper::createValueProducerWithCondition(pullerManager, metric);
+
+    // App update event.
+    int64_t appUpdateTimeNs = bucket2StartTimeNs + 1000;
+    valueProducer->notifyAppUpgrade(appUpdateTimeNs);
+
+    // Check dump report.
+    ProtoOutputStream output;
+    std::set<string> strSet;
+    int64_t dumpReportTimeNs = bucket2StartTimeNs + 10000000000; // 10 seconds
+    valueProducer->onDumpReport(dumpReportTimeNs, false /* include current buckets */, true,
+                                NO_TIME_CONSTRAINTS /* dumpLatency */, &strSet, &output);
+
+    StatsLogReport report = outputStreamToProto(&output);
+    EXPECT_TRUE(report.has_value_metrics());
+    ASSERT_EQ(0, report.value_metrics().data_size());
+    ASSERT_EQ(2, report.value_metrics().skipped_size());
+
+    EXPECT_EQ(NanoToMillis(bucketStartTimeNs),
+              report.value_metrics().skipped(0).start_bucket_elapsed_millis());
+    EXPECT_EQ(NanoToMillis(bucket2StartTimeNs),
+              report.value_metrics().skipped(0).end_bucket_elapsed_millis());
+    ASSERT_EQ(1, report.value_metrics().skipped(0).drop_event_size());
+
+    auto dropEvent = report.value_metrics().skipped(0).drop_event(0);
+    EXPECT_EQ(BucketDropReason::NO_DATA, dropEvent.drop_reason());
+    EXPECT_EQ(NanoToMillis(appUpdateTimeNs), dropEvent.drop_time_millis());
+
+    EXPECT_EQ(NanoToMillis(bucket2StartTimeNs),
+              report.value_metrics().skipped(1).start_bucket_elapsed_millis());
+    EXPECT_EQ(NanoToMillis(appUpdateTimeNs),
+              report.value_metrics().skipped(1).end_bucket_elapsed_millis());
+    ASSERT_EQ(1, report.value_metrics().skipped(1).drop_event_size());
+
+    dropEvent = report.value_metrics().skipped(1).drop_event(0);
+    EXPECT_EQ(BucketDropReason::NO_DATA, dropEvent.drop_reason());
+    EXPECT_EQ(NanoToMillis(appUpdateTimeNs), dropEvent.drop_time_millis());
+}
+
+/*
  * Test multiple bucket drop events in the same bucket.
  */
 TEST(ValueMetricProducerTest_BucketDrop, TestMultipleBucketDropEvents) {
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 8e43ca3..cffa59c 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -604,11 +604,8 @@
                     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, false /* alwaysReportChange */);
+                        newDisplayId);
             };
         }
 
@@ -4520,8 +4517,7 @@
         // 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,
-                        false /* alwaysReportChange */);
+                performConfigurationChangedForActivity(r, r.newConfig);
                 if (DEBUG_CONFIGURATION) {
                     Slog.v(TAG, "Resuming activity " + r.activityInfo.name + " with newConfig "
                             + r.activity.mCurrentConfig);
@@ -4841,8 +4837,7 @@
                     }
                 }
                 if (r.newConfig != null) {
-                    performConfigurationChangedForActivity(r, r.newConfig,
-                            false /* alwaysReportChange */);
+                    performConfigurationChangedForActivity(r, r.newConfig);
                     if (DEBUG_CONFIGURATION) Slog.v(TAG, "Updating activity vis "
                             + r.activityInfo.name + " with new config "
                             + r.activity.mCurrentConfig);
@@ -5510,12 +5505,11 @@
      * @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, boolean alwaysReportChange) {
+            Configuration newBaseConfig) {
         performConfigurationChangedForActivity(r, newBaseConfig, r.activity.getDisplayId(),
-                false /* movedToDifferentDisplay */, alwaysReportChange);
+                false /* movedToDifferentDisplay */);
     }
 
     /**
@@ -5528,19 +5522,16 @@
      *                      {@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,
-            boolean alwaysReportChange) {
+            Configuration newBaseConfig, int displayId, boolean movedToDifferentDisplay) {
         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,
-                alwaysReportChange);
+                r.tmpConfig, r.overrideConfig, displayId, movedToDifferentDisplay);
         freeTextLayoutCachesIfNeeded(r.activity.mCurrentConfig.diff(r.tmpConfig));
         return reportedConfig;
     }
@@ -5596,12 +5587,11 @@
      *                         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 alwaysReportChange) {
+            boolean movedToDifferentDisplay) {
         if (activity == null) {
             throw new IllegalArgumentException("No activity provided.");
         }
@@ -5614,31 +5604,27 @@
         // callback, see also PinnedStackTests#testConfigurationChangeOrderDuringTransition
         handleWindowingModeChangeIfNeeded(activity, newConfig);
 
-        boolean shouldChangeConfig = false;
+        boolean shouldReportChange = false;
         if (activity.mCurrentConfig == null) {
-            shouldChangeConfig = true;
+            shouldReportChange = true;
         } else {
             // If the new config is the same as the config this Activity is already running with and
             // the override config also didn't change, then don't bother calling
             // onConfigurationChanged.
             final int diff = activity.mCurrentConfig.diffPublicOnly(newConfig);
-
-            if (diff != 0 || !mResourcesManager.isSameResourcesOverrideConfig(activityToken,
+            if (diff == 0 && !movedToDifferentDisplay
+                    && mResourcesManager.isSameResourcesOverrideConfig(activityToken,
                     amOverrideConfig)) {
-                // 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 (alwaysReportChange
-                        || (~activity.mActivityInfo.getRealConfigChanged() & diff) == 0
-                        || !REPORT_TO_ACTIVITY) {
-                    shouldChangeConfig = true;
-                }
+                // Nothing significant, don't proceed with updating and reporting.
+                return null;
+            } else if ((~activity.mActivityInfo.getRealConfigChanged() & diff) == 0
+                    || !REPORT_TO_ACTIVITY) {
+                // If this activity doesn't handle any of the config changes, then don't bother
+                // calling onConfigurationChanged. Otherwise, report to the activity for the
+                // changes.
+                shouldReportChange = true;
             }
         }
-        if (!shouldChangeConfig && !movedToDifferentDisplay) {
-            // Nothing significant, don't proceed with updating and reporting.
-            return null;
-        }
 
         // Propagate the configuration change to ResourcesManager and Activity.
 
@@ -5675,7 +5661,7 @@
             activity.dispatchMovedToDisplay(displayId, configToReport);
         }
 
-        if (shouldChangeConfig) {
+        if (shouldReportChange) {
             activity.mCalled = false;
             activity.onConfigurationChanged(configToReport);
             if (!activity.mCalled) {
@@ -5792,7 +5778,7 @@
                     // config and avoid onConfigurationChanged if it hasn't changed.
                     Activity a = (Activity) cb;
                     performConfigurationChangedForActivity(mActivities.get(a.getActivityToken()),
-                            config, false /* alwaysReportChange */);
+                            config);
                 } else if (!equivalent) {
                     performConfigurationChanged(cb, config);
                 } else {
@@ -5943,18 +5929,6 @@
         }
     }
 
-    @Override
-    public void handleActivityConfigurationChanged(IBinder activityToken,
-            @NonNull 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. This method is a noop
      * if {@link #updatePendingActivityConfiguration(IBinder, Configuration)} has been called with
@@ -5964,10 +5938,10 @@
      * @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.
      */
-    void handleActivityConfigurationChanged(IBinder activityToken,
-            @NonNull Configuration overrideConfig, int displayId, boolean alwaysReportChange) {
+    @Override
+    public void handleActivityConfigurationChanged(IBinder activityToken,
+            @NonNull Configuration overrideConfig, int displayId) {
         ActivityClientRecord r = mActivities.get(activityToken);
         // Check input params.
         if (r == null || r.activity == null) {
@@ -6010,15 +5984,14 @@
                     + ", config=" + overrideConfig);
 
             final Configuration reportedConfig = performConfigurationChangedForActivity(r,
-                    mCompatConfiguration, displayId, true /* movedToDifferentDisplay */,
-                    alwaysReportChange);
+                    mCompatConfiguration, displayId, true /* movedToDifferentDisplay */);
             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, alwaysReportChange);
+            performConfigurationChangedForActivity(r, mCompatConfiguration);
         }
         // 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/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 2f488cd..c577d0e 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -2032,8 +2032,16 @@
     /**
      * Feature for {@link #getSystemAvailableFeatures} and
      * {@link #hasSystemFeature}: The device's main front and back cameras can stream
-     * concurrently as described in  {@link
-     * android.hardware.camera2.CameraManager#getConcurrentCameraIds()}
+     * concurrently as described in {@link
+     * android.hardware.camera2.CameraManager#getConcurrentCameraIds()}.
+     * </p>
+     * <p>While {@link android.hardware.camera2.CameraManager#getConcurrentCameraIds()} and
+     * associated APIs are only available on API level 30 or newer, this feature flag may be
+     * advertised by devices on API levels below 30. If present on such a device, the same
+     * guarantees hold: The main front and main back camera can be used at the same time, with
+     * guaranteed stream configurations as defined in the table for concurrent streaming at
+     * {@link android.hardware.camera2.CameraDevice#createCaptureSession(android.hardware.camera2.params.SessionConfiguration)}.
+     * </p>
      */
     @SdkConstant(SdkConstantType.FEATURE)
     public static final String FEATURE_CAMERA_CONCURRENT = "android.hardware.camera.concurrent";
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index bf105ce..2a6d6ed 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -230,13 +230,14 @@
     public static final String DISALLOW_MODIFY_ACCOUNTS = "no_modify_accounts";
 
     /**
-     * Specifies if a user is disallowed from changing Wi-Fi
-     * access points. The default value is <code>false</code>.
-     * <p>
-     * Device owner and profile owner can set this restriction, although the restriction has no
-     * effect in a managed profile. When it is set by the profile owner of an organization-owned
-     * managed profile on the parent profile, it will disallow the personal user from changing
-     * Wi-Fi access points.
+     * Specifies if a user is disallowed from changing Wi-Fi access points via Settings.
+     *
+     * <p>A device owner and a profile owner can set this restriction, although the restriction has
+     * no effect in a managed profile. When it is set by a device owner, a profile owner on the
+     * primary user or by a profile owner of an organization-owned managed profile on the parent
+     * profile, it disallows the primary user from changing Wi-Fi access points.
+     *
+     * <p>The default value is <code>false</code>.
      *
      * <p>Key for user restrictions.
      * <p>Type: Boolean
@@ -285,14 +286,16 @@
 
     /**
      * Specifies if a user is disallowed from turning on location sharing.
-     * The default value is <code>false</code>.
-     * <p>
-     * In a managed profile, location sharing always reflects the primary user's setting, but
+     *
+     * <p>In a managed profile, location sharing by default reflects the primary user's setting, but
      * can be overridden and forced off by setting this restriction to true in the managed profile.
-     * <p>
-     * Device owner and profile owner can set this restriction. When it is set by the profile
-     * owner of an organization-owned managed profile on the parent profile, it will prevent the
-     * user from turning on location sharing in the personal profile.
+     *
+     * <p>A device owner and a profile owner can set this restriction. When it is set by a device
+     * owner, a profile owner on the primary user or by a profile owner of an organization-owned
+     * managed profile on the parent profile, it prevents the primary user from turning on
+     * location sharing.
+     *
+     * <p>The default value is <code>false</code>.
      *
      * <p>Key for user restrictions.
      * <p>Type: Boolean
@@ -304,12 +307,13 @@
 
     /**
      * Specifies if airplane mode is disallowed on the device.
-     * <p>
-     * This restriction can only be set by the device owner, the profile owner on the primary user
-     * or the profile owner of an organization-owned managed profile on the parent profile, and it
-     * applies globally - i.e. it disables airplane mode on the entire device.
-     * <p>
-     * The default value is <code>false</code>.
+     *
+     * <p>This restriction can only be set by a device owner, a profile owner on the primary
+     * user or a profile owner of an organization-owned managed profile on the parent profile.
+     * When it is set by any of these owners, it applies globally - i.e., it disables airplane mode
+     * on the entire device.
+     *
+     * <p>The default value is <code>false</code>.
      *
      * <p>Key for user restrictions.
      * <p>Type: Boolean
@@ -397,17 +401,18 @@
             "no_install_unknown_sources_globally";
 
     /**
-     * Specifies if a user is disallowed from configuring bluetooth.
-     * This does <em>not</em> restrict the user from turning bluetooth on or off.
-     * The default value is <code>false</code>.
-     * <p>
-     * This restriction doesn't prevent the user from using bluetooth. For disallowing usage of
+     * Specifies if a user is disallowed from configuring bluetooth via Settings. This does
+     * <em>not</em> restrict the user from turning bluetooth on or off.
+     *
+     * <p>This restriction doesn't prevent the user from using bluetooth. For disallowing usage of
      * bluetooth completely on the device, use {@link #DISALLOW_BLUETOOTH}.
-     * <p>
-     * Device owner and profile owner can set this restriction, although the restriction has no
-     * effect in a managed profile. When it is set by the profile owner of an organization-owned
-     * managed profile on the parent profile, it will disallow the personal user from configuring
-     * bluetooth.
+     *
+     * <p>A device owner and a profile owner can set this restriction, although the restriction has
+     * no effect in a managed profile. When it is set by a device owner, a profile owner on the
+     * primary user or by a profile owner of an organization-owned managed profile on the parent
+     * profile, it disallows the primary user from configuring bluetooth.
+     *
+     * <p>The default value is <code>false</code>.
      *
      * <p>Key for user restrictions.
      * <p>Type: Boolean
@@ -418,13 +423,19 @@
     public static final String DISALLOW_CONFIG_BLUETOOTH = "no_config_bluetooth";
 
     /**
-     * Specifies if bluetooth is disallowed on the device.
+     * Specifies if bluetooth is disallowed on the device. If bluetooth is disallowed on the device,
+     * bluetooth cannot be turned on or configured via Settings.
      *
-     * <p> This restriction can only be set by the device owner, the profile owner on the
-     * primary user or the profile owner of an organization-owned managed profile on the
-     * parent profile and it applies globally - i.e. it disables bluetooth on the entire
-     * device.
+     * <p>This restriction can only be set by a device owner, a profile owner on the primary
+     * user or a profile owner of an organization-owned managed profile on the parent profile.
+     * When it is set by a device owner, it applies globally - i.e., it disables bluetooth on
+     * the entire device and all users will be affected. When it is set by a profile owner on the
+     * primary user or by a profile owner of an organization-owned managed profile on the parent
+     * profile, it disables the primary user from using bluetooth and configuring bluetooth
+     * in Settings.
+     *
      * <p>The default value is <code>false</code>.
+     *
      * <p>Key for user restrictions.
      * <p>Type: Boolean
      * @see DevicePolicyManager#addUserRestriction(ComponentName, String)
@@ -434,14 +445,17 @@
     public static final String DISALLOW_BLUETOOTH = "no_bluetooth";
 
     /**
-     * Specifies if outgoing bluetooth sharing is disallowed on the device. Device owner and profile
-     * owner can set this restriction. When it is set by device owner or the profile owner of an
-     * organization-owned managed profile on the parent profile, all users on this device will be
-     * affected.
+     * Specifies if outgoing bluetooth sharing is disallowed.
      *
-     * <p>Default is <code>true</code> for managed profiles and false for otherwise. When a device
-     * upgrades to {@link android.os.Build.VERSION_CODES#O}, the system sets it for all existing
-     * managed profiles.
+     * <p>A device owner and a profile owner can set this restriction. When it is set by a device
+     * owner, it applies globally. When it is set by a profile owner on the primary user or by a
+     * profile owner of an organization-owned managed profile on the parent profile, it disables
+     * the primary user from any outgoing bluetooth sharing.
+     *
+     * <p>Default is <code>true</code> for managed profiles and false otherwise.
+     *
+     * <p>When a device upgrades to {@link android.os.Build.VERSION_CODES#O}, the system sets it
+     * for all existing managed profiles.
      *
      * <p>Key for user restrictions.
      * <p>Type: Boolean
@@ -452,10 +466,17 @@
     public static final String DISALLOW_BLUETOOTH_SHARING = "no_bluetooth_sharing";
 
     /**
-     * Specifies if a user is disallowed from transferring files over
-     * USB. This can only be set by device owners, profile owners on the primary user or
-     * profile owners of organization-owned managed profiles on the parent profile.
-     * The default value is <code>false</code>.
+     * Specifies if a user is disallowed from transferring files over USB.
+     *
+     * <p>This restriction can only be set by a device owner, a profile owner on the primary
+     * user or a profile owner of an organization-owned managed profile on the parent profile.
+     * When it is set by a device owner, it applies globally. When it is set by a profile owner
+     * on the primary user or by a profile owner of an organization-owned managed profile on
+     * the parent profile, it disables the primary user from transferring files over USB. No other
+     * user on the device is able to use file transfer over USB because the UI for file transfer
+     * is always associated with the primary user.
+     *
+     * <p>The default value is <code>false</code>.
      *
      * <p>Key for user restrictions.
      * <p>Type: Boolean
@@ -512,13 +533,16 @@
     public static final String DISALLOW_REMOVE_MANAGED_PROFILE = "no_remove_managed_profile";
 
     /**
-     * Specifies if a user is disallowed from enabling or accessing debugging features. When set on
-     * the primary user or by the profile owner of an organization-owned managed profile on the
-     * parent profile, disables debugging features altogether, including USB debugging. When set on
-     * a managed profile or a secondary user, blocks debugging for that user only, including
-     * starting activities, making service calls, accessing content providers, sending broadcasts,
-     * installing/uninstalling packages, clearing user data, etc.
-     * The default value is <code>false</code>.
+     * Specifies if a user is disallowed from enabling or accessing debugging features.
+     *
+     * <p>A device owner and a profile owner can set this restriction. When it is set by a device
+     * owner, a profile owner on the primary user or by a profile owner of an organization-owned
+     * managed profile on the parent profile, it disables debugging features altogether, including
+     * USB debugging. When set on a managed profile or a secondary user, it blocks debugging for
+     * that user only, including starting activities, making service calls, accessing content
+     * providers, sending broadcasts, installing/uninstalling packages, clearing user data, etc.
+     *
+     * <p>The default value is <code>false</code>.
      *
      * <p>Key for user restrictions.
      * <p>Type: Boolean
@@ -546,19 +570,18 @@
 
     /**
      * Specifies if a user is disallowed from enabling or disabling location providers. As a
-     * result, user is disallowed from turning on or off location.
+     * result, user is disallowed from turning on or off location via Settings.
      *
-     * <p>
-     * In a managed profile, location sharing is forced off when it is turned off on the primary
-     * user or by the profile owner of an organization-owned managed profile on the parent profile.
-     * The user can still turn off location sharing on a managed profile when the restriction is
-     * set by the profile owner on a managed profile.
-     * <p>
-     * This user restriction is different from {@link #DISALLOW_SHARE_LOCATION},
-     * as the device owner or profile owner can still enable or disable location mode via
+     * <p>A device owner and a profile owner can set this restriction. When it is set by a device
+     * owner, a profile owner on the primary user or by a profile owner of an organization-owned
+     * managed profile on the parent profile, it disallows the primary user from turning location
+     * on or off.
+     *
+     * <p>The default value is <code>false</code>.
+     *
+     * <p>This user restriction is different from {@link #DISALLOW_SHARE_LOCATION},
+     * as a device owner or a profile owner can still enable or disable location mode via
      * {@link DevicePolicyManager#setLocationEnabled} when this restriction is on.
-     * <p>
-     * The default value is <code>false</code>.
      *
      * <p>Key for user restrictions.
      * <p>Type: Boolean
@@ -570,15 +593,18 @@
     public static final String DISALLOW_CONFIG_LOCATION = "no_config_location";
 
     /**
-     * Specifies if date, time and timezone configuring is disallowed.
+     * Specifies configuring date, time and timezone is disallowed via Settings.
      *
-     * <p>When restriction is set by device owners or profile owners of organization-owned
-     * managed profiles on the parent profile, it applies globally - i.e., it disables date,
-     * time and timezone setting on the entire device and all users will be affected. When it's set
-     * by profile owners, it's only applied to the managed user.
+     * <p>A device owner and a profile owner can set this restriction, although the restriction has
+     * no effect in a managed profile. When it is set by a device owner or by a profile owner of an
+     * organization-owned managed profile on the parent profile, it applies globally - i.e.,
+     * it disables date, time and timezone setting on the entire device and all users are affected.
+     * When it is set by a profile owner on the primary user, it disables the primary user
+     * from configuring date, time and timezone and disables all configuring of date, time and
+     * timezone in Settings.
+     *
      * <p>The default value is <code>false</code>.
      *
-     * <p>This user restriction has no effect on managed profiles.
      * <p>Key for user restrictions.
      * <p>Type: Boolean
      * @see DevicePolicyManager#addUserRestriction(ComponentName, String)
@@ -588,10 +614,18 @@
     public static final String DISALLOW_CONFIG_DATE_TIME = "no_config_date_time";
 
     /**
-     * Specifies if a user is disallowed from configuring Tethering
-     * & portable hotspots. This can only be set by device owners, profile owners on the
-     * primary user or profile owners of organization-owned managed profiles on the parent profile.
-     * The default value is <code>false</code>.
+     * Specifies if a user is disallowed from configuring Tethering and portable hotspots
+     * via Settings.
+     *
+     * <p>This restriction can only be set by a device owner, a profile owner on the primary
+     * user or a profile owner of an organization-owned managed profile on the parent profile.
+     * When it is set by a device owner, it applies globally. When it is set by a profile owner
+     * on the primary user or by a profile owner of an organization-owned managed profile on
+     * the parent profile, it disables the primary user from using Tethering and hotspots and
+     * disables all configuring of Tethering and hotspots in Settings.
+     *
+     * <p>The default value is <code>false</code>.
+     *
      * <p>In Android 9.0 or higher, if tethering is enabled when this restriction is set,
      * tethering will be automatically turned off.
      *
@@ -685,10 +719,16 @@
     public static final String ENSURE_VERIFY_APPS = "ensure_verify_apps";
 
     /**
-     * Specifies if a user is disallowed from configuring cell
-     * broadcasts. This can only be set by device owners, profile owners on the primary user or
-     * profile owners of organization-owned managed profiles on the parent profile.
-     * The default value is <code>false</code>.
+     * Specifies if a user is disallowed from configuring cell broadcasts.
+     *
+     * <p>This restriction can only be set by a device owner, a profile owner on the primary
+     * user or a profile owner of an organization-owned managed profile on the parent profile.
+     * When it is set by a device owner, it applies globally. When it is set by a profile owner
+     * on the primary user or by a profile owner of an organization-owned managed profile on
+     * the parent profile, it disables the primary user from configuring cell broadcasts.
+     *
+     * <p>The default value is <code>false</code>.
+     *
      * <p>This restriction has no effect on secondary users and managed profiles since only the
      * primary user can configure cell broadcasts.
      *
@@ -701,10 +741,16 @@
     public static final String DISALLOW_CONFIG_CELL_BROADCASTS = "no_config_cell_broadcasts";
 
     /**
-     * Specifies if a user is disallowed from configuring mobile
-     * networks. This can only be set by device owners, profile owners on the primary user or
-     * profile owners of organization-owned managed profiles on the parent profile.
-     * The default value is <code>false</code>.
+     * Specifies if a user is disallowed from configuring mobile networks.
+     *
+     * <p>This restriction can only be set by a device owner, a profile owner on the primary
+     * user or a profile owner of an organization-owned managed profile on the parent profile.
+     * When it is set by a device owner, it applies globally. When it is set by a profile owner
+     * on the primary user or by a profile owner of an organization-owned managed profile on
+     * the parent profile, it disables the primary user from configuring mobile networks.
+     *
+     * <p>The default value is <code>false</code>.
+     *
      * <p>This restriction has no effect on secondary users and managed profiles since only the
      * primary user can configure mobile networks.
      *
@@ -747,11 +793,14 @@
 
     /**
      * Specifies if a user is disallowed from mounting physical external media.
-     * <p>
-     * This restriction can only be set by the device owner, the profile owner on the primary user
-     * or the profile owner of an organization-owned managed profile on the parent profile.
-     * <p>
-     * The default value is <code>false</code>.
+     *
+     * <p>This restriction can only be set by a device owner, a profile owner on the primary
+     * user or a profile owner of an organization-owned managed profile on the parent profile.
+     * When it is set by a device owner, it applies globally. When it is set by a profile owner
+     * on the primary user or by a profile owner of an organization-owned managed profile on
+     * the parent profile, it disables the primary user from mounting physical external media.
+     *
+     * <p>The default value is <code>false</code>.
      *
      * <p>Key for user restrictions.
      * <p>Type: Boolean
@@ -764,13 +813,14 @@
     /**
      * Specifies if a user is disallowed from adjusting microphone volume. If set, the microphone
      * will be muted.
-     * <p>
-     * The default value is <code>false</code>.
-     * <p>
-     * Device owner and profile owner can set this restriction, although the restriction has no
-     * effect in a managed profile. When it is set by the profile owner of an organization-owned
-     * managed profile on the parent profile, it will disallow the personal user from adjusting the
-     * microphone volume.
+     *
+     * <p>A device owner and a profile owner can set this restriction, although the restriction has
+     * no effect in a managed profile. When it is set by a device owner, it applies globally. When
+     * it is set by a profile owner on the primary user or by a profile owner of an
+     * organization-owned managed profile on the parent profile, it will disallow the primary user
+     * from adjusting the microphone volume.
+     *
+     * <p>The default value is <code>false</code>.
      *
      * <p>Key for user restrictions.
      * <p>Type: Boolean
@@ -800,13 +850,13 @@
     /**
      * Specifies that the user is not allowed to make outgoing phone calls. Emergency calls are
      * still permitted.
-     * <p>
-     * The default value is <code>false</code>.
-     * <p>
-     * Device owner and profile owner can set this restriction, although the restriction has no
-     * effect in a managed profile. When it is set by the profile owner of an organization-owned
-     * managed profile on the parent profile, it will disallow the personal user from making
-     * outgoing phone calls.
+     *
+     * <p>A device owner and a profile owner can set this restriction, although the restriction has
+     * no effect in a managed profile. When it is set by a device owner, a profile owner on the
+     * primary user or by a profile owner of an organization-owned managed profile on the parent
+     * profile, it disallows the primary user from making outgoing phone calls.
+     *
+     * <p>The default value is <code>false</code>.
      *
      * <p>Key for user restrictions.
      * <p>Type: Boolean
@@ -817,12 +867,15 @@
     public static final String DISALLOW_OUTGOING_CALLS = "no_outgoing_calls";
 
     /**
-     * Specifies that the user is not allowed to send or receive
-     * SMS messages. The default value is <code>false</code>.
-     * <p>
-     * Device owner and profile owner can set this restriction. When it is set by the
-     * profile owner of an organization-owned managed profile on the parent profile,
-     * it will disable SMS in the personal profile.
+     * Specifies that the user is not allowed to send or receive SMS messages.
+     *
+     * <p>This restriction can only be set by a device owner, a profile owner on the primary
+     * user or a profile owner of an organization-owned managed profile on the parent profile.
+     * When it is set by a device owner, it applies globally. When it is set by a profile owner
+     * on the primary user or by a profile owner of an organization-owned managed profile on
+     * the parent profile, it disables the primary user from sending or receiving SMS messages.
+     *
+     * <p>The default value is <code>false</code>.
      *
      * <p>Key for user restrictions.
      * <p>Type: Boolean
@@ -941,9 +994,15 @@
 
     /**
      * Specifies if the user is not allowed to reboot the device into safe boot mode.
-     * This can only be set by device owners, profile owners on the primary user or profile
-     * owners of organization-owned managed profiles on the parent profile.
-     * The default value is <code>false</code>.
+     *
+     * <p>This restriction can only be set by a device owner, a profile owner on the primary
+     * user or a profile owner of an organization-owned managed profile on the parent profile.
+     * When it is set by a device owner, it applies globally. When it is set by a profile owner
+     * on the primary user or by a profile owner of an organization-owned managed profile on
+     * the parent profile, it disables the primary user from rebooting the device into safe
+     * boot mode.
+     *
+     * <p>The default value is <code>false</code>.
      *
      * <p>Key for user restrictions.
      * <p>Type: Boolean
@@ -981,12 +1040,14 @@
 
     /**
      * Specifies if a user is not allowed to use the camera.
-     * <p>
-     * Device owner and profile owner can set this restriction. When the restriction is set by
-     * the device owner or the profile owner of an organization-owned managed profile on the
-     * parent profile, it is applied globally.
-     * <p>
-     * The default value is <code>false</code>.
+     *
+     * <p>A device owner and a profile owner can set this restriction. When it is set by a
+     * device owner, it applies globally - i.e., it disables the use of camera on the entire device
+     * and all users are affected. When it is set by a profile owner on the primary user or by a
+     * profile owner of an organization-owned managed profile on the parent profile, it disables
+     * the primary user from using camera.
+     *
+     * <p>The default value is <code>false</code>.
      *
      * @see DevicePolicyManager#addUserRestriction(ComponentName, String)
      * @see DevicePolicyManager#clearUserRestriction(ComponentName, String)
@@ -1006,9 +1067,15 @@
     public static final String DISALLOW_UNMUTE_DEVICE = "disallow_unmute_device";
 
     /**
-     * Specifies if a user is not allowed to use cellular data when roaming. This can only be set by
-     * device owners or profile owners of organization-owned managed profiles on the parent profile.
-     * The default value is <code>false</code>.
+     * Specifies if a user is not allowed to use cellular data when roaming.
+     *
+     * <p>This restriction can only be set by a device owner, a profile owner on the primary
+     * user or a profile owner of an organization-owned managed profile on the parent profile.
+     * When it is set by a device owner, it applies globally. When it is set by a profile owner
+     * on the primary user or by a profile owner of an organization-owned managed profile on
+     * the parent profile, it disables the primary user from using cellular data when roaming.
+     *
+     * <p>The default value is <code>false</code>.
      *
      * @see DevicePolicyManager#addUserRestriction(ComponentName, String)
      * @see DevicePolicyManager#clearUserRestriction(ComponentName, String)
@@ -1103,9 +1170,10 @@
      * Specifies if the contents of a user's screen is not allowed to be captured for artificial
      * intelligence purposes.
      *
-     * <p>Device owner and profile owner can set this restriction. When it is set by the
-     * device owner or the profile owner of an organization-owned managed profile on the parent
-     * profile, only the target user will be affected.
+     * <p>A device owner and a profile owner can set this restriction. When it is set by a device
+     * owner, a profile owner on the primary user or by a profile owner of an organization-owned
+     * managed profile on the parent profile, it disables the primary user's screen from being
+     * captured for artificial intelligence purposes.
      *
      * <p>The default value is <code>false</code>.
      *
@@ -1119,9 +1187,10 @@
      * Specifies if the current user is able to receive content suggestions for selections based on
      * the contents of their screen.
      *
-     * <p>Device owner and profile owner can set this restriction. When it is set by the
-     * device owner or the profile owner of an organization-owned managed profile on the parent
-     * profile, only the target user will be affected.
+     * <p>A device owner and a profile owner can set this restriction. When it is set by a device
+     * owner, a profile owner on the primary user or by a profile owner of an organization-owned
+     * managed profile on the parent profile, it disables the primary user from receiving content
+     * suggestions for selections based on the contents of their screen.
      *
      * <p>The default value is <code>false</code>.
      *
@@ -1185,10 +1254,11 @@
     /**
      * Specifies whether the user is allowed to modify private DNS settings.
      *
-     * <p>The default value is <code>false</code>.
+     * <p>This restriction can only be set by a device owner or a profile owner of an
+     * organization-owned managed profile on the parent profile. When it is set by either of these
+     * owners, it applies globally.
      *
-     * <p>This user restriction can only be applied by the device owner or the profile owner
-     * of an organization-owned managed profile on the parent profile.
+     * <p>The default value is <code>false</code>.
      *
      * <p>Key for user restrictions.
      * <p>Type: Boolean
diff --git a/core/java/android/os/incremental/IIncrementalService.aidl b/core/java/android/os/incremental/IIncrementalService.aidl
index 25cb040..4c57570 100644
--- a/core/java/android/os/incremental/IIncrementalService.aidl
+++ b/core/java/android/os/incremental/IIncrementalService.aidl
@@ -19,6 +19,8 @@
 import android.content.pm.DataLoaderParamsParcel;
 import android.content.pm.IDataLoaderStatusListener;
 import android.os.incremental.IncrementalNewFileParams;
+import android.os.incremental.IStorageHealthListener;
+import android.os.incremental.StorageHealthCheckParams;
 
 /** @hide */
 interface IIncrementalService {
@@ -34,7 +36,10 @@
      * Opens or creates a storage given a target path and data loader params. Returns the storage ID.
      */
     int openStorage(in @utf8InCpp String path);
-    int createStorage(in @utf8InCpp String path, in DataLoaderParamsParcel params, in IDataLoaderStatusListener listener, int createMode);
+    int createStorage(in @utf8InCpp String path, in DataLoaderParamsParcel params, int createMode,
+                      in IDataLoaderStatusListener statusListener,
+                      in StorageHealthCheckParams healthCheckParams,
+                      in IStorageHealthListener healthListener);
     int createLinkedStorage(in @utf8InCpp String path, int otherStorageId, int createMode);
 
     /**
diff --git a/core/java/android/os/incremental/IStorageHealthListener.aidl b/core/java/android/os/incremental/IStorageHealthListener.aidl
new file mode 100644
index 0000000..9f93ede
--- /dev/null
+++ b/core/java/android/os/incremental/IStorageHealthListener.aidl
@@ -0,0 +1,35 @@
+/*
+ * 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.incremental;
+
+/** @hide */
+oneway interface IStorageHealthListener {
+    /** OK status, no pending reads. */
+    const int HEALTH_STATUS_OK = 0;
+    /* Statuses depend on timeouts defined in StorageHealthCheckParams. */
+    /** Pending reads detected, waiting for params.blockedTimeoutMs to confirm blocked state. */
+    const int HEALTH_STATUS_READS_PENDING = 1;
+    /** There are reads pending for params.blockedTimeoutMs, waiting till
+    *   params.unhealthyTimeoutMs to confirm unhealthy state. */
+    const int HEALTH_STATUS_BLOCKED = 2;
+    /** There are reads pending for params.unhealthyTimeoutMs>,
+    *   marking storage as unhealthy. */
+    const int HEALTH_STATUS_UNHEALTHY = 3;
+
+    /** Health status callback. */
+    void onHealthStatus(in int storageId, in int status);
+}
diff --git a/core/java/android/os/incremental/IncrementalFileStorages.java b/core/java/android/os/incremental/IncrementalFileStorages.java
index 958c7fb..863d86ef 100644
--- a/core/java/android/os/incremental/IncrementalFileStorages.java
+++ b/core/java/android/os/incremental/IncrementalFileStorages.java
@@ -65,7 +65,9 @@
     public static IncrementalFileStorages initialize(Context context,
             @NonNull File stageDir,
             @NonNull DataLoaderParams dataLoaderParams,
-            @Nullable IDataLoaderStatusListener dataLoaderStatusListener,
+            @Nullable IDataLoaderStatusListener statusListener,
+            @Nullable StorageHealthCheckParams healthCheckParams,
+            @Nullable IStorageHealthListener healthListener,
             List<InstallationFileParcel> addedFiles) throws IOException {
         // TODO(b/136132412): sanity check if session should not be incremental
         IncrementalManager incrementalManager = (IncrementalManager) context.getSystemService(
@@ -75,9 +77,9 @@
             throw new IOException("Failed to obtain incrementalManager.");
         }
 
-        final IncrementalFileStorages result =
-                new IncrementalFileStorages(stageDir, incrementalManager, dataLoaderParams,
-                                            dataLoaderStatusListener);
+        final IncrementalFileStorages result = new IncrementalFileStorages(stageDir,
+                incrementalManager, dataLoaderParams, statusListener, healthCheckParams,
+                healthListener);
         for (InstallationFileParcel file : addedFiles) {
             if (file.location == LOCATION_DATA_APP) {
                 try {
@@ -100,7 +102,9 @@
     private IncrementalFileStorages(@NonNull File stageDir,
             @NonNull IncrementalManager incrementalManager,
             @NonNull DataLoaderParams dataLoaderParams,
-            @Nullable IDataLoaderStatusListener dataLoaderStatusListener) throws IOException {
+            @Nullable IDataLoaderStatusListener statusListener,
+            @Nullable StorageHealthCheckParams healthCheckParams,
+            @Nullable IStorageHealthListener healthListener) throws IOException {
         try {
             mStageDir = stageDir;
             mIncrementalManager = incrementalManager;
@@ -117,10 +121,9 @@
                 mDefaultStorage.bind(stageDir.getAbsolutePath());
             } else {
                 mDefaultStorage = mIncrementalManager.createStorage(stageDir.getAbsolutePath(),
-                        dataLoaderParams,
-                        dataLoaderStatusListener,
-                        IncrementalManager.CREATE_MODE_CREATE
-                                | IncrementalManager.CREATE_MODE_TEMPORARY_BIND, false);
+                        dataLoaderParams, IncrementalManager.CREATE_MODE_CREATE
+                                | IncrementalManager.CREATE_MODE_TEMPORARY_BIND, false,
+                        statusListener, healthCheckParams, healthListener);
                 if (mDefaultStorage == null) {
                     throw new IOException(
                             "Couldn't create incremental storage at " + stageDir);
diff --git a/core/java/android/os/incremental/IncrementalManager.java b/core/java/android/os/incremental/IncrementalManager.java
index 916edfa..c7f50c9 100644
--- a/core/java/android/os/incremental/IncrementalManager.java
+++ b/core/java/android/os/incremental/IncrementalManager.java
@@ -110,11 +110,15 @@
      */
     @Nullable
     public IncrementalStorage createStorage(@NonNull String path,
-            @NonNull DataLoaderParams params, @Nullable IDataLoaderStatusListener listener,
+            @NonNull DataLoaderParams params,
             @CreateMode int createMode,
-            boolean autoStartDataLoader) {
+            boolean autoStartDataLoader,
+            @Nullable IDataLoaderStatusListener statusListener,
+            @Nullable StorageHealthCheckParams healthCheckParams,
+            @Nullable IStorageHealthListener healthListener) {
         try {
-            final int id = mService.createStorage(path, params.getData(), listener, createMode);
+            final int id = mService.createStorage(path, params.getData(), createMode,
+                    statusListener, healthCheckParams, healthListener);
             if (id < 0) {
                 return null;
             }
diff --git a/core/java/android/os/incremental/StorageHealthCheckParams.aidl b/core/java/android/os/incremental/StorageHealthCheckParams.aidl
new file mode 100644
index 0000000..6839317
--- /dev/null
+++ b/core/java/android/os/incremental/StorageHealthCheckParams.aidl
@@ -0,0 +1,35 @@
+/*
+ * 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.incremental;
+
+/**
+ * @hide
+ */
+parcelable StorageHealthCheckParams {
+    /** Timeouts of the oldest pending read.
+    *   Valid values 0ms < blockedTimeoutMs < unhealthyTimeoutMs < storage page read timeout.
+    *   Invalid values will disable health checking. */
+
+    /** To consider storage "blocked". */
+    int blockedTimeoutMs;
+    /** To consider storage "unhealthy". */
+    int unhealthyTimeoutMs;
+
+    /** After storage is marked "unhealthy", how often to check if it recovered.
+    *   Valid value 1000ms < unhealthyMonitoringMs. */
+    int unhealthyMonitoringMs;
+}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index e0bc764..1b19e12 100755
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -14363,6 +14363,21 @@
          * @hide
          */
         public static final String ADVANCED_BATTERY_USAGE_AMOUNT = "advanced_battery_usage_amount";
+
+        /**
+         * For 5G NSA capable devices, determines whether NR tracking indications are on
+         * when the screen is off.
+         *
+         * Values are:
+         * 0: off - All 5G NSA tracking indications are off when the screen is off.
+         * 1: extended - All 5G NSA tracking indications are on when the screen is off as long as
+         *    the device is camped on 5G NSA (5G icon is showing in status bar).
+         *    If the device is not camped on 5G NSA, tracking indications are off.
+         * 2: always on - All 5G NSA tracking indications are on whether the screen is on or off.
+         * @hide
+         */
+        public static final String NR_NSA_TRACKING_SCREEN_OFF_MODE =
+                "nr_nsa_tracking_screen_off_mode";
     }
 
     /**
diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java
index d20ffb3..9b5b882 100644
--- a/core/java/android/view/WindowlessWindowManager.java
+++ b/core/java/android/view/WindowlessWindowManager.java
@@ -136,6 +136,7 @@
         final SurfaceControl.Builder b = new SurfaceControl.Builder(mSurfaceSession)
                 .setParent(mRootSurface)
                 .setFormat(attrs.format)
+                .setBufferSize(getSurfaceWidth(attrs), getSurfaceHeight(attrs))
                 .setName(attrs.getTitle().toString());
         final SurfaceControl sc = b.build();
 
@@ -242,13 +243,8 @@
         WindowManager.LayoutParams attrs = state.mParams;
 
         if (viewFlags == View.VISIBLE) {
-            final Rect surfaceInsets = attrs.surfaceInsets;
-            int width = surfaceInsets != null
-                    ? attrs.width + surfaceInsets.left + surfaceInsets.right : attrs.width;
-            int height = surfaceInsets != null
-                    ? attrs.height + surfaceInsets.top + surfaceInsets.bottom : attrs.height;
-
-            t.setBufferSize(sc, width, height).setOpaque(sc, isOpaque(attrs)).show(sc).apply();
+            t.setBufferSize(sc, getSurfaceWidth(attrs), getSurfaceHeight(attrs))
+                    .setOpaque(sc, isOpaque(attrs)).show(sc).apply();
             outSurfaceControl.copyFrom(sc);
         } else {
             t.hide(sc).apply();
@@ -444,4 +440,15 @@
     public android.os.IBinder asBinder() {
         return null;
     }
+
+    private int getSurfaceWidth(WindowManager.LayoutParams attrs) {
+      final Rect surfaceInsets = attrs.surfaceInsets;
+      return surfaceInsets != null
+          ? attrs.width + surfaceInsets.left + surfaceInsets.right : attrs.width;
+    }
+    private int getSurfaceHeight(WindowManager.LayoutParams attrs) {
+      final Rect surfaceInsets = attrs.surfaceInsets;
+      return surfaceInsets != null
+          ? attrs.height + surfaceInsets.top + surfaceInsets.bottom : attrs.height;
+    }
 }
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index f041ad9..8b0dd8a 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -101,6 +101,7 @@
 import android.view.View.OnClickListener;
 import android.view.ViewGroup;
 import android.view.ViewGroup.LayoutParams;
+import android.view.ViewTreeObserver;
 import android.view.animation.AccelerateInterpolator;
 import android.view.animation.DecelerateInterpolator;
 import android.widget.Button;
@@ -2777,6 +2778,7 @@
     @Override
     public void onListRebuilt(ResolverListAdapter listAdapter) {
         setupScrollListener();
+        maybeSetupGlobalLayoutListener();
 
         ChooserListAdapter chooserListAdapter = (ChooserListAdapter) listAdapter;
         if (chooserListAdapter.getUserHandle()
@@ -2858,6 +2860,28 @@
                 });
     }
 
+    private void maybeSetupGlobalLayoutListener() {
+        if (shouldShowTabs()) {
+            return;
+        }
+        final View recyclerView = mChooserMultiProfilePagerAdapter.getActiveAdapterView();
+        recyclerView.getViewTreeObserver()
+                .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+                    @Override
+                    public void onGlobalLayout() {
+                        // Fixes an issue were the accessibility border disappears on list creation.
+                        recyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                        final TextView titleView = findViewById(R.id.title);
+                        if (titleView != null) {
+                            titleView.setFocusable(true);
+                            titleView.setFocusableInTouchMode(true);
+                            titleView.requestFocus();
+                            titleView.requestAccessibilityFocus();
+                        }
+                    }
+                });
+    }
+
     @Override // ChooserListCommunicator
     public boolean isSendAction(Intent targetIntent) {
         if (targetIntent == null) {
diff --git a/core/proto/android/providers/settings/global.proto b/core/proto/android/providers/settings/global.proto
index 762895b..023197b 100644
--- a/core/proto/android/providers/settings/global.proto
+++ b/core/proto/android/providers/settings/global.proto
@@ -695,6 +695,8 @@
     }
     optional Notification notification = 82;
 
+    optional SettingProto nr_nsa_tracking_screen_off_mode = 153 [ (android.privacy).dest = DEST_AUTOMATIC ];
+
     optional SettingProto nsd_on = 83 [ (android.privacy).dest = DEST_AUTOMATIC ];
 
     message Ntp {
@@ -1060,5 +1062,5 @@
 
     // Please insert fields in alphabetical order and group them into messages
     // if possible (to avoid reaching the method limit).
-    // Next tag = 153;
+    // Next tag = 154;
 }
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index bdb6bcc..8d73f8a9 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -409,6 +409,8 @@
         <permission name="android.permission.ACCESS_TV_DESCRAMBLER" />
         <permission name="android.permission.ACCESS_TV_TUNER" />
         <permission name="android.permission.TUNER_RESOURCE_ACCESS" />
+        <!-- Permissions required for CTS test - TVInputManagerTest -->
+        <permission name="android.permission.TV_INPUT_HARDWARE" />
     </privapp-permissions>
 
     <privapp-permissions package="com.android.statementservice">
diff --git a/libs/hwui/jni/android_graphics_Canvas.cpp b/libs/hwui/jni/android_graphics_Canvas.cpp
index 4aff3e5..b6c6cd0 100644
--- a/libs/hwui/jni/android_graphics_Canvas.cpp
+++ b/libs/hwui/jni/android_graphics_Canvas.cpp
@@ -113,7 +113,7 @@
     get_canvas(canvasHandle)->restoreUnclippedLayer(saveCount, *paint);
 }
 
-static bool restore(CRITICAL_JNI_PARAMS_COMMA jlong canvasHandle) {
+static jboolean restore(CRITICAL_JNI_PARAMS_COMMA jlong canvasHandle) {
     Canvas* canvas = get_canvas(canvasHandle);
     if (canvas->getSaveCount() <= 1) {
         return false; // cannot restore anymore
diff --git a/libs/hwui/jni/android_graphics_drawable_VectorDrawable.cpp b/libs/hwui/jni/android_graphics_drawable_VectorDrawable.cpp
index 8a26296..9cffceb 100644
--- a/libs/hwui/jni/android_graphics_drawable_VectorDrawable.cpp
+++ b/libs/hwui/jni/android_graphics_drawable_VectorDrawable.cpp
@@ -102,7 +102,7 @@
 /**
  * Draw
  */
-static int draw(JNIEnv* env, jobject, jlong treePtr, jlong canvasPtr,
+static jint draw(JNIEnv* env, jobject, jlong treePtr, jlong canvasPtr,
         jlong colorFilterPtr, jobject jrect, jboolean needsMirroring, jboolean canReuseCache) {
     VectorDrawable::Tree* tree = reinterpret_cast<VectorDrawable::Tree*>(treePtr);
     Canvas* canvas = reinterpret_cast<Canvas*>(canvasPtr);
diff --git a/libs/hwui/jni/android_util_PathParser.cpp b/libs/hwui/jni/android_util_PathParser.cpp
index df5e9cd..72995ef 100644
--- a/libs/hwui/jni/android_util_PathParser.cpp
+++ b/libs/hwui/jni/android_util_PathParser.cpp
@@ -39,18 +39,18 @@
     }
 }
 
-static long createEmptyPathData(JNIEnv*, jobject) {
+static jlong createEmptyPathData(JNIEnv*, jobject) {
     PathData* pathData = new PathData();
     return reinterpret_cast<jlong>(pathData);
 }
 
-static long createPathData(JNIEnv*, jobject, jlong pathDataPtr) {
+static jlong createPathData(JNIEnv*, jobject, jlong pathDataPtr) {
     PathData* pathData = reinterpret_cast<PathData*>(pathDataPtr);
     PathData* newPathData = new PathData(*pathData);
     return reinterpret_cast<jlong>(newPathData);
 }
 
-static long createPathDataFromStringPath(JNIEnv* env, jobject, jstring inputStr, jint strLength) {
+static jlong createPathDataFromStringPath(JNIEnv* env, jobject, jstring inputStr, jint strLength) {
     const char* pathString = env->GetStringUTFChars(inputStr, NULL);
     PathData* pathData = new PathData();
     PathParser::ParseResult result;
@@ -65,7 +65,7 @@
     }
 }
 
-static bool interpolatePathData(JNIEnv*, jobject, jlong outPathDataPtr, jlong fromPathDataPtr,
+static jboolean interpolatePathData(JNIEnv*, jobject, jlong outPathDataPtr, jlong fromPathDataPtr,
         jlong toPathDataPtr, jfloat fraction) {
     PathData* outPathData = reinterpret_cast<PathData*>(outPathDataPtr);
     PathData* fromPathData = reinterpret_cast<PathData*>(fromPathDataPtr);
@@ -79,7 +79,7 @@
     delete pathData;
 }
 
-static bool canMorphPathData(JNIEnv*, jobject, jlong fromPathDataPtr, jlong toPathDataPtr) {
+static jboolean canMorphPathData(JNIEnv*, jobject, jlong fromPathDataPtr, jlong toPathDataPtr) {
     PathData* fromPathData = reinterpret_cast<PathData*>(fromPathDataPtr);
     PathData* toPathData = reinterpret_cast<PathData*>(toPathDataPtr);
     return VectorDrawableUtils::canMorph(*fromPathData, *toPathData);
diff --git a/media/java/android/media/tv/ITvInputManager.aidl b/media/java/android/media/tv/ITvInputManager.aidl
index 508a46f4..1fbb672 100644
--- a/media/java/android/media/tv/ITvInputManager.aidl
+++ b/media/java/android/media/tv/ITvInputManager.aidl
@@ -111,4 +111,8 @@
     // For preview channels and programs
     void sendTvInputNotifyIntent(in Intent intent, int userId);
     void requestChannelBrowsable(in Uri channelUri, int userId);
+
+    // For CTS purpose only. Add/remove a TvInputHardware device
+    void addHardwareDevice(in int deviceId);
+    void removeHardwareDevice(in int deviceId);
 }
diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java
index e701055..98a01a4 100644
--- a/media/java/android/media/tv/TvInputManager.java
+++ b/media/java/android/media/tv/TvInputManager.java
@@ -23,6 +23,7 @@
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
+import android.annotation.TestApi;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Rect;
@@ -1801,6 +1802,40 @@
                 executor, callback);
     }
 
+    /**
+     * API to add a hardware device in the TvInputHardwareManager for CTS testing
+     * purpose.
+     *
+     * @param deviceId Id of the adding hardware device.
+     *
+     * @hide
+     */
+    @TestApi
+    public void addHardwareDevice(int deviceId) {
+        try {
+            mService.addHardwareDevice(deviceId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * API to remove a hardware device in the TvInputHardwareManager for CTS testing
+     * purpose.
+     *
+     * @param deviceId Id of the removing hardware device.
+     *
+     * @hide
+     */
+    @TestApi
+    public void removeHardwareDevice(int deviceId) {
+        try {
+            mService.removeHardwareDevice(deviceId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
     private Hardware acquireTvInputHardwareInternal(int deviceId, TvInputInfo info,
             String tvInputSessionId, int priorityHint,
             Executor executor, final HardwareCallback callback) {
diff --git a/media/java/android/media/tv/tuner/Tuner.java b/media/java/android/media/tv/tuner/Tuner.java
index a458b16..8bf688d 100644
--- a/media/java/android/media/tv/tuner/Tuner.java
+++ b/media/java/android/media/tv/tuner/Tuner.java
@@ -447,7 +447,7 @@
     private native DvrRecorder nativeOpenDvrRecorder(long bufferSize);
     private native DvrPlayback nativeOpenDvrPlayback(long bufferSize);
 
-    private static native DemuxCapabilities nativeGetDemuxCapabilities();
+    private native DemuxCapabilities nativeGetDemuxCapabilities();
 
     private native int nativeCloseDemux(int handle);
     private native int nativeCloseFrontend(int handle);
@@ -939,8 +939,7 @@
         Filter filter = nativeOpenFilter(
                 mainType, TunerUtils.getFilterSubtype(mainType, subType), bufferSize);
         if (filter != null) {
-            filter.setMainType(mainType);
-            filter.setSubtype(subType);
+            filter.setType(mainType, subType);
             filter.setCallback(cb, executor);
             if (mHandler == null) {
                 mHandler = createEventHandler();
@@ -1147,8 +1146,11 @@
     }
 
     /* package */ void releaseLnb() {
-        mTunerResourceManager.releaseLnb(mLnbHandle, mClientId);
-        mLnbHandle = null;
+        if (mLnbHandle != null) {
+            // LNB handle can be null if it's opened by name.
+            mTunerResourceManager.releaseLnb(mLnbHandle, mClientId);
+            mLnbHandle = null;
+        }
         mLnb = null;
     }
 }
diff --git a/media/java/android/media/tv/tuner/dvr/DvrPlayback.java b/media/java/android/media/tv/tuner/dvr/DvrPlayback.java
index 9971c84..68071b0 100644
--- a/media/java/android/media/tv/tuner/dvr/DvrPlayback.java
+++ b/media/java/android/media/tv/tuner/dvr/DvrPlayback.java
@@ -105,28 +105,33 @@
 
 
     /**
-     * Attaches a filter to DVR interface for recording.
+     * Attaches a filter to DVR interface for playback.
      *
-     * <p>There can be multiple filters attached. Attached filters are independent, so the order
-     * doesn't matter.
+     * <p>This method will be deprecated. Now it's a no-op.
+     * <p>Filters opened by {@link Tuner#openFilter} are used for DVR playback.
      *
      * @param filter the filter to be attached.
      * @return result status of the operation.
      */
     @Result
     public int attachFilter(@NonNull Filter filter) {
-        return nativeAttachFilter(filter);
+        // no-op
+        return Tuner.RESULT_UNAVAILABLE;
     }
 
     /**
      * Detaches a filter from DVR interface.
      *
+     * <p>This method will be deprecated. Now it's a no-op.
+     * <p>Filters opened by {@link Tuner#openFilter} are used for DVR playback.
+     *
      * @param filter the filter to be detached.
      * @return result status of the operation.
      */
     @Result
     public int detachFilter(@NonNull Filter filter) {
-        return nativeDetachFilter(filter);
+        // no-op
+        return Tuner.RESULT_UNAVAILABLE;
     }
 
     /**
diff --git a/media/java/android/media/tv/tuner/filter/Filter.java b/media/java/android/media/tv/tuner/filter/Filter.java
index cc932da..f0015b7 100644
--- a/media/java/android/media/tv/tuner/filter/Filter.java
+++ b/media/java/android/media/tv/tuner/filter/Filter.java
@@ -221,12 +221,9 @@
     }
 
     /** @hide */
-    public void setMainType(@Type int mainType) {
+    public void setType(@Type int mainType, @Subtype int subtype) {
         mMainType = mainType;
-    }
-    /** @hide */
-    public void setSubtype(@Subtype int subtype) {
-        mSubtype = subtype;
+        mSubtype = TunerUtils.getFilterSubtype(mainType, subtype);
     }
 
     /** @hide */
diff --git a/media/jni/android_media_tv_Tuner.cpp b/media/jni/android_media_tv_Tuner.cpp
index fb8c276..7e72140 100644
--- a/media/jni/android_media_tv_Tuner.cpp
+++ b/media/jni/android_media_tv_Tuner.cpp
@@ -2975,7 +2975,7 @@
     jbyte *dst = env->GetByteArrayElements(buffer, &isCopy);
     ALOGD("copyData, isCopy=%d", isCopy);
     if (dst == nullptr) {
-        ALOGD("Failed to GetByteArrayElements");
+        jniThrowRuntimeException(env, "Failed to GetByteArrayElements");
         return 0;
     }
 
@@ -2983,7 +2983,7 @@
         env->ReleaseByteArrayElements(buffer, dst, 0);
         flag->wake(static_cast<uint32_t>(DemuxQueueNotifyBits::DATA_CONSUMED));
     } else {
-        ALOGD("Failed to read FMQ");
+        jniThrowRuntimeException(env, "Failed to read FMQ");
         env->ReleaseByteArrayElements(buffer, dst, 0);
         return 0;
     }
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
index af72888..1d4cfdc 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
@@ -500,6 +500,20 @@
         }
     }
 
+    /**
+     * To generate and cache the label description.
+     *
+     * @param entry contain the entries of an app
+     */
+    public void ensureLabelDescription(AppEntry entry) {
+        if (entry.labelDescription != null) {
+            return;
+        }
+        synchronized (entry) {
+            entry.ensureLabelDescriptionLocked(mContext);
+        }
+    }
+
     public void requestSize(String packageName, int userId) {
         if (DEBUG_LOCKING) Log.v(TAG, "requestSize about to acquire lock...");
         synchronized (mEntriesMap) {
@@ -1524,6 +1538,7 @@
         public long size;
         public long internalSize;
         public long externalSize;
+        public String labelDescription;
 
         public boolean mounted;
 
@@ -1616,6 +1631,24 @@
                 return "";
             }
         }
+
+        /**
+         * Get the label description which distinguishes a personal app from a work app for
+         * accessibility purpose. If the app is in a work profile, then add a "work" prefix to the
+         * app label.
+         *
+         * @param context The application context
+         */
+        public void ensureLabelDescriptionLocked(Context context) {
+            final int userId = UserHandle.getUserId(this.info.uid);
+            if (UserManager.get(context).isManagedProfile(userId)) {
+                this.labelDescription = context.getString(
+                        com.android.settingslib.R.string.accessibility_work_profile_app_description,
+                        this.label);
+            } else {
+                this.labelDescription = this.label;
+            }
+        }
     }
 
     private static boolean hasFlag(int flags, int flag) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
index 959c42fe..b83a9c4 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
@@ -219,6 +219,33 @@
     }
 
     /**
+     * Get the MediaDevice list that can be removed from current media session.
+     *
+     * @return list of MediaDevice
+     */
+    List<MediaDevice> getDeselectableMediaDevice() {
+        final List<MediaDevice> deviceList = new ArrayList<>();
+        if (TextUtils.isEmpty(mPackageName)) {
+            Log.d(TAG, "getDeselectableMediaDevice() package name is null or empty!");
+            return deviceList;
+        }
+
+        final RoutingSessionInfo info = getRoutingSessionInfo();
+        if (info != null) {
+            for (MediaRoute2Info route : mRouterManager.getDeselectableRoutes(info)) {
+                deviceList.add(new InfoMediaDevice(mContext, mRouterManager,
+                        route, mPackageName));
+                Log.d(TAG, route.getName() + " is deselectable for " + mPackageName);
+            }
+            return deviceList;
+        }
+        Log.d(TAG, "getDeselectableMediaDevice() cannot found deselectable MediaDevice from : "
+                + mPackageName);
+
+        return deviceList;
+    }
+
+    /**
      * Get the MediaDevice list that has been selected to current media.
      *
      * @return list of MediaDevice
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
index f34903c..e89f628 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
@@ -308,6 +308,15 @@
     }
 
     /**
+     * Get the MediaDevice list that can be removed from current media session.
+     *
+     * @return list of MediaDevice
+     */
+    public List<MediaDevice> getDeselectableMediaDevice() {
+        return mInfoMediaManager.getDeselectableMediaDevice();
+    }
+
+    /**
      * Release session to stop playing media on MediaDevice.
      */
     public boolean releaseSession() {
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
index c514671..248eb5b 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
@@ -416,6 +416,31 @@
     }
 
     @Test
+    public void getDeselectableMediaDevice_packageNameIsNull_returnFalse() {
+        mInfoMediaManager.mPackageName = null;
+
+        assertThat(mInfoMediaManager.getDeselectableMediaDevice()).isEmpty();
+    }
+
+    @Test
+    public void getDeselectableMediaDevice_checkList() {
+        final List<RoutingSessionInfo> routingSessionInfos = new ArrayList<>();
+        final RoutingSessionInfo info = mock(RoutingSessionInfo.class);
+        routingSessionInfos.add(info);
+        final List<MediaRoute2Info> mediaRoute2Infos = new ArrayList<>();
+        final MediaRoute2Info mediaRoute2Info = mock(MediaRoute2Info.class);
+        mediaRoute2Infos.add(mediaRoute2Info);
+        mShadowRouter2Manager.setRoutingSessions(routingSessionInfos);
+        mShadowRouter2Manager.setDeselectableRoutes(mediaRoute2Infos);
+        when(mediaRoute2Info.getName()).thenReturn(TEST_NAME);
+
+        final List<MediaDevice> mediaDevices = mInfoMediaManager.getDeselectableMediaDevice();
+
+        assertThat(mediaDevices.size()).isEqualTo(1);
+        assertThat(mediaDevices.get(0).getName()).isEqualTo(TEST_NAME);
+    }
+
+    @Test
     public void adjustSessionVolume_routingSessionInfoIsNull_noCrash() {
         mInfoMediaManager.adjustSessionVolume(null, 10);
     }
diff --git a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowRouter2Manager.java b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowRouter2Manager.java
index db0cb06..5bb5500 100644
--- a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowRouter2Manager.java
+++ b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowRouter2Manager.java
@@ -15,6 +15,7 @@
  */
 package com.android.settingslib.testutils.shadow;
 
+import android.annotation.NonNull;
 import android.media.MediaRoute2Info;
 import android.media.MediaRouter2Manager;
 import android.media.RoutingSessionInfo;
@@ -31,6 +32,7 @@
 
     private List<MediaRoute2Info> mAvailableRoutes;
     private List<MediaRoute2Info> mAllRoutes;
+    private List<MediaRoute2Info> mDeselectableRoutes;
     private List<RoutingSessionInfo> mActiveSessions;
     private List<RoutingSessionInfo> mRoutingSessions;
 
@@ -70,6 +72,15 @@
         mRoutingSessions = infos;
     }
 
+    @Implementation
+    public List<MediaRoute2Info> getDeselectableRoutes(@NonNull RoutingSessionInfo sessionInfo) {
+        return mDeselectableRoutes;
+    }
+
+    public void setDeselectableRoutes(List<MediaRoute2Info> routes) {
+        mDeselectableRoutes = routes;
+    }
+
     public static ShadowRouter2Manager getShadow() {
         return (ShadowRouter2Manager) Shadow.extract(
                 MediaRouter2Manager.getInstance(RuntimeEnvironment.application));
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index 3d7559b..aae72e5 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -1131,6 +1131,10 @@
                 Settings.Global.NSD_ON,
                 GlobalSettingsProto.NSD_ON);
 
+        dumpSetting(s, p,
+                Settings.Global.NR_NSA_TRACKING_SCREEN_OFF_MODE,
+                GlobalSettingsProto.NR_NSA_TRACKING_SCREEN_OFF_MODE);
+
         final long ntpToken = p.start(GlobalSettingsProto.NTP);
         dumpSetting(s, p,
                 Settings.Global.NTP_SERVER,
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index e537b76..fa87b62 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -391,6 +391,7 @@
                     Settings.Global.NITZ_UPDATE_DIFF,
                     Settings.Global.NITZ_UPDATE_SPACING,
                     Settings.Global.NOTIFICATION_SNOOZE_OPTIONS,
+                    Settings.Global.NR_NSA_TRACKING_SCREEN_OFF_MODE,
                     Settings.Global.NSD_ON,
                     Settings.Global.NTP_SERVER,
                     Settings.Global.NTP_TIMEOUT,
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index b85c771..a0130f8 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -300,6 +300,9 @@
     <!-- Permissions needed to test shared libraries -->
     <uses-permission android:name="android.permission.ACCESS_SHARED_LIBRARIES" />
 
+    <!-- Permissions required for CTS test - TVInputManagerTest -->
+    <uses-permission android:name="android.permission.TV_INPUT_HARDWARE" />
+
     <application android:label="@string/app_label"
                 android:theme="@android:style/Theme.DeviceDefault.DayNight"
                 android:defaultToDeviceProtectedStorage="true"
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 055b2be..985269b 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -607,7 +607,7 @@
                   android:excludeFromRecents="true"
                   android:stateNotNeeded="true"
                   android:resumeWhilePausing="true"
-                  android:theme="@android:style/Theme.Black.NoTitleBar">
+                  android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen">
             <intent-filter>
                 <action android:name="android.app.action.CONFIRM_DEVICE_CREDENTIAL_WITH_USER" />
                 <category android:name="android.intent.category.DEFAULT" />
diff --git a/packages/SystemUI/OWNERS b/packages/SystemUI/OWNERS
index c8cf7f50..28491d6 100644
--- a/packages/SystemUI/OWNERS
+++ b/packages/SystemUI/OWNERS
@@ -41,6 +41,7 @@
 [email protected]
 [email protected]
 [email protected]
[email protected]
 
 #Android Auto
 [email protected]
diff --git a/packages/SystemUI/res/layout/keyguard_media_header.xml b/packages/SystemUI/res/layout/keyguard_media_header.xml
index a520719..63a878f 100644
--- a/packages/SystemUI/res/layout/keyguard_media_header.xml
+++ b/packages/SystemUI/res/layout/keyguard_media_header.xml
@@ -24,25 +24,4 @@
     android:paddingEnd="0dp"
     android:focusable="true"
     android:clickable="true"
->
-
-    <!-- Background views required by ActivatableNotificationView. -->
-    <com.android.systemui.statusbar.notification.row.NotificationBackgroundView
-        android:id="@+id/backgroundNormal"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-    />
-
-    <com.android.systemui.statusbar.notification.row.NotificationBackgroundView
-        android:id="@+id/backgroundDimmed"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-    />
-
-    <com.android.systemui.statusbar.notification.FakeShadowView
-        android:id="@+id/fake_shadow"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-    />
-
-</com.android.systemui.statusbar.notification.stack.MediaHeaderView>
+/>
diff --git a/packages/SystemUI/res/layout/qs_media_panel.xml b/packages/SystemUI/res/layout/media_view.xml
similarity index 85%
rename from packages/SystemUI/res/layout/qs_media_panel.xml
rename to packages/SystemUI/res/layout/media_view.xml
index bf06242..1a1fddb 100644
--- a/packages/SystemUI/res/layout/qs_media_panel.xml
+++ b/packages/SystemUI/res/layout/media_view.xml
@@ -16,7 +16,7 @@
   -->
 
 <!-- Layout for media controls inside QSPanel carousel -->
-<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<com.android.systemui.util.animation.TransitionLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:id="@+id/qs_media_controls"
     android:layout_width="match_parent"
@@ -24,18 +24,7 @@
     android:clipChildren="false"
     android:clipToPadding="false"
     android:gravity="center_horizontal|fill_vertical"
-    app:layoutDescription="@xml/media_scene">
-
-    <View
-        android:id="@+id/media_background"
-        android:layout_width="0dp"
-        android:layout_height="0dp"
-        android:background="@drawable/qs_media_background"
-        app:layout_constraintEnd_toEndOf="@id/view_width"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintBottom_toBottomOf="parent"
-        />
+    android:background="@drawable/qs_media_background">
 
     <FrameLayout
         android:id="@+id/notification_media_progress_time"
@@ -185,15 +174,5 @@
     <!-- Buttons to remove this view when no longer needed -->
     <include
         layout="@layout/qs_media_panel_options"
-        android:visibility="gone"
-        app:layout_constraintEnd_toEndOf="@id/view_width"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent" />
-
-    <androidx.constraintlayout.widget.Guideline
-        android:id="@+id/view_width"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:orientation="vertical"
-        app:layout_constraintGuide_begin="300dp" />
-</androidx.constraintlayout.motion.widget.MotionLayout>
+        android:visibility="gone" />
+</com.android.systemui.util.animation.TransitionLayout>
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index 09918e7..d47ad7f 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -90,6 +90,8 @@
     <item type="id" name="view_width_animator_end_tag"/>
     <item type="id" name="view_width_current_value"/>
 
+    <item type="id" name="requires_remeasuring"/>
+
     <!-- Whether the icon is from a notification for which targetSdk < L -->
     <item type="id" name="icon_is_pre_L"/>
 
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 52ace2b..8102043 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -573,6 +573,7 @@
         <item name="android:textColor">?attr/wallpaperTextColor</item>
         <item name="android:textAllCaps">false</item>
         <item name="android:textSize">14sp</item>
+        <item name="android:minWidth">0dp</item>
     </style>
 
     <style name="TextAppearance.HeadsUpStatusBarText"
diff --git a/packages/SystemUI/res/xml/media_collapsed.xml b/packages/SystemUI/res/xml/media_collapsed.xml
new file mode 100644
index 0000000..57e6f36
--- /dev/null
+++ b/packages/SystemUI/res/xml/media_collapsed.xml
@@ -0,0 +1,184 @@
+<?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
+  -->
+<ConstraintSet
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+    <Constraint
+        android:id="@+id/icon"
+        android:layout_width="16dp"
+        android:layout_height="16dp"
+        android:layout_marginStart="18dp"
+        android:layout_marginTop="22dp"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        />
+
+    <Constraint
+        android:id="@+id/app_name"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="10dp"
+        android:layout_marginStart="10dp"
+        android:layout_marginTop="20dp"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintStart_toEndOf="@id/icon"
+        app:layout_constraintEnd_toStartOf="@id/media_seamless"
+        app:layout_constraintHorizontal_bias="0"
+        />
+
+    <Constraint
+        android:id="@+id/media_seamless"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintWidth_min="60dp"
+        android:layout_marginTop="@dimen/qs_media_panel_outer_padding"
+        android:layout_marginEnd="@dimen/qs_media_panel_outer_padding"
+        />
+
+    <Constraint
+        android:id="@+id/album_art"
+        android:layout_width="@dimen/qs_media_album_size"
+        android:layout_height="@dimen/qs_media_album_size"
+        android:layout_marginTop="16dp"
+        android:layout_marginStart="@dimen/qs_media_panel_outer_padding"
+        android:layout_marginBottom="24dp"
+        app:layout_constraintTop_toBottomOf="@id/icon"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        />
+
+    <!-- Song name -->
+    <Constraint
+        android:id="@+id/header_title"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="17dp"
+        android:layout_marginStart="16dp"
+        app:layout_constraintTop_toBottomOf="@id/app_name"
+        app:layout_constraintBottom_toTopOf="@id/header_artist"
+        app:layout_constraintStart_toEndOf="@id/album_art"
+        app:layout_constraintEnd_toStartOf="@id/action0"
+        app:layout_constraintHorizontal_bias="0"/>
+
+    <!-- Artist name -->
+    <Constraint
+        android:id="@+id/header_artist"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="3dp"
+        android:layout_marginBottom="24dp"
+        app:layout_constraintTop_toBottomOf="@id/header_title"
+        app:layout_constraintStart_toStartOf="@id/header_title"
+        app:layout_constraintEnd_toStartOf="@id/action0"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintHorizontal_bias="0"/>
+
+    <!-- Seek Bar -->
+    <Constraint
+        android:id="@+id/media_progress_bar"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:alpha="0.0"
+        app:layout_constraintTop_toBottomOf="@id/album_art"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        android:visibility="gone"
+        />
+
+    <Constraint
+        android:id="@+id/notification_media_progress_time"
+        android:alpha="0.0"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="35dp"
+        android:layout_marginEnd="@dimen/qs_media_panel_outer_padding"
+        android:layout_marginStart="@dimen/qs_media_panel_outer_padding"
+        app:layout_constraintTop_toBottomOf="@id/album_art"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        android:visibility="gone"
+        />
+
+    <Constraint
+        android:id="@+id/action0"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        android:layout_marginStart="4dp"
+        android:layout_marginTop="16dp"
+        android:visibility="gone"
+        app:layout_constraintHorizontal_chainStyle="packed"
+        app:layout_constraintTop_toBottomOf="@id/app_name"
+        app:layout_constraintLeft_toRightOf="@id/header_title"
+        app:layout_constraintRight_toLeftOf="@id/action1"
+        >
+    </Constraint>
+
+    <Constraint
+        android:id="@+id/action1"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        android:layout_marginStart="4dp"
+        android:layout_marginEnd="4dp"
+        android:layout_marginTop="18dp"
+        app:layout_constraintTop_toBottomOf="@id/app_name"
+        app:layout_constraintLeft_toRightOf="@id/action0"
+        app:layout_constraintRight_toLeftOf="@id/action2"
+        >
+    </Constraint>
+
+    <Constraint
+        android:id="@+id/action2"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        android:layout_marginStart="4dp"
+        android:layout_marginEnd="4dp"
+        android:layout_marginTop="18dp"
+        app:layout_constraintTop_toBottomOf="@id/app_name"
+        app:layout_constraintLeft_toRightOf="@id/action1"
+        app:layout_constraintRight_toLeftOf="@id/action3"
+        >
+    </Constraint>
+
+    <Constraint
+        android:id="@+id/action3"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        android:layout_marginStart="4dp"
+        android:layout_marginEnd="4dp"
+        android:layout_marginTop="18dp"
+        app:layout_constraintTop_toBottomOf="@id/app_name"
+        app:layout_constraintLeft_toRightOf="@id/action2"
+        app:layout_constraintRight_toLeftOf="@id/action4"
+        >
+    </Constraint>
+
+    <Constraint
+        android:id="@+id/action4"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        android:layout_marginStart="4dp"
+        android:layout_marginEnd="4dp"
+        android:visibility="gone"
+        android:layout_marginTop="18dp"
+        app:layout_constraintTop_toBottomOf="@id/app_name"
+        app:layout_constraintLeft_toRightOf="@id/action3"
+        app:layout_constraintRight_toRightOf="parent"
+        >
+    </Constraint>
+</ConstraintSet>
diff --git a/packages/SystemUI/res/xml/media_expanded.xml b/packages/SystemUI/res/xml/media_expanded.xml
new file mode 100644
index 0000000..78973f3
--- /dev/null
+++ b/packages/SystemUI/res/xml/media_expanded.xml
@@ -0,0 +1,178 @@
+<?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
+  -->
+<ConstraintSet
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+    <Constraint
+        android:id="@+id/icon"
+        android:layout_width="16dp"
+        android:layout_height="16dp"
+        android:layout_marginStart="18dp"
+        android:layout_marginTop="22dp"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        />
+
+    <Constraint
+        android:id="@+id/app_name"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="10dp"
+        android:layout_marginStart="10dp"
+        android:layout_marginTop="20dp"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintStart_toEndOf="@id/icon"
+        app:layout_constraintEnd_toStartOf="@id/media_seamless"
+        app:layout_constraintHorizontal_bias="0"
+        />
+
+    <Constraint
+        android:id="@+id/media_seamless"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintWidth_min="60dp"
+        android:layout_marginTop="@dimen/qs_media_panel_outer_padding"
+        android:layout_marginEnd="@dimen/qs_media_panel_outer_padding"
+        />
+
+    <Constraint
+        android:id="@+id/album_art"
+        android:layout_width="@dimen/qs_media_album_size"
+        android:layout_height="@dimen/qs_media_album_size"
+        android:layout_marginTop="14dp"
+        android:layout_marginStart="@dimen/qs_media_panel_outer_padding"
+        app:layout_constraintTop_toBottomOf="@+id/app_name"
+        app:layout_constraintStart_toStartOf="parent"
+        />
+
+    <!-- Song name -->
+    <Constraint
+        android:id="@+id/header_title"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="@dimen/qs_media_panel_outer_padding"
+        android:layout_marginTop="17dp"
+        android:layout_marginStart="16dp"
+        app:layout_constraintTop_toBottomOf="@+id/app_name"
+        app:layout_constraintStart_toEndOf="@id/album_art"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="0"/>
+
+    <!-- Artist name -->
+    <Constraint
+        android:id="@+id/header_artist"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="@dimen/qs_media_panel_outer_padding"
+        android:layout_marginTop="3dp"
+        app:layout_constraintTop_toBottomOf="@id/header_title"
+        app:layout_constraintStart_toStartOf="@id/header_title"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="0"/>
+
+    <!-- Seek Bar -->
+    <Constraint
+        android:id="@+id/media_progress_bar"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="3dp"
+        app:layout_constraintTop_toBottomOf="@id/header_artist"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        />
+
+    <Constraint
+        android:id="@+id/notification_media_progress_time"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="38dp"
+        android:layout_marginEnd="@dimen/qs_media_panel_outer_padding"
+        android:layout_marginStart="@dimen/qs_media_panel_outer_padding"
+        app:layout_constraintTop_toBottomOf="@id/header_artist"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        />
+
+    <Constraint
+        android:id="@+id/action0"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        android:layout_marginTop="5dp"
+        android:layout_marginStart="4dp"
+        android:layout_marginEnd="4dp"
+        android:layout_marginBottom="@dimen/qs_media_panel_outer_padding"
+        app:layout_constraintHorizontal_chainStyle="packed"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toLeftOf="@id/action1"
+        app:layout_constraintTop_toBottomOf="@id/notification_media_progress_time"
+        app:layout_constraintBottom_toBottomOf="parent">
+    </Constraint>
+
+    <Constraint
+        android:id="@+id/action1"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        android:layout_marginStart="4dp"
+        android:layout_marginEnd="4dp"
+        android:layout_marginBottom="@dimen/qs_media_panel_outer_padding"
+        app:layout_constraintLeft_toRightOf="@id/action0"
+        app:layout_constraintRight_toLeftOf="@id/action2"
+        app:layout_constraintTop_toTopOf="@id/action0"
+        app:layout_constraintBottom_toBottomOf="parent">
+    </Constraint>
+
+    <Constraint
+        android:id="@+id/action2"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        android:layout_marginStart="4dp"
+        android:layout_marginEnd="4dp"
+        android:layout_marginBottom="@dimen/qs_media_panel_outer_padding"
+        app:layout_constraintLeft_toRightOf="@id/action1"
+        app:layout_constraintRight_toLeftOf="@id/action3"
+        app:layout_constraintTop_toTopOf="@id/action0"
+        app:layout_constraintBottom_toBottomOf="parent">
+    </Constraint>
+
+    <Constraint
+        android:id="@+id/action3"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        android:layout_marginStart="4dp"
+        android:layout_marginEnd="4dp"
+        app:layout_constraintLeft_toRightOf="@id/action2"
+        app:layout_constraintRight_toLeftOf="@id/action4"
+        app:layout_constraintTop_toTopOf="@id/action0"
+        android:layout_marginBottom="@dimen/qs_media_panel_outer_padding"
+        app:layout_constraintBottom_toBottomOf="parent">
+    </Constraint>
+
+    <Constraint
+        android:id="@+id/action4"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        android:layout_marginStart="4dp"
+        android:layout_marginEnd="4dp"
+        android:layout_marginBottom="@dimen/qs_media_panel_outer_padding"
+        app:layout_constraintLeft_toRightOf="@id/action3"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="@id/action0"
+        app:layout_constraintBottom_toBottomOf="parent">
+    </Constraint>
+</ConstraintSet>
diff --git a/packages/SystemUI/res/xml/media_scene.xml b/packages/SystemUI/res/xml/media_scene.xml
deleted file mode 100644
index f61b2b0..0000000
--- a/packages/SystemUI/res/xml/media_scene.xml
+++ /dev/null
@@ -1,447 +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
-  -->
-<MotionScene
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto">
-
-    <Transition
-        app:constraintSetStart="@id/collapsed"
-        app:constraintSetEnd="@id/expanded"
-        app:duration="1000" >
-        <KeyFrameSet >
-            <KeyPosition
-                app:motionTarget="@+id/action0"
-                app:keyPositionType="pathRelative"
-                app:framePosition="70"
-                app:sizePercent="0.9" />
-            <KeyPosition
-                app:motionTarget="@+id/action1"
-                app:keyPositionType="pathRelative"
-                app:framePosition="70"
-                app:sizePercent="0.9" />
-            <KeyPosition
-                app:motionTarget="@+id/action2"
-                app:keyPositionType="pathRelative"
-                app:framePosition="70"
-                app:sizePercent="0.9" />
-            <KeyPosition
-                app:motionTarget="@+id/action3"
-                app:keyPositionType="pathRelative"
-                app:framePosition="70"
-                app:sizePercent="0.9" />
-            <KeyPosition
-                app:motionTarget="@+id/action4"
-                app:keyPositionType="pathRelative"
-                app:framePosition="70"
-                app:sizePercent="0.9" />
-            <KeyPosition
-                app:motionTarget="@+id/media_progress_bar"
-                app:keyPositionType="pathRelative"
-                app:framePosition="70"
-                app:sizePercent="0.9" />
-            <KeyAttribute
-                app:motionTarget="@id/media_progress_bar"
-                app:framePosition="0"
-                android:alpha="0.0" />
-            <KeyAttribute
-                app:motionTarget="@+id/media_progress_bar"
-                app:framePosition="70"
-                android:alpha="0.0"/>
-            <KeyPosition
-                app:motionTarget="@+id/notification_media_progress_time"
-                app:keyPositionType="pathRelative"
-                app:framePosition="70"
-                app:sizePercent="0.9" />
-            <KeyAttribute
-                app:motionTarget="@id/notification_media_progress_time"
-                app:framePosition="0"
-                android:alpha="0.0" />
-            <KeyAttribute
-                app:motionTarget="@+id/notification_media_progress_time"
-                app:framePosition="70"
-                android:alpha="0.0"/>
-            <KeyAttribute
-                app:motionTarget="@id/action0"
-                app:framePosition="0"
-                android:alpha="0.0" />
-            <KeyAttribute
-                app:motionTarget="@+id/action0"
-                app:framePosition="70"
-                android:alpha="0.0"/>
-            <KeyAttribute
-                app:motionTarget="@id/action1"
-                app:framePosition="0"
-                android:alpha="0.0" />
-            <KeyAttribute
-                app:motionTarget="@+id/action1"
-                app:framePosition="70"
-                android:alpha="0.0"/>
-            <KeyAttribute
-                app:motionTarget="@id/action2"
-                app:framePosition="0"
-                android:alpha="0.0" />
-            <KeyAttribute
-                app:motionTarget="@+id/action2"
-                app:framePosition="70"
-                android:alpha="0.0"/>
-            <KeyAttribute
-                app:motionTarget="@id/action3"
-                app:framePosition="0"
-                android:alpha="0.0" />
-            <KeyAttribute
-                app:motionTarget="@+id/action3"
-                app:framePosition="70"
-                android:alpha="0.0"/>
-            <KeyAttribute
-                app:motionTarget="@id/action4"
-                app:framePosition="0"
-                android:alpha="0.0" />
-            <KeyAttribute
-                app:motionTarget="@+id/action4"
-                app:framePosition="70"
-                android:alpha="0.0"/>
-        </KeyFrameSet>
-    </Transition>
-
-    <ConstraintSet android:id="@+id/expanded">
-        <Constraint
-            android:id="@+id/icon"
-            android:layout_width="16dp"
-            android:layout_height="16dp"
-            android:layout_marginStart="18dp"
-            android:layout_marginTop="22dp"
-            app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintStart_toStartOf="parent"
-            />
-
-        <Constraint
-            android:id="@+id/app_name"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:layout_marginEnd="10dp"
-            android:layout_marginStart="10dp"
-            android:layout_marginTop="20dp"
-            app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintStart_toEndOf="@id/icon"
-            app:layout_constraintEnd_toStartOf="@id/media_seamless"
-            app:layout_constraintHorizontal_bias="0"
-            />
-
-        <Constraint
-            android:id="@+id/media_seamless"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            app:layout_constraintEnd_toEndOf="@id/view_width"
-            app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintWidth_min="60dp"
-            android:layout_marginTop="@dimen/qs_media_panel_outer_padding"
-            android:layout_marginEnd="@dimen/qs_media_panel_outer_padding"
-            />
-
-        <Constraint
-            android:id="@+id/album_art"
-            android:layout_width="@dimen/qs_media_album_size"
-            android:layout_height="@dimen/qs_media_album_size"
-            android:layout_marginTop="14dp"
-            android:layout_marginStart="@dimen/qs_media_panel_outer_padding"
-            app:layout_constraintTop_toBottomOf="@+id/app_name"
-            app:layout_constraintStart_toStartOf="parent"
-            />
-
-        <!-- Song name -->
-        <Constraint
-            android:id="@+id/header_title"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:layout_marginEnd="@dimen/qs_media_panel_outer_padding"
-            android:layout_marginTop="17dp"
-            android:layout_marginStart="16dp"
-            app:layout_constraintTop_toBottomOf="@+id/app_name"
-            app:layout_constraintStart_toEndOf="@id/album_art"
-            app:layout_constraintEnd_toEndOf="@id/view_width"
-            app:layout_constraintHorizontal_bias="0"/>
-
-        <!-- Artist name -->
-        <Constraint
-            android:id="@+id/header_artist"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:layout_marginEnd="@dimen/qs_media_panel_outer_padding"
-            android:layout_marginTop="3dp"
-            app:layout_constraintTop_toBottomOf="@id/header_title"
-            app:layout_constraintStart_toStartOf="@id/header_title"
-            app:layout_constraintEnd_toEndOf="@id/view_width"
-            app:layout_constraintHorizontal_bias="0"/>
-
-        <!-- Seek Bar -->
-        <Constraint
-            android:id="@+id/media_progress_bar"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="3dp"
-            app:layout_constraintTop_toBottomOf="@id/header_artist"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintEnd_toEndOf="@id/view_width"
-            />
-
-        <Constraint
-            android:id="@+id/notification_media_progress_time"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="38dp"
-            android:layout_marginEnd="@dimen/qs_media_panel_outer_padding"
-            android:layout_marginStart="@dimen/qs_media_panel_outer_padding"
-            app:layout_constraintTop_toBottomOf="@id/header_artist"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintEnd_toEndOf="@id/view_width"
-            />
-
-        <Constraint
-            android:id="@+id/action0"
-            android:layout_width="48dp"
-            android:layout_height="48dp"
-            android:layout_marginTop="5dp"
-            android:layout_marginStart="4dp"
-            android:layout_marginEnd="4dp"
-            android:layout_marginBottom="@dimen/qs_media_panel_outer_padding"
-            app:layout_constraintHorizontal_chainStyle="packed"
-            app:layout_constraintLeft_toLeftOf="parent"
-            app:layout_constraintRight_toLeftOf="@id/action1"
-            app:layout_constraintTop_toBottomOf="@id/notification_media_progress_time"
-            app:layout_constraintBottom_toBottomOf="parent">
-        </Constraint>
-
-        <Constraint
-            android:id="@+id/action1"
-            android:layout_width="48dp"
-            android:layout_height="48dp"
-            android:layout_marginStart="4dp"
-            android:layout_marginEnd="4dp"
-            android:layout_marginBottom="@dimen/qs_media_panel_outer_padding"
-            app:layout_constraintLeft_toRightOf="@id/action0"
-            app:layout_constraintRight_toLeftOf="@id/action2"
-            app:layout_constraintTop_toTopOf="@id/action0"
-            app:layout_constraintBottom_toBottomOf="parent">
-        </Constraint>
-
-        <Constraint
-            android:id="@+id/action2"
-            android:layout_width="48dp"
-            android:layout_height="48dp"
-            android:layout_marginStart="4dp"
-            android:layout_marginEnd="4dp"
-            android:layout_marginBottom="@dimen/qs_media_panel_outer_padding"
-            app:layout_constraintLeft_toRightOf="@id/action1"
-            app:layout_constraintRight_toLeftOf="@id/action3"
-            app:layout_constraintTop_toTopOf="@id/action0"
-            app:layout_constraintBottom_toBottomOf="parent">
-        </Constraint>
-
-        <Constraint
-            android:id="@+id/action3"
-            android:layout_width="48dp"
-            android:layout_height="48dp"
-            android:layout_marginStart="4dp"
-            android:layout_marginEnd="4dp"
-            app:layout_constraintLeft_toRightOf="@id/action2"
-            app:layout_constraintRight_toLeftOf="@id/action4"
-            app:layout_constraintTop_toTopOf="@id/action0"
-            android:layout_marginBottom="@dimen/qs_media_panel_outer_padding"
-            app:layout_constraintBottom_toBottomOf="parent">
-        </Constraint>
-
-        <Constraint
-            android:id="@+id/action4"
-            android:layout_width="48dp"
-            android:layout_height="48dp"
-            android:layout_marginStart="4dp"
-            android:layout_marginEnd="4dp"
-            android:layout_marginBottom="@dimen/qs_media_panel_outer_padding"
-            app:layout_constraintLeft_toRightOf="@id/action3"
-            app:layout_constraintRight_toRightOf="@id/view_width"
-            app:layout_constraintTop_toTopOf="@id/action0"
-            app:layout_constraintBottom_toBottomOf="parent">
-        </Constraint>
-    </ConstraintSet>
-
-    <ConstraintSet android:id="@+id/collapsed">
-        <Constraint
-            android:id="@+id/icon"
-            android:layout_width="16dp"
-            android:layout_height="16dp"
-            android:layout_marginStart="18dp"
-            android:layout_marginTop="22dp"
-            app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintStart_toStartOf="parent"
-            />
-
-        <Constraint
-            android:id="@+id/app_name"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:layout_marginEnd="10dp"
-            android:layout_marginStart="10dp"
-            android:layout_marginTop="20dp"
-            app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintStart_toEndOf="@id/icon"
-            app:layout_constraintEnd_toStartOf="@id/media_seamless"
-            app:layout_constraintHorizontal_bias="0"
-            />
-
-        <Constraint
-            android:id="@+id/media_seamless"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            app:layout_constraintEnd_toEndOf="@id/view_width"
-            app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintWidth_min="60dp"
-            android:layout_marginTop="@dimen/qs_media_panel_outer_padding"
-            android:layout_marginEnd="@dimen/qs_media_panel_outer_padding"
-            />
-
-        <Constraint
-            android:id="@+id/album_art"
-            android:layout_width="@dimen/qs_media_album_size"
-            android:layout_height="@dimen/qs_media_album_size"
-            android:layout_marginTop="16dp"
-            android:layout_marginStart="@dimen/qs_media_panel_outer_padding"
-            android:layout_marginBottom="24dp"
-            app:layout_constraintTop_toBottomOf="@id/icon"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintBottom_toBottomOf="parent"
-            />
-
-        <!-- Song name -->
-        <Constraint
-            android:id="@+id/header_title"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="17dp"
-            android:layout_marginStart="16dp"
-            app:layout_constraintTop_toBottomOf="@id/app_name"
-            app:layout_constraintBottom_toTopOf="@id/header_artist"
-            app:layout_constraintStart_toEndOf="@id/album_art"
-            app:layout_constraintEnd_toStartOf="@id/action0"
-            app:layout_constraintHorizontal_bias="0"/>
-
-        <!-- Artist name -->
-        <Constraint
-            android:id="@+id/header_artist"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="3dp"
-            android:layout_marginBottom="24dp"
-            app:layout_constraintTop_toBottomOf="@id/header_title"
-            app:layout_constraintStart_toStartOf="@id/header_title"
-            app:layout_constraintEnd_toStartOf="@id/action0"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintHorizontal_bias="0"/>
-
-        <!-- Seek Bar -->
-        <Constraint
-            android:id="@+id/media_progress_bar"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:alpha="0.0"
-            app:layout_constraintTop_toBottomOf="@id/album_art"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintEnd_toEndOf="@id/view_width"
-            android:visibility="gone"
-            />
-
-        <Constraint
-            android:id="@+id/notification_media_progress_time"
-            android:alpha="0.0"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="35dp"
-            android:layout_marginEnd="@dimen/qs_media_panel_outer_padding"
-            android:layout_marginStart="@dimen/qs_media_panel_outer_padding"
-            app:layout_constraintTop_toBottomOf="@id/album_art"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintEnd_toEndOf="@id/view_width"
-            android:visibility="gone"
-            />
-
-        <Constraint
-            android:id="@+id/action0"
-            android:layout_width="48dp"
-            android:layout_height="48dp"
-            android:layout_marginStart="4dp"
-            android:layout_marginTop="16dp"
-            android:visibility="gone"
-            app:layout_constraintHorizontal_chainStyle="packed"
-            app:layout_constraintTop_toBottomOf="@id/app_name"
-            app:layout_constraintLeft_toRightOf="@id/header_title"
-            app:layout_constraintRight_toLeftOf="@id/action1"
-            >
-        </Constraint>
-
-        <Constraint
-            android:id="@+id/action1"
-            android:layout_width="48dp"
-            android:layout_height="48dp"
-            android:layout_marginStart="4dp"
-            android:layout_marginEnd="4dp"
-            android:layout_marginTop="18dp"
-            app:layout_constraintTop_toBottomOf="@id/app_name"
-            app:layout_constraintLeft_toRightOf="@id/action0"
-            app:layout_constraintRight_toLeftOf="@id/action2"
-            >
-        </Constraint>
-
-        <Constraint
-            android:id="@+id/action2"
-            android:layout_width="48dp"
-            android:layout_height="48dp"
-            android:layout_marginStart="4dp"
-            android:layout_marginEnd="4dp"
-            android:layout_marginTop="18dp"
-            app:layout_constraintTop_toBottomOf="@id/app_name"
-            app:layout_constraintLeft_toRightOf="@id/action1"
-            app:layout_constraintRight_toLeftOf="@id/action3"
-            >
-        </Constraint>
-
-        <Constraint
-            android:id="@+id/action3"
-            android:layout_width="48dp"
-            android:layout_height="48dp"
-            android:layout_marginStart="4dp"
-            android:layout_marginEnd="4dp"
-            android:layout_marginTop="18dp"
-            app:layout_constraintTop_toBottomOf="@id/app_name"
-            app:layout_constraintLeft_toRightOf="@id/action2"
-            app:layout_constraintRight_toLeftOf="@id/action4"
-            >
-        </Constraint>
-
-        <Constraint
-            android:id="@+id/action4"
-            android:layout_width="48dp"
-            android:layout_height="48dp"
-            android:layout_marginStart="4dp"
-            android:layout_marginEnd="4dp"
-            android:visibility="gone"
-            android:layout_marginTop="18dp"
-            app:layout_constraintTop_toBottomOf="@id/app_name"
-            app:layout_constraintLeft_toRightOf="@id/action3"
-            app:layout_constraintRight_toRightOf="@id/view_width"
-            >
-        </Constraint>
-    </ConstraintSet>
-</MotionScene>
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
index f1b401e..ecd8b45 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
@@ -57,7 +57,6 @@
 
     private NotificationEntry mEntry;
     private final String mKey;
-    private final String mGroupId;
 
     private long mLastUpdated;
     private long mLastAccessed;
@@ -69,6 +68,8 @@
 
     /** Whether flyout text should be suppressed, regardless of any other flags or state. */
     private boolean mSuppressFlyout;
+    /** Whether this bubble should auto expand regardless of the normal flag, used for overflow. */
+    private boolean mShouldAutoExpand;
 
     // Items that are typically loaded later
     private String mAppName;
@@ -96,17 +97,10 @@
     private int mDotColor;
     private Path mDotPath;
 
-
-    public static String groupId(NotificationEntry entry) {
-        UserHandle user = entry.getSbn().getUser();
-        return user.getIdentifier() + "|" + entry.getSbn().getPackageName();
-    }
-
     // TODO: Decouple Bubble from NotificationEntry and transform ShortcutInfo into Bubble
     Bubble(ShortcutInfo shortcutInfo) {
         mShortcutInfo = shortcutInfo;
         mKey = shortcutInfo.getId();
-        mGroupId = shortcutInfo.getId();
     }
 
     /** Used in tests when no UI is required. */
@@ -116,7 +110,6 @@
         mEntry = e;
         mKey = e.getKey();
         mLastUpdated = e.getSbn().getPostTime();
-        mGroupId = groupId(e);
         mSuppressionListener = listener;
     }
 
@@ -129,10 +122,6 @@
         return mEntry;
     }
 
-    public String getGroupId() {
-        return mGroupId;
-    }
-
     public String getPackageName() {
         return mEntry.getSbn().getPackageName();
     }
@@ -297,20 +286,13 @@
     }
 
     /**
-     * @return the newer of {@link #getLastUpdateTime()} and {@link #getLastAccessTime()}
+     * @return the last time this bubble was updated or accessed, whichever is most recent.
      */
     long getLastActivity() {
         return Math.max(mLastUpdated, mLastAccessed);
     }
 
     /**
-     * @return the timestamp in milliseconds of the most recent notification entry for this bubble
-     */
-    long getLastUpdateTime() {
-        return mLastUpdated;
-    }
-
-    /**
      * @return if the bubble was ever expanded
      */
     boolean getWasAccessed() {
@@ -411,15 +393,6 @@
         return mFlyoutMessage;
     }
 
-    /**
-     * Returns whether the notification for this bubble is a foreground service. It shows that this
-     * is an ongoing bubble.
-     */
-    boolean isOngoing() {
-        int flags = mEntry.getSbn().getNotification().flags;
-        return (flags & Notification.FLAG_FOREGROUND_SERVICE) != 0;
-    }
-
     float getDesiredHeight(Context context) {
         Notification.BubbleMetadata data = mEntry.getBubbleMetadata();
         boolean useRes = data.getDesiredHeightResId() != 0;
@@ -499,7 +472,11 @@
 
     boolean shouldAutoExpand() {
         Notification.BubbleMetadata metadata = mEntry.getBubbleMetadata();
-        return metadata != null && metadata.getAutoExpandBubble();
+        return (metadata != null && metadata.getAutoExpandBubble()) ||  mShouldAutoExpand;
+    }
+
+    void setShouldAutoExpand(boolean shouldAutoExpand) {
+        mShouldAutoExpand = shouldAutoExpand;
     }
 
     @Override
@@ -562,7 +539,7 @@
                     normalX,
                     normalY,
                     this.showInShade(),
-                    this.isOngoing(),
+                    false /* isOngoing (unused) */,
                     false /* isAppForeground (unused) */);
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index eb4ba6f..5f157c1 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -729,8 +729,9 @@
                 mNotificationEntryManager.getActiveNotificationsForCurrentUser()) {
             if (savedBubbleKeys.contains(e.getKey())
                     && mNotificationInterruptStateProvider.shouldBubbleUp(e)
+                    && e.isBubble()
                     && canLaunchInActivityView(mContext, e)) {
-                updateBubble(e, /* suppressFlyout= */ true);
+                updateBubble(e, true /* suppressFlyout */, false /* showInShade */);
             }
         }
         // Finally, remove the entries for this user now that bubbles are restored.
@@ -844,25 +845,34 @@
 
     void promoteBubbleFromOverflow(Bubble bubble) {
         bubble.setInflateSynchronously(mInflateSynchronously);
-        setIsBubble(bubble, /* isBubble */ true);
+        setIsBubble(bubble.getEntry(), /* isBubble */ true);
         mBubbleData.promoteBubbleFromOverflow(bubble, mStackView, mBubbleIconFactory);
     }
 
     /**
      * Request the stack expand if needed, then select the specified Bubble as current.
+     * If no bubble exists for this entry, one is created.
      *
-     * @param notificationKey the notification key for the bubble to be selected
+     * @param entry the notification for the bubble to be selected
      */
-    public void expandStackAndSelectBubble(String notificationKey) {
-        Bubble bubble = mBubbleData.getBubbleInStackWithKey(notificationKey);
-        if (bubble == null) {
-            bubble = mBubbleData.getOverflowBubbleWithKey(notificationKey);
-            if (bubble != null) {
-                mBubbleData.promoteBubbleFromOverflow(bubble, mStackView, mBubbleIconFactory);
-            }
-        } else if (bubble.getEntry().isBubble()){
+    public void expandStackAndSelectBubble(NotificationEntry entry) {
+        String key = entry.getKey();
+        Bubble bubble = mBubbleData.getBubbleInStackWithKey(key);
+        if (bubble != null) {
             mBubbleData.setSelectedBubble(bubble);
+        } else {
+            bubble = mBubbleData.getOverflowBubbleWithKey(key);
+            if (bubble != null) {
+                bubble.setShouldAutoExpand(true);
+                promoteBubbleFromOverflow(bubble);
+            } else if (entry.canBubble()) {
+                // It can bubble but it's not -- it got aged out of the overflow before it
+                // was dismissed or opened, make it a bubble again.
+                setIsBubble(entry, true);
+                updateBubble(entry, true /* suppressFlyout */, false /* showInShade */);
+            }
         }
+
         mBubbleData.setExpanded(true);
     }
 
@@ -882,11 +892,7 @@
      * @param notif the notification associated with this bubble.
      */
     void updateBubble(NotificationEntry notif) {
-        updateBubble(notif, false /* suppressFlyout */);
-    }
-
-    void updateBubble(NotificationEntry notif, boolean suppressFlyout) {
-        updateBubble(notif, suppressFlyout, true /* showInShade */);
+        updateBubble(notif, false /* suppressFlyout */, true /* showInShade */);
     }
 
     void updateBubble(NotificationEntry notif, boolean suppressFlyout, boolean showInShade) {
@@ -901,7 +907,8 @@
         bubble.setInflateSynchronously(mInflateSynchronously);
         bubble.inflate(
                 b -> {
-                    mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade);
+                    mBubbleData.notificationEntryUpdated(b, suppressFlyout,
+                            showInShade);
                     if (bubble.getBubbleIntent() == null) {
                         return;
                     }
@@ -979,18 +986,20 @@
 
     private void onEntryAdded(NotificationEntry entry) {
         if (mNotificationInterruptStateProvider.shouldBubbleUp(entry)
+                && entry.isBubble()
                 && canLaunchInActivityView(mContext, entry)) {
             updateBubble(entry);
         }
     }
 
     private void onEntryUpdated(NotificationEntry entry) {
+        // shouldBubbleUp checks canBubble & for bubble metadata
         boolean shouldBubble = mNotificationInterruptStateProvider.shouldBubbleUp(entry)
                 && canLaunchInActivityView(mContext, entry);
         if (!shouldBubble && mBubbleData.hasAnyBubbleWithKey(entry.getKey())) {
             // It was previously a bubble but no longer a bubble -- lets remove it
             removeBubble(entry, DISMISS_NO_LONGER_BUBBLE);
-        } else if (shouldBubble) {
+        } else if (shouldBubble && entry.isBubble()) {
             updateBubble(entry);
         }
     }
@@ -1036,14 +1045,14 @@
         }
     }
 
-    private void setIsBubble(Bubble b, boolean isBubble) {
+    private void setIsBubble(NotificationEntry entry, boolean isBubble) {
         if (isBubble) {
-            b.getEntry().getSbn().getNotification().flags |= FLAG_BUBBLE;
+            entry.getSbn().getNotification().flags |= FLAG_BUBBLE;
         } else {
-            b.getEntry().getSbn().getNotification().flags &= ~FLAG_BUBBLE;
+            entry.getSbn().getNotification().flags &= ~FLAG_BUBBLE;
         }
         try {
-            mBarService.onNotificationBubbleChanged(b.getKey(), isBubble, 0);
+            mBarService.onNotificationBubbleChanged(entry.getKey(), isBubble, 0);
         } catch (RemoteException e) {
             // Bad things have happened
         }
@@ -1092,7 +1101,7 @@
                         }
                     } else {
                         if (bubble.getEntry().isBubble() && bubble.showInShade()) {
-                            setIsBubble(bubble, /* isBubble */ false);
+                            setIsBubble(bubble.getEntry(), false /* isBubble */);
                         }
                         if (bubble.getEntry().getRow() != null) {
                             bubble.getEntry().getRow().updateBubbleButton();
@@ -1327,7 +1336,8 @@
                 boolean clearedTask, boolean wasVisible) {
             for (Bubble b : mBubbleData.getBubbles()) {
                 if (b.getDisplayId() == task.displayId) {
-                    expandStackAndSelectBubble(b.getKey());
+                    mBubbleData.setSelectedBubble(b);
+                    mBubbleData.setExpanded(true);
                     return;
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
index 65d5beb..35647b0 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
@@ -21,12 +21,9 @@
 import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES;
 import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
 
-import static java.util.stream.Collectors.toList;
-
 import android.app.Notification;
 import android.app.PendingIntent;
 import android.content.Context;
-import android.service.notification.NotificationListenerService;
 import android.util.Log;
 import android.util.Pair;
 import android.view.View;
@@ -45,7 +42,6 @@
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 import java.util.Objects;
 
 import javax.inject.Inject;
@@ -62,9 +58,6 @@
     private static final Comparator<Bubble> BUBBLES_BY_SORT_KEY_DESCENDING =
             Comparator.comparing(BubbleData::sortKey).reversed();
 
-    private static final Comparator<Map.Entry<String, Long>> GROUPS_BY_MAX_SORT_KEY_DESCENDING =
-            Comparator.<Map.Entry<String, Long>, Long>comparing(Map.Entry::getValue).reversed();
-
     /** Contains information about changes that have been made to the state of bubbles. */
     static final class Update {
         boolean expandedChanged;
@@ -129,8 +122,6 @@
     // State tracked during an operation -- keeps track of what listener events to dispatch.
     private Update mStateChange;
 
-    private NotificationListenerService.Ranking mTmpRanking;
-
     private TimeSource mTimeSource = System::currentTimeMillis;
 
     @Nullable
@@ -216,15 +207,14 @@
         }
         moveOverflowBubbleToPending(bubble);
         // Preserve new order for next repack, which sorts by last updated time.
-        bubble.markUpdatedAt(mTimeSource.currentTimeMillis());
         bubble.inflate(
                 b -> {
-                    notificationEntryUpdated(bubble, /* suppressFlyout */
-                            false, /* showInShade */ true);
-                    setSelectedBubble(bubble);
+                    b.setShouldAutoExpand(true);
+                    b.markUpdatedAt(mTimeSource.currentTimeMillis());
+                    notificationEntryUpdated(bubble, false /* suppressFlyout */,
+                            true /* showInShade */);
                 },
                 mContext, stack, factory);
-        dispatchPendingChanges();
     }
 
     void setShowingOverflow(boolean showingOverflow) {
@@ -290,13 +280,13 @@
             bubble.setSuppressFlyout(suppressFlyout);
             doUpdate(bubble);
         }
+
         if (bubble.shouldAutoExpand()) {
+            bubble.setShouldAutoExpand(false);
             setSelectedBubbleInternal(bubble);
             if (!mExpanded) {
                 setExpandedInternal(true);
             }
-        } else if (mSelectedBubble == null) {
-            setSelectedBubbleInternal(bubble);
         }
 
         boolean isBubbleExpandedAndSelected = mExpanded && mSelectedBubble == bubble;
@@ -370,20 +360,11 @@
         if (DEBUG_BUBBLE_DATA) {
             Log.d(TAG, "doAdd: " + bubble);
         }
-        int minInsertPoint = 0;
-        boolean newGroup = !hasBubbleWithGroupId(bubble.getGroupId());
-        if (isExpanded()) {
-            // first bubble of a group goes to the beginning, otherwise within the existing group
-            minInsertPoint = newGroup ? 0 : findFirstIndexForGroup(bubble.getGroupId());
-        }
-        if (insertBubble(minInsertPoint, bubble) < mBubbles.size() - 1) {
-            mStateChange.orderChanged = true;
-        }
+        mBubbles.add(0, bubble);
         mStateChange.addedBubble = bubble;
-
+        // Adding the first bubble doesn't change the order
+        mStateChange.orderChanged = mBubbles.size() > 1;
         if (!isExpanded()) {
-            mStateChange.orderChanged |= packGroup(findFirstIndexForGroup(bubble.getGroupId()));
-            // Top bubble becomes selected.
             setSelectedBubbleInternal(mBubbles.get(0));
         }
     }
@@ -406,14 +387,10 @@
         }
         mStateChange.updatedBubble = bubble;
         if (!isExpanded()) {
-            // while collapsed, update causes re-pack
             int prevPos = mBubbles.indexOf(bubble);
             mBubbles.remove(bubble);
-            int newPos = insertBubble(0, bubble);
-            if (prevPos != newPos) {
-                packGroup(newPos);
-                mStateChange.orderChanged = true;
-            }
+            mBubbles.add(0, bubble);
+            mStateChange.orderChanged = prevPos != 0;
             setSelectedBubbleInternal(mBubbles.get(0));
         }
     }
@@ -581,7 +558,6 @@
                 Log.e(TAG, "Attempt to expand stack without selected bubble!");
                 return;
             }
-            mSelectedBubble.markUpdatedAt(mTimeSource.currentTimeMillis());
             mSelectedBubble.markAsAccessedAt(mTimeSource.currentTimeMillis());
             mStateChange.orderChanged |= repackAll();
         } else if (!mBubbles.isEmpty()) {
@@ -596,17 +572,11 @@
             }
             if (mBubbles.indexOf(mSelectedBubble) > 0) {
                 // Move the selected bubble to the top while collapsed.
-                if (!mSelectedBubble.isOngoing() && mBubbles.get(0).isOngoing()) {
-                    // The selected bubble cannot be raised to the first position because
-                    // there is an ongoing bubble there. Instead, force the top ongoing bubble
-                    // to become selected.
-                    setSelectedBubbleInternal(mBubbles.get(0));
-                } else {
-                    // Raise the selected bubble (and it's group) up to the front so the selected
-                    // bubble remains on top.
+                int index = mBubbles.indexOf(mSelectedBubble);
+                if (index != 0) {
                     mBubbles.remove(mSelectedBubble);
                     mBubbles.add(0, mSelectedBubble);
-                    mStateChange.orderChanged |= packGroup(0);
+                    mStateChange.orderChanged = true;
                 }
             }
         }
@@ -616,91 +586,12 @@
     }
 
     private static long sortKey(Bubble bubble) {
-        long key = bubble.getLastUpdateTime();
-        if (bubble.isOngoing()) {
-            // Set 2nd highest bit (signed long int), to partition between ongoing and regular
-            key |= 0x4000000000000000L;
-        }
-        return key;
+        return bubble.getLastActivity();
     }
 
     /**
-     * Locates and inserts the bubble into a sorted position. The is inserted
-     * based on sort key, groupId is not considered. A call to {@link #packGroup(int)} may be
-     * required to keep grouping intact.
-     *
-     * @param minPosition the first insert point to consider
-     * @param newBubble   the bubble to insert
-     * @return the position where the bubble was inserted
-     */
-    private int insertBubble(int minPosition, Bubble newBubble) {
-        long newBubbleSortKey = sortKey(newBubble);
-        String previousGroupId = null;
-
-        for (int pos = minPosition; pos < mBubbles.size(); pos++) {
-            Bubble bubbleAtPos = mBubbles.get(pos);
-            String groupIdAtPos = bubbleAtPos.getGroupId();
-            boolean atStartOfGroup = !groupIdAtPos.equals(previousGroupId);
-
-            if (atStartOfGroup && newBubbleSortKey > sortKey(bubbleAtPos)) {
-                // Insert before the start of first group which has older bubbles.
-                mBubbles.add(pos, newBubble);
-                return pos;
-            }
-            previousGroupId = groupIdAtPos;
-        }
-        mBubbles.add(newBubble);
-        return mBubbles.size() - 1;
-    }
-
-    private boolean hasBubbleWithGroupId(String groupId) {
-        return mBubbles.stream().anyMatch(b -> b.getGroupId().equals(groupId));
-    }
-
-    private int findFirstIndexForGroup(String appId) {
-        for (int i = 0; i < mBubbles.size(); i++) {
-            Bubble bubbleAtPos = mBubbles.get(i);
-            if (bubbleAtPos.getGroupId().equals(appId)) {
-                return i;
-            }
-        }
-        return 0;
-    }
-
-    /**
-     * Starting at the given position, moves all bubbles with the same group id to follow. Bubbles
-     * at positions lower than {@code position} are unchanged. Relative order within the group
-     * unchanged. Relative order of any other bubbles are also unchanged.
-     *
-     * @param position the position of the first bubble for the group
-     * @return true if the position of any bubbles has changed as a result
-     */
-    private boolean packGroup(int position) {
-        if (DEBUG_BUBBLE_DATA) {
-            Log.d(TAG, "packGroup: position=" + position);
-        }
-        Bubble groupStart = mBubbles.get(position);
-        final String groupAppId = groupStart.getGroupId();
-        List<Bubble> moving = new ArrayList<>();
-
-        // Walk backward, collect bubbles within the group
-        for (int i = mBubbles.size() - 1; i > position; i--) {
-            if (mBubbles.get(i).getGroupId().equals(groupAppId)) {
-                moving.add(0, mBubbles.get(i));
-            }
-        }
-        if (moving.isEmpty()) {
-            return false;
-        }
-        mBubbles.removeAll(moving);
-        mBubbles.addAll(position + 1, moving);
-        return true;
-    }
-
-    /**
-     * This applies a full sort and group pass to all existing bubbles. The bubbles are grouped
-     * by groupId. Each group is then sorted by the max(lastUpdated) time of its bubbles. Bubbles
-     * within each group are then sorted by lastUpdated descending.
+     * This applies a full sort and group pass to all existing bubbles.
+     * Bubbles are sorted by lastUpdated descending.
      *
      * @return true if the position of any bubbles changed as a result
      */
@@ -711,31 +602,11 @@
         if (mBubbles.isEmpty()) {
             return false;
         }
-        Map<String, Long> groupLastActivity = new HashMap<>();
-        for (Bubble bubble : mBubbles) {
-            long maxSortKeyForGroup = groupLastActivity.getOrDefault(bubble.getGroupId(), 0L);
-            long sortKeyForBubble = sortKey(bubble);
-            if (sortKeyForBubble > maxSortKeyForGroup) {
-                groupLastActivity.put(bubble.getGroupId(), sortKeyForBubble);
-            }
-        }
-
-        // Sort groups by their most recently active bubble
-        List<String> groupsByMostRecentActivity =
-                groupLastActivity.entrySet().stream()
-                        .sorted(GROUPS_BY_MAX_SORT_KEY_DESCENDING)
-                        .map(Map.Entry::getKey)
-                        .collect(toList());
-
         List<Bubble> repacked = new ArrayList<>(mBubbles.size());
-
-        // For each group, add bubbles, freshest to oldest
-        for (String appId : groupsByMostRecentActivity) {
-            mBubbles.stream()
-                    .filter((b) -> b.getGroupId().equals(appId))
-                    .sorted(BUBBLES_BY_SORT_KEY_DESCENDING)
-                    .forEachOrdered(repacked::add);
-        }
+        // Add bubbles, freshest to oldest
+        mBubbles.stream()
+                .sorted(BUBBLES_BY_SORT_KEY_DESCENDING)
+                .forEachOrdered(repacked::add);
         if (repacked.equals(mBubbles)) {
             return false;
         }
@@ -778,11 +649,12 @@
     public List<Bubble> getBubbles() {
         return Collections.unmodifiableList(mBubbles);
     }
+
     /**
      * The set of bubbles in overflow.
      */
     @VisibleForTesting(visibility = PRIVATE)
-    public List<Bubble> getOverflowBubbles() {
+    List<Bubble> getOverflowBubbles() {
         return Collections.unmodifiableList(mOverflowBubbles);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDebugConfig.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDebugConfig.java
index 19733a5..d98fee3 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDebugConfig.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDebugConfig.java
@@ -69,10 +69,10 @@
                         && selected.getKey() != BubbleOverflow.KEY
                         && bubble == selected);
                 String arrow = isSelected ? "=>" : "  ";
-                sb.append(String.format("%s Bubble{act=%12d, ongoing=%d, key=%s}\n",
+                sb.append(String.format("%s Bubble{act=%12d, showInShade=%d, key=%s}\n",
                         arrow,
                         bubble.getLastActivity(),
-                        (bubble.isOngoing() ? 1 : 0),
+                        (bubble.showInShade() ? 1 : 0),
                         bubble.getKey()));
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index bdfcb35..418cc50 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -49,7 +49,6 @@
 import android.graphics.RectF;
 import android.graphics.Region;
 import android.os.Bundle;
-import android.os.Vibrator;
 import android.provider.Settings;
 import android.service.notification.StatusBarNotification;
 import android.util.Log;
@@ -188,7 +187,6 @@
     private final SpringAnimation mExpandedViewYAnim;
     private final BubbleData mBubbleData;
 
-    private final Vibrator mVibrator;
     private final ValueAnimator mDesaturateAndDarkenAnimator;
     private final Paint mDesaturateAndDarkenPaint = new Paint();
 
@@ -701,8 +699,6 @@
         // We use the real size & subtract screen decorations / window insets ourselves when needed
         wm.getDefaultDisplay().getRealSize(mDisplaySize);
 
-        mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
-
         mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
         int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
 
@@ -2334,7 +2330,7 @@
                 getNormalizedXPosition(),
                 getNormalizedYPosition(),
                 bubble.showInShade(),
-                bubble.isOngoing(),
+                false /* isOngoing (unused) */,
                 false /* isAppForeground (unused) */);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java b/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java
index 23bcb29..8368b2c 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java
@@ -96,12 +96,6 @@
         return new AmbientDisplayConfiguration(context);
     }
 
-    /** */
-    @Provides
-    public Handler provideHandler() {
-        return new Handler();
-    }
-
     @Singleton
     @Provides
     public DataSaverController provideDataSaverController(NetworkController networkController) {
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeFactory.java b/packages/SystemUI/src/com/android/systemui/doze/DozeFactory.java
index 6572937..95a9006 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeFactory.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeFactory.java
@@ -28,6 +28,7 @@
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.R;
 import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.plugins.FalsingManager;
@@ -69,7 +70,7 @@
             WakefulnessLifecycle wakefulnessLifecycle, KeyguardUpdateMonitor keyguardUpdateMonitor,
             DockManager dockManager, @Nullable IWallpaperManager wallpaperManager,
             ProximitySensor proximitySensor,
-            DelayedWakeLock.Builder delayedWakeLockBuilder, Handler handler,
+            DelayedWakeLock.Builder delayedWakeLockBuilder, @Main Handler handler,
             DelayableExecutor delayableExecutor,
             BiometricUnlockController biometricUnlockController,
             BroadcastDispatcher broadcastDispatcher, DozeHost dozeHost) {
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
index ac289cb..61c9a96 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
@@ -129,6 +129,7 @@
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.GlobalActions.GlobalActionsManager;
 import com.android.systemui.plugins.GlobalActionsPanelPlugin;
+import com.android.systemui.settings.CurrentUserContextTracker;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
 import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -238,10 +239,10 @@
     private final Executor mBackgroundExecutor;
     private List<ControlsServiceInfo> mControlsServiceInfos = new ArrayList<>();
     private ControlsController mControlsController;
-    private SharedPreferences mControlsPreferences;
     private final RingerModeTracker mRingerModeTracker;
     private int mDialogPressDelay = DIALOG_PRESS_DELAY; // ms
     private Handler mMainHandler;
+    private CurrentUserContextTracker mCurrentUserContextTracker;
     @VisibleForTesting
     boolean mShowLockScreenCardsAndControls = false;
 
@@ -301,7 +302,8 @@
             @Background Executor backgroundExecutor,
             ControlsListingController controlsListingController,
             ControlsController controlsController, UiEventLogger uiEventLogger,
-            RingerModeTracker ringerModeTracker, SysUiState sysUiState, @Main Handler handler) {
+            RingerModeTracker ringerModeTracker, SysUiState sysUiState, @Main Handler handler,
+            CurrentUserContextTracker currentUserContextTracker) {
         mContext = new ContextThemeWrapper(context, com.android.systemui.R.style.qs_theme);
         mWindowManagerFuncs = windowManagerFuncs;
         mAudioManager = audioManager;
@@ -330,6 +332,7 @@
         mControlsController = controlsController;
         mSysUiState = sysUiState;
         mMainHandler = handler;
+        mCurrentUserContextTracker = currentUserContextTracker;
 
         // receive broadcasts
         IntentFilter filter = new IntentFilter();
@@ -382,12 +385,6 @@
 
         controlsListingController.addCallback(list -> mControlsServiceInfos = list);
 
-        // Need to be user-specific with the context to make sure we read the correct prefs
-        Context userContext = context.createContextAsUser(
-                new UserHandle(mUserManager.getUserHandle()), 0);
-        mControlsPreferences = userContext.getSharedPreferences(PREFS_CONTROLS_FILE,
-            Context.MODE_PRIVATE);
-
         // Listen for changes to show controls on the power menu while locked
         onPowerMenuLockScreenSettingsChanged();
         mContext.getContentResolver().registerContentObserver(
@@ -403,8 +400,14 @@
 
     private void seedFavorites() {
         if (mControlsServiceInfos.isEmpty()
-                || mControlsController.getFavorites().size() > 0
-                || mControlsPreferences.getBoolean(PREFS_CONTROLS_SEEDING_COMPLETED, false)) {
+                || mControlsController.getFavorites().size() > 0) {
+            return;
+        }
+
+        // Need to be user-specific with the context to make sure we read the correct prefs
+        SharedPreferences prefs = mCurrentUserContextTracker.getCurrentUserContext()
+                .getSharedPreferences(PREFS_CONTROLS_FILE, Context.MODE_PRIVATE);
+        if (prefs.getBoolean(PREFS_CONTROLS_SEEDING_COMPLETED, false)) {
             return;
         }
 
@@ -426,7 +429,7 @@
 
         if (preferredComponent == null) {
             Log.i(TAG, "Controls seeding: No preferred component has been set, will not seed");
-            mControlsPreferences.edit().putBoolean(PREFS_CONTROLS_SEEDING_COMPLETED, true).apply();
+            prefs.edit().putBoolean(PREFS_CONTROLS_SEEDING_COMPLETED, true).apply();
             return;
         }
 
@@ -434,8 +437,7 @@
                 preferredComponent,
                 (accepted) -> {
                     Log.i(TAG, "Controls seeded: " + accepted);
-                    mControlsPreferences.edit().putBoolean(PREFS_CONTROLS_SEEDING_COMPLETED,
-                        accepted).apply();
+                    prefs.edit().putBoolean(PREFS_CONTROLS_SEEDING_COMPLETED, accepted).apply();
                 });
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/GoneChildrenHideHelper.kt b/packages/SystemUI/src/com/android/systemui/media/GoneChildrenHideHelper.kt
deleted file mode 100644
index 2fe0d9f..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/GoneChildrenHideHelper.kt
+++ /dev/null
@@ -1,52 +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
- */
-package com.android.systemui.media
-
-import android.graphics.Rect
-import android.view.View
-import android.view.ViewGroup
-
-private val EMPTY_RECT = Rect(0,0,0,0)
-
-private val LAYOUT_CHANGE_LISTENER = object : View.OnLayoutChangeListener {
-
-    override fun onLayoutChange(v: View?, left: Int, top: Int, right: Int, bottom: Int,
-                                oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
-        v?.let {
-            if (v.visibility == View.GONE) {
-                v.clipBounds = EMPTY_RECT
-            } else {
-                v.clipBounds = null
-            }
-        }
-    }
-}
-/**
- * A helper class that clips all GONE children. Useful for transitions in motionlayout which
- * don't clip its children.
- */
-class GoneChildrenHideHelper private constructor() {
-    companion object {
-        @JvmStatic
-        fun clipGoneChildrenOnLayout(layout: ViewGroup) {
-            val childCount = layout.childCount
-            for (i in 0 until childCount) {
-                val child = layout.getChildAt(i)
-                child.addOnLayoutChangeListener(LAYOUT_CHANGE_LISTENER)
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
index 85e1c6b..5f43e43 100644
--- a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
@@ -45,6 +45,7 @@
             }
         })
     }
+
     private var view: MediaHeaderView? = null
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/media/LayoutAnimationHelper.kt b/packages/SystemUI/src/com/android/systemui/media/LayoutAnimationHelper.kt
deleted file mode 100644
index a366725..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/LayoutAnimationHelper.kt
+++ /dev/null
@@ -1,115 +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
- */
-package com.android.systemui.media
-
-import android.graphics.Rect
-import android.view.View
-import android.view.ViewGroup
-import android.view.ViewTreeObserver
-import com.android.systemui.statusbar.notification.AnimatableProperty
-import com.android.systemui.statusbar.notification.PropertyAnimator
-import com.android.systemui.statusbar.notification.stack.AnimationProperties
-
-/**
- * A utility class that helps with animations of bound changes designed for motionlayout which
- * doesn't work together with regular changeBounds.
- */
-class LayoutAnimationHelper {
-
-    private val layout: ViewGroup
-    private var sizeAnimationPending = false
-    private val desiredBounds = mutableMapOf<View, Rect>()
-    private val animationProperties = AnimationProperties()
-    private val layoutListener = object : View.OnLayoutChangeListener {
-        override fun onLayoutChange(v: View?, left: Int, top: Int, right: Int, bottom: Int,
-                                    oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
-            v?.let {
-                if (v.alpha == 0.0f || v.visibility == View.GONE || oldLeft - oldRight == 0 ||
-                        oldTop - oldBottom == 0) {
-                    return
-                }
-                if (oldLeft != left || oldTop != top || oldBottom != bottom || oldRight != right) {
-                    val rect = desiredBounds.getOrPut(v, { Rect() })
-                    rect.set(left, top, right, bottom)
-                    onDesiredLocationChanged(v, rect)
-                }
-            }
-        }
-    }
-
-    constructor(layout: ViewGroup) {
-        this.layout = layout
-        val childCount = this.layout.childCount
-        for (i in 0 until childCount) {
-            val child = this.layout.getChildAt(i)
-            child.addOnLayoutChangeListener(layoutListener)
-        }
-    }
-
-    private fun onDesiredLocationChanged(v: View, rect: Rect) {
-        if (!sizeAnimationPending) {
-            applyBounds(v, rect, animate = false)
-        }
-        // We need to reapply the current bounds in every frame since the layout may override
-        // the layout bounds making this view jump and not all calls to apply bounds actually
-        // reapply them, for example if there's already an animator to the same target
-        reapplyProperty(v, AnimatableProperty.ABSOLUTE_X);
-        reapplyProperty(v, AnimatableProperty.ABSOLUTE_Y);
-        reapplyProperty(v, AnimatableProperty.WIDTH);
-        reapplyProperty(v, AnimatableProperty.HEIGHT);
-    }
-
-    private fun reapplyProperty(v: View, property: AnimatableProperty) {
-        property.property.set(v, property.property.get(v))
-    }
-
-    private fun applyBounds(v: View, newBounds: Rect, animate: Boolean) {
-        PropertyAnimator.setProperty(v, AnimatableProperty.ABSOLUTE_X, newBounds.left.toFloat(),
-                animationProperties, animate)
-        PropertyAnimator.setProperty(v, AnimatableProperty.ABSOLUTE_Y, newBounds.top.toFloat(),
-                animationProperties, animate)
-        PropertyAnimator.setProperty(v, AnimatableProperty.WIDTH, newBounds.width().toFloat(),
-                animationProperties, animate)
-        PropertyAnimator.setProperty(v, AnimatableProperty.HEIGHT, newBounds.height().toFloat(),
-                animationProperties, animate)
-    }
-
-    private fun startBoundAnimation(v: View) {
-        val target = desiredBounds[v] ?: return
-        applyBounds(v, target, animate = true)
-    }
-
-    fun animatePendingSizeChange(duration: Long, delay: Long) {
-        animationProperties.duration = duration
-        animationProperties.delay = delay
-        if (!sizeAnimationPending) {
-            sizeAnimationPending = true
-            layout.viewTreeObserver.addOnPreDrawListener (
-                    object : ViewTreeObserver.OnPreDrawListener {
-                        override fun onPreDraw(): Boolean {
-                            layout.viewTreeObserver.removeOnPreDrawListener(this)
-                            sizeAnimationPending = false
-                            val childCount = layout.childCount
-                            for (i in 0 until childCount) {
-                                val child = layout.getChildAt(i)
-                                startBoundAnimation(child)
-                            }
-                            return true
-                        }
-                    })
-        }
-    }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
index c7b9326..8e1e1b2 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
@@ -43,12 +43,9 @@
 import android.widget.SeekBar;
 import android.widget.TextView;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
-import androidx.constraintlayout.motion.widget.Key;
-import androidx.constraintlayout.motion.widget.KeyAttributes;
-import androidx.constraintlayout.motion.widget.KeyFrames;
-import androidx.constraintlayout.motion.widget.MotionLayout;
 import androidx.constraintlayout.widget.ConstraintSet;
 import androidx.core.graphics.drawable.RoundedBitmapDrawable;
 import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
@@ -59,11 +56,11 @@
 import com.android.systemui.R;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.qs.QSMediaBrowser;
+import com.android.systemui.util.animation.TransitionLayout;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 
 import org.jetbrains.annotations.NotNull;
 
-import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Executor;
 
@@ -87,16 +84,15 @@
     private final Executor mForegroundExecutor;
     protected final Executor mBackgroundExecutor;
     private final ActivityStarter mActivityStarter;
-    private LayoutAnimationHelper mLayoutAnimationHelper;
 
     private Context mContext;
     private PlayerViewHolder mViewHolder;
+    private MediaViewController mMediaViewController;
     private MediaSession.Token mToken;
     private MediaController mController;
     private int mBackgroundColor;
     protected ComponentName mServiceComponent;
     private boolean mIsRegistered = false;
-    private List<KeyFrames> mKeyFrames;
     private String mKey;
     private int mAlbumArtSize;
     private int mAlbumArtRadius;
@@ -133,12 +129,14 @@
      * @param activityStarter activity starter
      */
     public MediaControlPanel(Context context, Executor foregroundExecutor,
-            DelayableExecutor backgroundExecutor, ActivityStarter activityStarter) {
+            DelayableExecutor backgroundExecutor, ActivityStarter activityStarter,
+            MediaHostStatesManager mediaHostStatesManager) {
         mContext = context;
         mForegroundExecutor = foregroundExecutor;
         mBackgroundExecutor = backgroundExecutor;
         mActivityStarter = activityStarter;
         mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor);
+        mMediaViewController = new MediaViewController(context, mediaHostStatesManager);
         loadDimens();
     }
 
@@ -147,6 +145,7 @@
             mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver);
         }
         mSeekBarViewModel.onDestroy();
+        mMediaViewController.onDestroy();
     }
 
     private void loadDimens() {
@@ -165,6 +164,15 @@
     }
 
     /**
+     * Get the view controller used to display media controls
+     * @return the media view controller
+     */
+    @NonNull
+    public MediaViewController getMediaViewController() {
+        return mMediaViewController;
+    }
+
+    /**
      * Sets the listening state of the player.
      *
      * Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid
@@ -187,15 +195,13 @@
     /** Attaches the player to the view holder. */
     public void attach(PlayerViewHolder vh) {
         mViewHolder = vh;
-        MotionLayout motionView = vh.getPlayer();
-        mLayoutAnimationHelper = new LayoutAnimationHelper(motionView);
-        GoneChildrenHideHelper.clipGoneChildrenOnLayout(motionView);
-        mKeyFrames = motionView.getDefinedTransitions().get(0).getKeyFrameList();
+        TransitionLayout player = vh.getPlayer();
         mSeekBarObserver = new SeekBarObserver(vh);
         mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver);
         SeekBar bar = vh.getSeekBar();
         bar.setOnSeekBarChangeListener(mSeekBarViewModel.getSeekBarListener());
         bar.setOnTouchListener(mSeekBarViewModel.getSeekBarTouchListener());
+        mMediaViewController.attach(player);
     }
 
     /**
@@ -220,8 +226,8 @@
 
         mController = new MediaController(mContext, mToken);
 
-        ConstraintSet expandedSet = mViewHolder.getPlayer().getConstraintSet(R.id.expanded);
-        ConstraintSet collapsedSet = mViewHolder.getPlayer().getConstraintSet(R.id.collapsed);
+        ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
+        ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
 
         // Try to find a browser service component for this app
         // TODO also check for a media button receiver intended for restarting (b/154127084)
@@ -247,7 +253,7 @@
 
         mController.registerCallback(mSessionCallback);
 
-        mViewHolder.getBackground().setBackgroundTintList(
+        mViewHolder.getPlayer().setBackgroundTintList(
                 ColorStateList.valueOf(mBackgroundColor));
 
         // Click action
@@ -356,7 +362,6 @@
                 }
             });
             boolean visibleInCompat = actionsWhenCollapsed.contains(i);
-            updateKeyFrameVisibility(actionId, visibleInCompat);
             setVisibleAndAlpha(collapsedSet, actionId, visibleInCompat);
             setVisibleAndAlpha(expandedSet, actionId, true /*visible */);
         }
@@ -374,9 +379,9 @@
         // Set up long press menu
         // TODO: b/156036025 bring back media guts
 
-        // Update both constraint sets to regenerate the animation.
-        mViewHolder.getPlayer().updateState(R.id.collapsed, collapsedSet);
-        mViewHolder.getPlayer().updateState(R.id.expanded, expandedSet);
+        // TODO: We don't need to refresh this state constantly, only if the state actually changed
+        // to something which might impact the measurement
+        mMediaViewController.refreshState();
     }
 
     @UiThread
@@ -412,30 +417,6 @@
     }
 
     /**
-     * Updates the keyframe visibility such that only views that are not visible actually go
-     * through a transition and fade in.
-     *
-     * @param actionId the id to change
-     * @param visible is the view visible
-     */
-    private void updateKeyFrameVisibility(int actionId, boolean visible) {
-        if (mKeyFrames == null) {
-            return;
-        }
-        for (int i = 0; i < mKeyFrames.size(); i++) {
-            KeyFrames keyframe = mKeyFrames.get(i);
-            ArrayList<Key> viewKeyFrames = keyframe.getKeyFramesForView(actionId);
-            for (int j = 0; j < viewKeyFrames.size(); j++) {
-                Key key = viewKeyFrames.get(j);
-                if (key instanceof KeyAttributes) {
-                    KeyAttributes attributes = (KeyAttributes) key;
-                    attributes.setValue("alpha", visible ? 1.0f : 0.0f);
-                }
-            }
-        }
-    }
-
-    /**
      * Return the token for the current media session
      * @return the token
      */
@@ -528,8 +509,8 @@
         }
         // Hide all the old buttons
 
-        ConstraintSet expandedSet = mViewHolder.getPlayer().getConstraintSet(R.id.expanded);
-        ConstraintSet collapsedSet = mViewHolder.getPlayer().getConstraintSet(R.id.collapsed);
+        ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
+        ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
         for (int i = 1; i < ACTION_IDS.length; i++) {
             setVisibleAndAlpha(expandedSet, ACTION_IDS[i], false /*visible */);
             setVisibleAndAlpha(collapsedSet, ACTION_IDS[i], false /*visible */);
@@ -571,6 +552,7 @@
             options.setVisibility(View.VISIBLE);
             return true; // consumed click
         });
+        mMediaViewController.refreshState();
     }
 
     private void setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible) {
@@ -649,33 +631,4 @@
      * Called when a player can't be resumed to give it an opportunity to hide or remove itself
      */
     protected void removePlayer() { }
-
-    public void measure(@Nullable MediaMeasurementInput input) {
-        if (mViewHolder == null) {
-            return;
-        }
-        if (input != null) {
-            int width = input.getWidth();
-            setPlayerWidth(width);
-            mViewHolder.getPlayer().measure(input.getWidthMeasureSpec(),
-                    input.getHeightMeasureSpec());
-        }
-    }
-
-    public void setPlayerWidth(int width) {
-        if (mViewHolder == null) {
-            return;
-        }
-        MotionLayout view = mViewHolder.getPlayer();
-        ConstraintSet expandedSet = view.getConstraintSet(R.id.expanded);
-        ConstraintSet collapsedSet = view.getConstraintSet(R.id.collapsed);
-        collapsedSet.setGuidelineBegin(R.id.view_width, width);
-        expandedSet.setGuidelineBegin(R.id.view_width, width);
-        view.updateState(R.id.collapsed, collapsedSet);
-        view.updateState(R.id.expanded, expandedSet);
-    }
-
-    public void animatePendingSizeChange(long duration, long startDelay) {
-        mLayoutAnimationHelper.animatePendingSizeChange(duration, startDelay);
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
index 0b04fd0..552fea6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
@@ -81,7 +81,7 @@
 
     private fun processDevice(key: String, device: MediaDevice?) {
         val enabled = device != null
-        val data = MediaDeviceData(enabled, device?.icon, device?.name)
+        val data = MediaDeviceData(enabled, device?.iconWithoutBackground, device?.name)
         listeners.forEach {
             it.onMediaDeviceChanged(key, data)
         }
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
index cce1d3e..775a1649 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
@@ -21,10 +21,13 @@
 import android.animation.ValueAnimator
 import android.annotation.IntDef
 import android.content.Context
+import android.graphics.Rect
+import android.util.MathUtils
 import android.view.View
 import android.view.ViewGroup
 import android.view.ViewGroupOverlay
 import com.android.systemui.Interpolators
+import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.statusbar.StatusBarState
@@ -47,8 +50,8 @@
     private val keyguardStateController: KeyguardStateController,
     private val bypassController: KeyguardBypassController,
     private val mediaViewManager: MediaViewManager,
-    private val mediaMeasurementProvider: MediaMeasurementManager,
-    private val notifLockscreenUserManager: NotificationLockscreenUserManager
+    private val notifLockscreenUserManager: NotificationLockscreenUserManager,
+    wakefulnessLifecycle: WakefulnessLifecycle
 ) {
     /**
      * The root overlay of the hierarchy. This is where the media notification is attached to
@@ -56,23 +59,31 @@
      * view is always in its final state when it is attached to a view host.
      */
     private var rootOverlay: ViewGroupOverlay? = null
-    private lateinit var currentState: MediaState
-    private val mediaCarousel
+
+    private var rootView: View? = null
+    private var currentBounds = Rect()
+    private var animationStartBounds: Rect = Rect()
+    private var targetBounds: Rect = Rect()
+    private val mediaFrame
         get() = mediaViewManager.mediaFrame
-    private var animationStartState: MediaState? = null
     private var statusbarState: Int = statusBarStateController.state
     private var animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply {
         interpolator = Interpolators.FAST_OUT_SLOW_IN
         addUpdateListener {
             updateTargetState()
-            applyState(animationStartState!!.interpolate(targetState!!, animatedFraction))
+            interpolateBounds(animationStartBounds, targetBounds, animatedFraction,
+                    result = currentBounds)
+            applyState(currentBounds)
         }
         addListener(object : AnimatorListenerAdapter() {
             private var cancelled: Boolean = false
 
             override fun onAnimationCancel(animation: Animator?) {
                 cancelled = true
+                animationPending = false
+                rootView?.removeCallbacks(startAnimation)
             }
+
             override fun onAnimationEnd(animation: Animator?) {
                 if (!cancelled) {
                     applyTargetStateIfNotAnimating()
@@ -81,30 +92,41 @@
 
             override fun onAnimationStart(animation: Animator?) {
                 cancelled = false
+                animationPending = false
             }
         })
     }
-    private var targetState: MediaState? = null
-    private val mediaHosts = arrayOfNulls<MediaHost>(LOCATION_LOCKSCREEN + 1)
 
+    private val mediaHosts = arrayOfNulls<MediaHost>(LOCATION_LOCKSCREEN + 1)
     /**
      * The last location where this view was at before going to the desired location. This is
      * useful for guided transitions.
      */
-    @MediaLocation private var previousLocation = -1
-
+    @MediaLocation
+    private var previousLocation = -1
     /**
      * The desired location where the view will be at the end of the transition.
      */
-    @MediaLocation private var desiredLocation = -1
+    @MediaLocation
+    private var desiredLocation = -1
 
     /**
      * The current attachment location where the view is currently attached.
      * Usually this matches the desired location except for animations whenever a view moves
      * to the new desired location, during which it is in [IN_OVERLAY].
      */
-    @MediaLocation private var currentAttachmentLocation = -1
+    @MediaLocation
+    private var currentAttachmentLocation = -1
 
+    /**
+     * Are we currently waiting on an animation to start?
+     */
+    private var animationPending: Boolean = false
+    private val startAnimation: Runnable = Runnable { animator.start() }
+
+    /**
+     * The expansion of quick settings
+     */
     var qsExpansion: Float = 0.0f
         set(value) {
             if (field != value) {
@@ -117,6 +139,40 @@
             }
         }
 
+    /**
+     * Are location changes currently blocked?
+     */
+    private val blockLocationChanges: Boolean
+        get() {
+            return goingToSleep || dozeAnimationRunning
+        }
+
+    /**
+     * Are we currently going to sleep
+     */
+    private var goingToSleep: Boolean = false
+        set(value) {
+            if (field != value) {
+                field = value
+                if (!value) {
+                    updateDesiredLocation()
+                }
+            }
+        }
+
+    /**
+     * Is the doze animation currently Running
+     */
+    private var dozeAnimationRunning: Boolean = false
+        private set(value) {
+            if (field != value) {
+                field = value
+                if (!value) {
+                    updateDesiredLocation()
+                }
+            }
+        }
+
     init {
         statusBarStateController.addCallback(object : StatusBarStateController.StateListener {
             override fun onStatePreChange(oldState: Int, newState: Int) {
@@ -129,6 +185,34 @@
             override fun onStateChanged(newState: Int) {
                 updateTargetState()
             }
+
+            override fun onDozeAmountChanged(linear: Float, eased: Float) {
+                dozeAnimationRunning = linear != 0.0f && linear != 1.0f
+            }
+
+            override fun onDozingChanged(isDozing: Boolean) {
+                if (!isDozing) {
+                    dozeAnimationRunning = false
+                }
+            }
+        })
+
+        wakefulnessLifecycle.addObserver(object : WakefulnessLifecycle.Observer {
+            override fun onFinishedGoingToSleep() {
+                goingToSleep = false
+            }
+
+            override fun onStartedGoingToSleep() {
+                goingToSleep = true
+            }
+
+            override fun onFinishedWakingUp() {
+                goingToSleep = false
+            }
+
+            override fun onStartedWakingUp() {
+                goingToSleep = false
+            }
         })
     }
 
@@ -138,8 +222,8 @@
      *
      * @return the hostView associated with this location
      */
-    fun register(mediaObject: MediaHost): ViewGroup {
-        val viewHost = createUniqueObjectHost(mediaObject)
+    fun register(mediaObject: MediaHost): UniqueObjectHostView {
+        val viewHost = createUniqueObjectHost()
         mediaObject.hostView = viewHost
         mediaHosts[mediaObject.location] = mediaObject
         if (mediaObject.location == desiredLocation) {
@@ -154,22 +238,13 @@
         return viewHost
     }
 
-    private fun createUniqueObjectHost(host: MediaHost): UniqueObjectHostView {
+    private fun createUniqueObjectHost(): UniqueObjectHostView {
         val viewHost = UniqueObjectHostView(context)
-        viewHost.measurementCache = mediaMeasurementProvider.obtainCache(host)
-        viewHost.onMeasureListener = { input ->
-            if (host.location == desiredLocation) {
-                // Measurement of the currently active player is happening, Let's make
-                // sure the player width is up to date
-                val measuringInput = host.getMeasuringInput(input)
-                mediaViewManager.setPlayerWidth(measuringInput.width)
-            }
-        }
-
         viewHost.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
             override fun onViewAttachedToWindow(p0: View?) {
                 if (rootOverlay == null) {
-                    rootOverlay = (viewHost.viewRootImpl.view.overlay as ViewGroupOverlay)
+                    rootView = viewHost.viewRootImpl.view
+                    rootOverlay = (rootView!!.overlay as ViewGroupOverlay)
                 }
                 viewHost.removeOnAttachStateChangeListener(this)
             }
@@ -195,8 +270,9 @@
             // Let's perform a transition
             val animate = shouldAnimateTransition(desiredLocation, previousLocation)
             val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
-            mediaViewManager.onDesiredLocationChanged(getHost(desiredLocation)?.currentState,
-                    animate, animDuration, delay)
+            val host = getHost(desiredLocation)
+            mediaViewManager.onDesiredLocationChanged(desiredLocation, host, animate, animDuration,
+                    delay)
             performTransitionToNewLocation(isNewView, animate)
         }
     }
@@ -222,14 +298,18 @@
                 // Let's animate to the new position, starting from the current position
                 // We also go in here in case the view was detached, since the bounds wouldn't
                 // be correct anymore
-                animationStartState = currentState.copy()
+                animationStartBounds.set(currentBounds)
             } else {
                 // otherwise, let's take the freshest state, since the current one could
                 // be outdated
-                animationStartState = previousHost.currentState.copy()
+                animationStartBounds.set(previousHost.currentBounds)
             }
             adjustAnimatorForTransition(desiredLocation, previousLocation)
-            animator.start()
+            rootView?.let {
+                // Let's delay the animation start until we finished laying out
+                animationPending = true
+                it.postOnAnimation(startAnimation)
+            }
         } else {
             cancelAnimationAndApplyDesiredState()
         }
@@ -239,6 +319,9 @@
         @MediaLocation currentLocation: Int,
         @MediaLocation previousLocation: Int
     ): Boolean {
+        if (isCurrentlyInGuidedTransformation()) {
+            return false
+        }
         if (currentLocation == LOCATION_QQS &&
                 previousLocation == LOCATION_LOCKSCREEN &&
                 (statusBarStateController.leaveOpenOnKeyguardHide() ||
@@ -247,7 +330,7 @@
             // non-trivial reattaching logic happening that will make the view not-shown earlier
             return true
         }
-        return mediaCarousel.isShown || animator.isRunning
+        return mediaFrame.isShown || animator.isRunning || animationPending
     }
 
     private fun adjustAnimatorForTransition(desiredLocation: Int, previousLocation: Int) {
@@ -279,7 +362,7 @@
             // Let's immediately apply the target state (which is interpolated) if there is
             // no animation running. Otherwise the animation update will already update
             // the location
-            applyState(targetState!!)
+            applyState(targetBounds)
         }
     }
 
@@ -291,14 +374,34 @@
             val progress = getTransformationProgress()
             val currentHost = getHost(desiredLocation)!!
             val previousHost = getHost(previousLocation)!!
-            val newState = currentHost.currentState
-            val previousState = previousHost.currentState
-            targetState = previousState.interpolate(newState, progress)
+            val newBounds = currentHost.currentBounds
+            val previousBounds = previousHost.currentBounds
+            targetBounds = interpolateBounds(previousBounds, newBounds, progress)
         } else {
-            targetState = getHost(desiredLocation)?.currentState
+            val bounds = getHost(desiredLocation)?.currentBounds ?: return
+            targetBounds.set(bounds)
         }
     }
 
+    private fun interpolateBounds(
+        startBounds: Rect,
+        endBounds: Rect,
+        progress: Float,
+        result: Rect? = null
+    ): Rect {
+        val left = MathUtils.lerp(startBounds.left.toFloat(),
+                endBounds.left.toFloat(), progress).toInt()
+        val top = MathUtils.lerp(startBounds.top.toFloat(),
+                endBounds.top.toFloat(), progress).toInt()
+        val right = MathUtils.lerp(startBounds.right.toFloat(),
+                endBounds.right.toFloat(), progress).toInt()
+        val bottom = MathUtils.lerp(startBounds.bottom.toFloat(),
+                endBounds.bottom.toFloat(), progress).toInt()
+        val resultBounds = result ?: Rect()
+        resultBounds.set(left, top, right, bottom)
+        return resultBounds
+    }
+
     /**
      * @return true if this transformation is guided by an external progress like a finger
      */
@@ -339,21 +442,27 @@
     private fun cancelAnimationAndApplyDesiredState() {
         animator.cancel()
         getHost(desiredLocation)?.let {
-            applyState(it.currentState)
+            applyState(it.currentBounds, immediately = true)
         }
     }
 
-    private fun applyState(state: MediaState) {
-        currentState = state.copy()
-        mediaViewManager.setCurrentState(currentState)
+    /**
+     * Apply the current state to the view, updating it's bounds and desired state
+     */
+    private fun applyState(bounds: Rect, immediately: Boolean = false) {
+        currentBounds.set(bounds)
+        val currentlyInGuidedTransformation = isCurrentlyInGuidedTransformation()
+        val startLocation = if (currentlyInGuidedTransformation) previousLocation else -1
+        val progress = if (currentlyInGuidedTransformation) getTransformationProgress() else 1.0f
+        val endLocation = desiredLocation
+        mediaViewManager.setCurrentState(startLocation, endLocation, progress, immediately)
         updateHostAttachment()
         if (currentAttachmentLocation == IN_OVERLAY) {
-            val boundsOnScreen = state.boundsOnScreen
-            mediaCarousel.setLeftTopRightBottom(
-                    boundsOnScreen.left,
-                    boundsOnScreen.top,
-                    boundsOnScreen.right,
-                    boundsOnScreen.bottom)
+            mediaFrame.setLeftTopRightBottom(
+                    currentBounds.left,
+                    currentBounds.top,
+                    currentBounds.right,
+                    currentBounds.bottom)
         }
     }
 
@@ -364,26 +473,29 @@
             currentAttachmentLocation = newLocation
 
             // Remove the carousel from the old host
-            (mediaCarousel.parent as ViewGroup?)?.removeView(mediaCarousel)
+            (mediaFrame.parent as ViewGroup?)?.removeView(mediaFrame)
 
             // Add it to the new one
             val targetHost = getHost(desiredLocation)!!.hostView
             if (inOverlay) {
-                rootOverlay!!.add(mediaCarousel)
+                rootOverlay!!.add(mediaFrame)
             } else {
-                targetHost.addView(mediaCarousel)
-                mediaViewManager.onViewReattached()
+                targetHost.addView(mediaFrame)
             }
         }
     }
 
     private fun isTransitionRunning(): Boolean {
         return isCurrentlyInGuidedTransformation() && getTransformationProgress() != 1.0f ||
-                animator.isRunning
+                animator.isRunning || animationPending
     }
 
     @MediaLocation
     private fun calculateLocation(): Int {
+        if (blockLocationChanges) {
+            // Keep the current location until we're allowed to again
+            return desiredLocation
+        }
         val onLockscreen = (!bypassController.bypassEnabled &&
                 (statusbarState == StatusBarState.KEYGUARD ||
                         statusbarState == StatusBarState.FULLSCREEN_USER_SWITCHER))
@@ -396,13 +508,6 @@
         }
     }
 
-    /**
-     * The expansion of quick settings
-     */
-    @IntDef(prefix = ["LOCATION_"], value = [LOCATION_QS, LOCATION_QQS, LOCATION_LOCKSCREEN])
-    @Retention(AnnotationRetention.SOURCE)
-    annotation class MediaLocation
-
     companion object {
         /**
          * Attached in expanded quick settings
@@ -425,3 +530,8 @@
         const val IN_OVERLAY = -1000
     }
 }
+
+@IntDef(prefix = ["LOCATION_"], value = [MediaHierarchyManager.LOCATION_QS,
+    MediaHierarchyManager.LOCATION_QQS, MediaHierarchyManager.LOCATION_LOCKSCREEN])
+@Retention(AnnotationRetention.SOURCE)
+annotation class MediaLocation
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
index 240e44c..e904e93 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
@@ -1,20 +1,22 @@
 package com.android.systemui.media
 
 import android.graphics.Rect
-import android.util.MathUtils
 import android.view.View
 import android.view.View.OnAttachStateChangeListener
-import android.view.ViewGroup
-import com.android.systemui.media.MediaHierarchyManager.MediaLocation
 import com.android.systemui.util.animation.MeasurementInput
+import com.android.systemui.util.animation.MeasurementOutput
+import com.android.systemui.util.animation.UniqueObjectHostView
+import java.util.Objects
 import javax.inject.Inject
 
 class MediaHost @Inject constructor(
-    private val state: MediaHostState,
+    private val state: MediaHostStateHolder,
     private val mediaHierarchyManager: MediaHierarchyManager,
-    private val mediaDataManager: MediaDataManager
-) : MediaState by state {
-    lateinit var hostView: ViewGroup
+    private val mediaDataManager: MediaDataManager,
+    private val mediaDataManagerCombineLatest: MediaDataCombineLatest,
+    private val mediaHostStatesManager: MediaHostStatesManager
+) : MediaHostState by state {
+    lateinit var hostView: UniqueObjectHostView
     var location: Int = -1
         private set
     var visibleChangedListener: ((Boolean) -> Unit)? = null
@@ -24,9 +26,9 @@
     private val tmpLocationOnScreen: IntArray = intArrayOf(0, 0)
 
     /**
-     * Get the current Media state. This also updates the location on screen
+     * Get the current bounds on the screen. This makes sure the state is fresh and up to date
      */
-    val currentState: MediaState
+    val currentBounds: Rect = Rect()
         get() {
             hostView.getLocationOnScreen(tmpLocationOnScreen)
             var left = tmpLocationOnScreen[0] + hostView.paddingLeft
@@ -43,8 +45,8 @@
                 bottom = 0
                 top = 0
             }
-            state.boundsOnScreen.set(left, top, right, bottom)
-            return state
+            field.set(left, top, right, bottom)
+            return field
         }
 
     private val listener = object : MediaDataManager.Listener {
@@ -59,6 +61,8 @@
 
     /**
      * Initialize this MediaObject and create a host view.
+     * All state should already be set on this host before calling this method in order to avoid
+     * unnecessary state changes which lead to remeasurings later on.
      *
      * @param location the location this host name has. Used to identify the host during
      *                 transitions.
@@ -68,14 +72,39 @@
         hostView = mediaHierarchyManager.register(this)
         hostView.addOnAttachStateChangeListener(object : OnAttachStateChangeListener {
             override fun onViewAttachedToWindow(v: View?) {
-                mediaDataManager.addListener(listener)
+                // we should listen to the combined state change, since otherwise there might
+                // be a delay until the views and the controllers are initialized, leaving us
+                // with either a blank view or the controllers not yet initialized and the
+                // measuring wrong
+                mediaDataManagerCombineLatest.addListener(listener)
                 updateViewVisibility()
             }
 
             override fun onViewDetachedFromWindow(v: View?) {
-                mediaDataManager.removeListener(listener)
+                mediaDataManagerCombineLatest.removeListener(listener)
             }
         })
+
+        // Listen to measurement updates and update our state with it
+        hostView.measurementManager = object : UniqueObjectHostView.MeasurementManager {
+            override fun onMeasure(input: MeasurementInput): MeasurementOutput {
+                // Modify the measurement to exactly match the dimensions
+                if (View.MeasureSpec.getMode(input.widthMeasureSpec) == View.MeasureSpec.AT_MOST) {
+                    input.widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(
+                            View.MeasureSpec.getSize(input.widthMeasureSpec),
+                            View.MeasureSpec.EXACTLY)
+                }
+                // This will trigger a state change that ensures that we now have a state available
+                state.measurementInput = input
+                return mediaHostStatesManager.getPlayerDimensions(state)
+            }
+        }
+
+        // Whenever the state changes, let our state manager know
+        state.changedListener = {
+            mediaHostStatesManager.updateHostState(location, state)
+        }
+
         updateViewVisibility()
     }
 
@@ -89,71 +118,93 @@
         visibleChangedListener?.invoke(visible)
     }
 
-    class MediaHostState @Inject constructor() : MediaState {
-        var measurementInput: MediaMeasurementInput? = null
-        override var expansion: Float = 0.0f
-        override var showsOnlyActiveMedia: Boolean = false
-        override val boundsOnScreen: Rect = Rect()
+    class MediaHostStateHolder @Inject constructor() : MediaHostState {
 
-        override fun copy(): MediaState {
-            val mediaHostState = MediaHostState()
+        override var measurementInput: MeasurementInput? = null
+            set(value) {
+                if (value?.equals(field) != true) {
+                    field = value
+                    changedListener?.invoke()
+                }
+            }
+
+        override var expansion: Float = 0.0f
+            set(value) {
+                if (!value.equals(field)) {
+                    field = value
+                    changedListener?.invoke()
+                }
+            }
+
+        override var showsOnlyActiveMedia: Boolean = false
+            set(value) {
+                if (!value.equals(field)) {
+                    field = value
+                    changedListener?.invoke()
+                }
+            }
+
+        /**
+         * A listener for all changes. This won't be copied over when invoking [copy]
+         */
+        var changedListener: (() -> Unit)? = null
+
+        /**
+         * Get a copy of this state. This won't copy any listeners it may have set
+         */
+        override fun copy(): MediaHostState {
+            val mediaHostState = MediaHostStateHolder()
             mediaHostState.expansion = expansion
             mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia
-            mediaHostState.boundsOnScreen.set(boundsOnScreen)
-            mediaHostState.measurementInput = measurementInput
+            mediaHostState.measurementInput = measurementInput?.copy()
             return mediaHostState
         }
 
-        override fun interpolate(other: MediaState, amount: Float): MediaState {
-            val result = MediaHostState()
-            result.expansion = MathUtils.lerp(expansion, other.expansion, amount)
-            val left = MathUtils.lerp(boundsOnScreen.left.toFloat(),
-                    other.boundsOnScreen.left.toFloat(), amount).toInt()
-            val top = MathUtils.lerp(boundsOnScreen.top.toFloat(),
-                    other.boundsOnScreen.top.toFloat(), amount).toInt()
-            val right = MathUtils.lerp(boundsOnScreen.right.toFloat(),
-                    other.boundsOnScreen.right.toFloat(), amount).toInt()
-            val bottom = MathUtils.lerp(boundsOnScreen.bottom.toFloat(),
-                    other.boundsOnScreen.bottom.toFloat(), amount).toInt()
-            result.boundsOnScreen.set(left, top, right, bottom)
-            result.showsOnlyActiveMedia = other.showsOnlyActiveMedia || showsOnlyActiveMedia
-            if (amount > 0.0f) {
-                if (other is MediaHostState) {
-                    result.measurementInput = other.measurementInput
-                }
-            } else {
-                result.measurementInput
+        override fun equals(other: Any?): Boolean {
+            if (!(other is MediaHostState)) {
+                return false
             }
+            if (!Objects.equals(measurementInput, other.measurementInput)) {
+                return false
+            }
+            if (expansion != other.expansion) {
+                return false
+            }
+            if (showsOnlyActiveMedia != other.showsOnlyActiveMedia) {
+                return false
+            }
+            return true
+        }
+
+        override fun hashCode(): Int {
+            var result = measurementInput?.hashCode() ?: 0
+            result = 31 * result + expansion.hashCode()
+            result = 31 * result + showsOnlyActiveMedia.hashCode()
             return result
         }
-
-        override fun getMeasuringInput(input: MeasurementInput): MediaMeasurementInput {
-            measurementInput = MediaMeasurementInput(input, expansion)
-            return measurementInput as MediaMeasurementInput
-        }
     }
 }
 
-interface MediaState {
+interface MediaHostState {
+
+    /**
+     * The last measurement input that this state was measured with. Infers with and height of
+     * the players.
+     */
+    var measurementInput: MeasurementInput?
+
+    /**
+     * The expansion of the player, 0 for fully collapsed, 1 for fully expanded
+     */
     var expansion: Float
-    var showsOnlyActiveMedia: Boolean
-    val boundsOnScreen: Rect
-    fun copy(): MediaState
-    fun interpolate(other: MediaState, amount: Float): MediaState
-    fun getMeasuringInput(input: MeasurementInput): MediaMeasurementInput
-}
-/**
- * The measurement input for a Media View
- */
-data class MediaMeasurementInput(
-    private val viewInput: MeasurementInput,
-    val expansion: Float
-) : MeasurementInput by viewInput {
 
-    override fun sameAs(input: MeasurementInput?): Boolean {
-        if (!(input is MediaMeasurementInput)) {
-            return false
-        }
-        return width == input.width && expansion == input.expansion
-    }
+    /**
+     * Is this host only showing active media or is it showing all of them including resumption?
+     */
+    var showsOnlyActiveMedia: Boolean
+
+    /**
+     * Get a copy of this view state, deepcopying all appropriate members
+     */
+    fun copy(): MediaHostState
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt
new file mode 100644
index 0000000..f90af2a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt
@@ -0,0 +1,116 @@
+/*
+ * 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.media
+
+import com.android.systemui.util.animation.MeasurementOutput
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * A class responsible for managing all media host states of the various host locations and
+ * coordinating the heights among different players. This class can be used to get the most up to
+ * date state for any location.
+ */
+@Singleton
+class MediaHostStatesManager @Inject constructor() {
+
+    private val callbacks: MutableSet<Callback> = mutableSetOf()
+    private val controllers: MutableSet<MediaViewController> = mutableSetOf()
+
+    /**
+     * A map with all media states of all locations.
+     */
+    val mediaHostStates: MutableMap<Int, MediaHostState> = mutableMapOf()
+
+    /**
+     * Notify that a media state for a given location has changed. Should only be called from
+     * Media hosts themselves.
+     */
+    fun updateHostState(@MediaLocation location: Int, hostState: MediaHostState) {
+        val currentState = mediaHostStates.get(location)
+        if (!hostState.equals(currentState)) {
+            val newState = hostState.copy()
+            mediaHostStates.put(location, newState)
+            // First update all the controllers to ensure they get the chance to measure
+            for (controller in controllers) {
+                controller.stateCallback.onHostStateChanged(location, newState)
+            }
+
+            // Then update all other callbacks which may depend on the controllers above
+            for (callback in callbacks) {
+                callback.onHostStateChanged(location, newState)
+            }
+        }
+    }
+
+    /**
+     * Get the dimensions of all players combined, which determines the overall height of the
+     * media carousel and the media hosts.
+     */
+    fun getPlayerDimensions(hostState: MediaHostState): MeasurementOutput {
+        val result = MeasurementOutput(0, 0)
+        for (controller in controllers) {
+            val measurement = controller.getMeasurementsForState(hostState)
+            measurement?.let {
+                if (it.measuredHeight > result.measuredHeight) {
+                    result.measuredHeight = it.measuredHeight
+                }
+                if (it.measuredWidth > result.measuredWidth) {
+                    result.measuredWidth = it.measuredWidth
+                }
+            }
+        }
+        return result
+    }
+
+    /**
+     * Add a callback to be called when a MediaState has updated
+     */
+    fun addCallback(callback: Callback) {
+        callbacks.add(callback)
+    }
+
+    /**
+     * Remove a callback that listens to media states
+     */
+    fun removeCallback(callback: Callback) {
+        callbacks.remove(callback)
+    }
+
+    /**
+     * Register a controller that listens to media states and is used to determine the size of
+     * the media carousel
+     */
+    fun addController(controller: MediaViewController) {
+        controllers.add(controller)
+    }
+
+    /**
+     * Notify the manager about the removal of a controller.
+     */
+    fun removeController(controller: MediaViewController) {
+        controllers.remove(controller)
+    }
+
+    interface Callback {
+        /**
+         * Notify the callbacks that a media state for a host has changed, and that the
+         * corresponding view states should be updated and applied
+         */
+        fun onHostStateChanged(@MediaLocation location: Int, mediaHostState: MediaHostState)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaMeasurementManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaMeasurementManager.kt
deleted file mode 100644
index 4bbf5eb..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaMeasurementManager.kt
+++ /dev/null
@@ -1,59 +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
- */
-
-package com.android.systemui.media
-
-import com.android.systemui.util.animation.BaseMeasurementCache
-import com.android.systemui.util.animation.GuaranteedMeasurementCache
-import com.android.systemui.util.animation.MeasurementCache
-import com.android.systemui.util.animation.MeasurementInput
-import com.android.systemui.util.animation.MeasurementOutput
-import javax.inject.Inject
-import javax.inject.Singleton
-
-/**
- * A class responsible creating measurement caches for media hosts which also coordinates with
- * the view manager to obtain sizes for unknown measurement inputs.
- */
-@Singleton
-class MediaMeasurementManager @Inject constructor(
-    private val mediaViewManager: MediaViewManager
-) {
-    private val baseCache: MeasurementCache
-
-    init {
-        baseCache = BaseMeasurementCache()
-    }
-
-    private fun provideMeasurement(input: MediaMeasurementInput) : MeasurementOutput? {
-        return mediaViewManager.obtainMeasurement(input)
-    }
-
-    /**
-     * Obtain a guaranteed measurement cache for a host view. The measurement cache makes sure that
-     * requesting any size from the cache will always return the correct value.
-     */
-    fun obtainCache(host: MediaState): GuaranteedMeasurementCache {
-        val remapper = { input: MeasurementInput ->
-            host.getMeasuringInput(input)
-        }
-        val provider = { input: MeasurementInput ->
-            provideMeasurement(input as MediaMeasurementInput)
-        }
-        return GuaranteedMeasurementCache(baseCache, remapper, provider)
-    }
-}
-
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
new file mode 100644
index 0000000..e82bb40
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
@@ -0,0 +1,280 @@
+/*
+ * 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.media
+
+import android.content.Context
+import androidx.constraintlayout.widget.ConstraintSet
+import com.android.systemui.R
+import com.android.systemui.util.animation.TransitionLayout
+import com.android.systemui.util.animation.TransitionLayoutController
+import com.android.systemui.util.animation.TransitionViewState
+import com.android.systemui.util.animation.MeasurementOutput
+
+/**
+ * A class responsible for controlling a single instance of a media player handling interactions
+ * with the view instance and keeping the media view states up to date.
+ */
+class MediaViewController(
+    context: Context,
+    val mediaHostStatesManager: MediaHostStatesManager
+) {
+
+    private var firstRefresh: Boolean = true
+    private var transitionLayout: TransitionLayout? = null
+    private val layoutController = TransitionLayoutController()
+    private var animationDelay: Long = 0
+    private var animationDuration: Long = 0
+    private var animateNextStateChange: Boolean = false
+    private val measurement = MeasurementOutput(0, 0)
+
+    /**
+     * A map containing all viewStates for all locations of this mediaState
+     */
+    private val mViewStates: MutableMap<MediaHostState, TransitionViewState?> = mutableMapOf()
+
+    /**
+     * The ending location of the view where it ends when all animations and transitions have
+     * finished
+     */
+    private var currentEndLocation: Int = -1
+
+    /**
+     * The ending location of the view where it ends when all animations and transitions have
+     * finished
+     */
+    private var currentStartLocation: Int = -1
+
+    /**
+     * The progress of the transition or 1.0 if there is no transition happening
+     */
+    private var currentTransitionProgress: Float = 1.0f
+
+    /**
+     * A temporary state used to store intermediate measurements.
+     */
+    private val tmpState = TransitionViewState()
+
+    /**
+     * A callback for media state changes
+     */
+    val stateCallback = object : MediaHostStatesManager.Callback {
+        override fun onHostStateChanged(location: Int, mediaHostState: MediaHostState) {
+            if (location == currentEndLocation || location == currentStartLocation) {
+                setCurrentState(currentStartLocation,
+                        currentEndLocation,
+                        currentTransitionProgress,
+                        applyImmediately = false)
+            }
+        }
+    }
+
+    /**
+     * The expanded constraint set used to render a expanded player. If it is modified, make sure
+     * to call [refreshState]
+     */
+    val collapsedLayout = ConstraintSet()
+
+    /**
+     * The expanded constraint set used to render a collapsed player. If it is modified, make sure
+     * to call [refreshState]
+     */
+    val expandedLayout = ConstraintSet()
+
+    init {
+        collapsedLayout.load(context, R.xml.media_collapsed)
+        expandedLayout.load(context, R.xml.media_expanded)
+        mediaHostStatesManager.addController(this)
+    }
+
+    /**
+     * Notify this controller that the view has been removed and all listeners should be destroyed
+     */
+    fun onDestroy() {
+        mediaHostStatesManager.removeController(this)
+    }
+
+    private fun ensureAllMeasurements() {
+        val mediaStates = mediaHostStatesManager.mediaHostStates
+        for (entry in mediaStates) {
+            obtainViewState(entry.value)
+        }
+    }
+
+    /**
+     * Get the constraintSet for a given expansion
+     */
+    private fun constraintSetForExpansion(expansion: Float): ConstraintSet =
+            if (expansion > 0) expandedLayout else collapsedLayout
+
+    /**
+     * Obtain a new viewState for a given media state. This usually returns a cached state, but if
+     * it's not available, it will recreate one by measuring, which may be expensive.
+     */
+    private fun obtainViewState(state: MediaHostState): TransitionViewState? {
+        val viewState = mViewStates[state]
+        if (viewState != null) {
+            // we already have cached this measurement, let's continue
+            return viewState
+        }
+
+        val result: TransitionViewState?
+        if (transitionLayout != null && state.measurementInput != null) {
+            // Let's create a new measurement
+            if (state.expansion == 0.0f || state.expansion == 1.0f) {
+                result = transitionLayout!!.calculateViewState(
+                        state.measurementInput!!,
+                        constraintSetForExpansion(state.expansion),
+                        TransitionViewState())
+
+                // We don't want to cache interpolated or null states as this could quickly fill up
+                // our cache. We only cache the start and the end states since the interpolation
+                // is cheap
+                mViewStates[state.copy()] = result
+            } else {
+                // This is an interpolated state
+                val startState = state.copy().also { it.expansion = 0.0f }
+
+                // Given that we have a measurement and a view, let's get (guaranteed) viewstates
+                // from the start and end state and interpolate them
+                val startViewState = obtainViewState(startState) as TransitionViewState
+                val endState = state.copy().also { it.expansion = 1.0f }
+                val endViewState = obtainViewState(endState) as TransitionViewState
+                result = TransitionViewState()
+                layoutController.getInterpolatedState(
+                        startViewState,
+                        endViewState,
+                        state.expansion,
+                        result)
+            }
+        } else {
+            result = null
+        }
+        return result
+    }
+
+    /**
+     * Attach a view to this controller. This may perform measurements if it's not available yet
+     * and should therefore be done carefully.
+     */
+    fun attach(transitionLayout: TransitionLayout) {
+        this.transitionLayout = transitionLayout
+        layoutController.attach(transitionLayout)
+        ensureAllMeasurements()
+        if (currentEndLocation == -1) {
+            return
+        }
+        // Set the previously set state immediately to the view, now that it's finally attached
+        setCurrentState(
+                startLocation = currentStartLocation,
+                endLocation = currentEndLocation,
+                transitionProgress = currentTransitionProgress,
+                applyImmediately = true)
+    }
+
+    /**
+     * Obtain a measurement for a given location. This makes sure that the state is up to date
+     * and all widgets know their location. Calling this method may create a measurement if we
+     * don't have a cached value available already.
+     */
+    fun getMeasurementsForState(hostState: MediaHostState): MeasurementOutput? {
+        val viewState = obtainViewState(hostState) ?: return null
+        measurement.measuredWidth = viewState.width
+        measurement.measuredHeight = viewState.height
+        return measurement
+    }
+
+    /**
+     * Set a new state for the controlled view which can be an interpolation between multiple
+     * locations.
+     */
+    fun setCurrentState(
+        @MediaLocation startLocation: Int,
+        @MediaLocation endLocation: Int,
+        transitionProgress: Float,
+        applyImmediately: Boolean
+    ) {
+        currentEndLocation = endLocation
+        currentStartLocation = startLocation
+        currentTransitionProgress = transitionProgress
+
+        val shouldAnimate = animateNextStateChange && !applyImmediately
+
+        // Obtain the view state that we'd want to be at the end
+        // The view might not be bound yet or has never been measured and in that case will be
+        // reset once the state is fully available
+        val endState = obtainViewStateForLocation(endLocation) ?: return
+        layoutController.setMeasureState(endState)
+
+        // If the view isn't bound, we can drop the animation, otherwise we'll executute it
+        animateNextStateChange = false
+        if (transitionLayout == null) {
+            return
+        }
+
+        val startState = obtainViewStateForLocation(startLocation)
+        val result: TransitionViewState?
+        if (transitionProgress == 1.0f || startState == null) {
+            result = endState
+        } else if (transitionProgress == 0.0f) {
+            result = startState
+        } else {
+            layoutController.getInterpolatedState(startState, endState, transitionProgress,
+                    tmpState)
+            result = tmpState
+        }
+        layoutController.setState(result, applyImmediately, shouldAnimate, animationDuration,
+                animationDelay)
+    }
+
+    private fun obtainViewStateForLocation(location: Int): TransitionViewState? {
+        val mediaState = mediaHostStatesManager.mediaHostStates[location] ?: return null
+        return obtainViewState(mediaState)
+    }
+
+    /**
+     * Notify that the location is changing right now and a [setCurrentState] change is imminent.
+     * This updates the width the view will me measured with.
+     */
+    fun onLocationPreChange(@MediaLocation newLocation: Int) {
+        val viewState = obtainViewStateForLocation(newLocation)
+        viewState?.let {
+            layoutController.setMeasureState(it)
+        }
+    }
+
+    /**
+     * Request that the next state change should be animated with the given parameters.
+     */
+    fun animatePendingStateChange(duration: Long, delay: Long) {
+        animateNextStateChange = true
+        animationDuration = duration
+        animationDelay = delay
+    }
+
+    /**
+     * Clear all existing measurements and refresh the state to match the view.
+     */
+    fun refreshState() {
+        if (!firstRefresh) {
+            // Let's clear all of our measurements and recreate them!
+            mViewStates.clear()
+            setCurrentState(currentStartLocation, currentEndLocation, currentTransitionProgress,
+                    applyImmediately = false)
+        }
+        firstRefresh = false
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt
index 16302d1..8ab30c7 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt
@@ -16,8 +16,8 @@
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.qs.PageIndicator
 import com.android.systemui.statusbar.notification.VisualStabilityManager
-import com.android.systemui.util.animation.MeasurementOutput
 import com.android.systemui.util.animation.UniqueObjectHostView
+import com.android.systemui.util.animation.requiresRemeasuring
 import com.android.systemui.util.concurrency.DelayableExecutor
 import java.util.concurrent.Executor
 import javax.inject.Inject
@@ -36,16 +36,51 @@
     @Background private val backgroundExecutor: DelayableExecutor,
     private val visualStabilityManager: VisualStabilityManager,
     private val activityStarter: ActivityStarter,
+    private val mediaHostStatesManager: MediaHostStatesManager,
     mediaManager: MediaDataCombineLatest
 ) {
-    private var playerWidth: Int = 0
+
+    /**
+     * The desired location where we'll be at the end of the transformation. Usually this matches
+     * the end location, except when we're still waiting on a state update call.
+     */
+    @MediaLocation
+    private var desiredLocation: Int = -1
+
+    /**
+     * The ending location of the view where it ends when all animations and transitions have
+     * finished
+     */
+    @MediaLocation
+    private var currentEndLocation: Int = -1
+
+    /**
+     * The ending location of the view where it ends when all animations and transitions have
+     * finished
+     */
+    @MediaLocation
+    private var currentStartLocation: Int = -1
+
+    /**
+     * The progress of the transition or 1.0 if there is no transition happening
+     */
+    private var currentTransitionProgress: Float = 1.0f
+
+    /**
+     * The measured width of the carousel
+     */
+    private var carouselMeasureWidth: Int = 0
+
+    /**
+     * The measured height of the carousel
+     */
+    private var carouselMeasureHeight: Int = 0
     private var playerWidthPlusPadding: Int = 0
-    private var desiredState: MediaHost.MediaHostState? = null
-    private var currentState: MediaState? = null
+    private var desiredHostState: MediaHostState? = null
     private val mediaCarousel: HorizontalScrollView
     val mediaFrame: ViewGroup
+    val mediaPlayers: MutableMap<String, MediaControlPanel> = mutableMapOf()
     private val mediaContent: ViewGroup
-    private val mediaPlayers: MutableMap<String, MediaControlPanel> = mutableMapOf()
     private val pageIndicator: PageIndicator
     private val gestureDetector: GestureDetectorCompat
     private val visualStabilityCallback: VisualStabilityManager.Callback
@@ -115,6 +150,7 @@
             override fun onMediaDataLoaded(key: String, data: MediaData) {
                 updateView(key, data)
                 updatePlayerVisibilities()
+                mediaCarousel.requiresRemeasuring = true
             }
 
             override fun onMediaDataRemoved(key: String) {
@@ -137,6 +173,13 @@
                 }
             }
         })
+        mediaHostStatesManager.addCallback(object : MediaHostStatesManager.Callback {
+            override fun onHostStateChanged(location: Int, mediaHostState: MediaHostState) {
+                if (location == desiredLocation) {
+                    onDesiredLocationChanged(desiredLocation, mediaHostState, animate = false)
+                }
+            }
+        })
     }
 
     private fun inflateMediaCarousel(): ViewGroup {
@@ -220,7 +263,7 @@
         var existingPlayer = mediaPlayers[key]
         if (existingPlayer == null) {
             existingPlayer = MediaControlPanel(context, foregroundExecutor, backgroundExecutor,
-                    activityStarter)
+                    activityStarter, mediaHostStatesManager)
             existingPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context),
                     mediaContent))
             mediaPlayers[key] = existingPlayer
@@ -228,14 +271,14 @@
                     ViewGroup.LayoutParams.WRAP_CONTENT)
             existingPlayer.view?.player?.setLayoutParams(lp)
             existingPlayer.setListening(currentlyExpanded)
+            updatePlayerToState(existingPlayer, noAnimation = true)
             if (existingPlayer.isPlaying) {
                 mediaContent.addView(existingPlayer.view?.player, 0)
             } else {
                 mediaContent.addView(existingPlayer.view?.player)
             }
-            updatePlayerToCurrentState(existingPlayer)
         } else if (existingPlayer.isPlaying &&
-                    mediaContent.indexOfChild(existingPlayer.view?.player) != 0) {
+                mediaContent.indexOfChild(existingPlayer.view?.player) != 0) {
             if (visualStabilityManager.isReorderingAllowed) {
                 mediaContent.removeView(existingPlayer.view?.player)
                 mediaContent.addView(existingPlayer.view?.player, 0)
@@ -244,20 +287,10 @@
             }
         }
         existingPlayer.bind(data)
-        // Resetting the progress to make sure it's taken into account for the latest
-        // motion model
-        existingPlayer.view?.player?.progress = currentState?.expansion ?: 0.0f
         updateMediaPaddings()
         updatePageIndicator()
     }
 
-    private fun updatePlayerToCurrentState(existingPlayer: MediaControlPanel) {
-        if (desiredState != null && desiredState!!.measurementInput != null) {
-            // make sure the player width is set to the current state
-            existingPlayer.setPlayerWidth(playerWidth)
-        }
-    }
-
     private fun updateMediaPaddings() {
         val padding = context.resources.getDimensionPixelSize(R.dimen.qs_media_padding)
         val childCount = mediaContent.childCount
@@ -281,117 +314,101 @@
     }
 
     /**
-     * Set the current state of a view. This is updated often during animations and we shouldn't
-     * do anything expensive.
+     * Set a new interpolated state for all players. This is a state that is usually controlled
+     * by a finger movement where the user drags from one state to the next.
      */
-    fun setCurrentState(state: MediaState) {
-        currentState = state
-        currentlyExpanded = state.expansion > 0
+    fun setCurrentState(
+        @MediaLocation startLocation: Int,
+        @MediaLocation endLocation: Int,
+        progress: Float,
+        immediately: Boolean
+    ) {
         // Hack: Since the indicator doesn't move with the player expansion, just make it disappear
         // and then reappear at the end.
-        pageIndicator.alpha = if (state.expansion == 1f || state.expansion == 0f) 1f else 0f
-        for (mediaPlayer in mediaPlayers.values) {
-            val view = mediaPlayer.view?.player
-            view?.progress = state.expansion
+        pageIndicator.alpha = if (progress == 1f || progress == 0f) 1f else 0f
+        if (startLocation != currentStartLocation ||
+                endLocation != currentEndLocation ||
+                progress != currentTransitionProgress ||
+                immediately
+        ) {
+            currentStartLocation = startLocation
+            currentEndLocation = endLocation
+            currentTransitionProgress = progress
+            for (mediaPlayer in mediaPlayers.values) {
+                updatePlayerToState(mediaPlayer, immediately)
+            }
         }
     }
 
+    private fun updatePlayerToState(mediaPlayer: MediaControlPanel, noAnimation: Boolean) {
+        mediaPlayer.mediaViewController.setCurrentState(
+                startLocation = currentStartLocation,
+                endLocation = currentEndLocation,
+                transitionProgress = currentTransitionProgress,
+                applyImmediately = noAnimation)
+    }
+
     /**
      * The desired location of this view has changed. We should remeasure the view to match
      * the new bounds and kick off bounds animations if necessary.
      * If an animation is happening, an animation is kicked of externally, which sets a new
      * current state until we reach the targetState.
      *
-     * @param desiredState the target state we're transitioning to
+     * @param desiredLocation the location we're going to
+     * @param desiredHostState the target state we're transitioning to
      * @param animate should this be animated
      */
     fun onDesiredLocationChanged(
-        desiredState: MediaState?,
+        desiredLocation: Int,
+        desiredHostState: MediaHostState?,
         animate: Boolean,
-        duration: Long,
-        startDelay: Long
+        duration: Long = 200,
+        startDelay: Long = 0
     ) {
-        if (desiredState is MediaHost.MediaHostState) {
+        desiredHostState?.let {
             // This is a hosting view, let's remeasure our players
-            this.desiredState = desiredState
-            val width = desiredState.boundsOnScreen.width()
-            if (playerWidth != width) {
-                setPlayerWidth(width)
-                for (mediaPlayer in mediaPlayers.values) {
-                    if (animate && mediaPlayer.view?.player?.visibility == View.VISIBLE) {
-                        mediaPlayer.animatePendingSizeChange(duration, startDelay)
-                    }
-                }
-                val widthSpec = desiredState.measurementInput?.widthMeasureSpec ?: 0
-                val heightSpec = desiredState.measurementInput?.heightMeasureSpec ?: 0
-                var left = 0
-                for (i in 0 until mediaContent.childCount) {
-                    val view = mediaContent.getChildAt(i)
-                    view.measure(widthSpec, heightSpec)
-                    view.layout(left, 0, left + width, view.measuredHeight)
-                    left = left + playerWidthPlusPadding
-                }
-            }
-        }
-    }
-
-    fun setPlayerWidth(width: Int) {
-        if (width != playerWidth) {
-            playerWidth = width
-            playerWidthPlusPadding = playerWidth + context.resources.getDimensionPixelSize(
-                    R.dimen.qs_media_padding)
+            this.desiredLocation = desiredLocation
+            this.desiredHostState = it
+            currentlyExpanded = it.expansion > 0
             for (mediaPlayer in mediaPlayers.values) {
-                mediaPlayer.setPlayerWidth(width)
+                if (animate) {
+                    mediaPlayer.mediaViewController.animatePendingStateChange(
+                            duration = duration,
+                            delay = startDelay)
+                }
+                mediaPlayer.mediaViewController.onLocationPreChange(desiredLocation)
             }
-            // The player width has changed, let's update the scroll position to make sure
-            // it's still at the same place
-            var newScroll = activeMediaIndex * playerWidthPlusPadding
-            if (scrollIntoCurrentMedia > playerWidthPlusPadding) {
-                newScroll += playerWidthPlusPadding
-                - (scrollIntoCurrentMedia - playerWidthPlusPadding)
-            } else {
-                newScroll += scrollIntoCurrentMedia
-            }
-            mediaCarousel.scrollX = newScroll
+            updateCarouselSize()
         }
     }
 
     /**
-     * Get a measurement for the given input state. This measures the first player and returns
-     * its bounds as if it were measured with the given measurement dimensions
+     * Update the size of the carousel, remeasuring it if necessary.
      */
-    fun obtainMeasurement(input: MediaMeasurementInput): MeasurementOutput? {
-        val firstPlayer = mediaPlayers.values.firstOrNull() ?: return null
-        var result: MeasurementOutput? = null
-        firstPlayer.view?.player?.let {
-            // Let's measure the size of the first player and return its height
-            val previousProgress = it.progress
-            val previousRight = it.right
-            val previousBottom = it.bottom
-            it.progress = input.expansion
-            firstPlayer.measure(input)
-            // Relayouting is necessary in motionlayout to obtain its size properly ....
-            it.layout(0, 0, it.measuredWidth, it.measuredHeight)
-            result = MeasurementOutput(it.measuredWidth, it.measuredHeight)
-            it.progress = previousProgress
-            if (desiredState != null) {
-                // remeasure it to the old size again!
-                firstPlayer.measure(desiredState!!.measurementInput)
-                it.layout(0, 0, previousRight, previousBottom)
+    private fun updateCarouselSize() {
+        val width = desiredHostState?.measurementInput?.width ?: 0
+        val height = desiredHostState?.measurementInput?.height ?: 0
+        if (width != carouselMeasureWidth && width != 0 ||
+                height != carouselMeasureWidth && height != 0) {
+            carouselMeasureWidth = width
+            carouselMeasureHeight = height
+            playerWidthPlusPadding = carouselMeasureWidth + context.resources.getDimensionPixelSize(
+                    R.dimen.qs_media_padding)
+            // The player width has changed, let's update the scroll position to make sure
+            // it's still at the same place
+            var newScroll = activeMediaIndex * playerWidthPlusPadding
+            if (scrollIntoCurrentMedia > playerWidthPlusPadding) {
+                newScroll += playerWidthPlusPadding -
+                        (scrollIntoCurrentMedia - playerWidthPlusPadding)
+            } else {
+                newScroll += scrollIntoCurrentMedia
             }
-        }
-        return result
-    }
-
-    fun onViewReattached() {
-        if (desiredState is MediaHost.MediaHostState) {
-            // HACK: MotionLayout doesn't always properly reevalate the state, let's kick of
-            // a measure to force it.
-            val widthSpec = desiredState!!.measurementInput?.widthMeasureSpec ?: 0
-            val heightSpec = desiredState!!.measurementInput?.heightMeasureSpec ?: 0
-            for (mediaPlayer in mediaPlayers.values) {
-                mediaPlayer.view?.player?.measure(widthSpec, heightSpec)
-            }
+            mediaCarousel.scrollX = newScroll
+            // Let's remeasure the carousel
+            val widthSpec = desiredHostState?.measurementInput?.widthMeasureSpec ?: 0
+            val heightSpec = desiredHostState?.measurementInput?.heightMeasureSpec ?: 0
+            mediaCarousel.measure(widthSpec, heightSpec)
+            mediaCarousel.layout(0, 0, width, mediaCarousel.measuredHeight)
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt
index 764dbe6..60c576b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt
@@ -23,18 +23,15 @@
 import android.widget.ImageView
 import android.widget.SeekBar
 import android.widget.TextView
-
-import androidx.constraintlayout.motion.widget.MotionLayout
-
 import com.android.systemui.R
+import com.android.systemui.util.animation.TransitionLayout
 
 /**
  * ViewHolder for a media player.
  */
 class PlayerViewHolder private constructor(itemView: View) {
 
-    val player = itemView as MotionLayout
-    val background = itemView.requireViewById<View>(R.id.media_background)
+    val player = itemView as TransitionLayout
 
     // Player information
     val appIcon = itemView.requireViewById<ImageView>(R.id.icon)
@@ -61,7 +58,7 @@
     val action4 = itemView.requireViewById<ImageButton>(R.id.action4)
 
     init {
-        (background.background as IlluminationDrawable).let {
+        (player.background as IlluminationDrawable).let {
             it.setupTouch(seamless, player)
             it.setupTouch(action0, player)
             it.setupTouch(action1, player)
@@ -95,7 +92,7 @@
          * @param parent Parent of inflated view.
          */
         @JvmStatic fun create(inflater: LayoutInflater, parent: ViewGroup): PlayerViewHolder {
-            val v = inflater.inflate(R.layout.qs_media_panel, parent, false)
+            val v = inflater.inflate(R.layout.media_view, parent, false)
             return PlayerViewHolder(v)
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipBoundsHandler.java b/packages/SystemUI/src/com/android/systemui/pip/PipBoundsHandler.java
index b9b8a25..a10972e 100644
--- a/packages/SystemUI/src/com/android/systemui/pip/PipBoundsHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/pip/PipBoundsHandler.java
@@ -36,10 +36,11 @@
 import android.util.TypedValue;
 import android.view.DisplayInfo;
 import android.view.Gravity;
-import android.view.IWindowManager;
-import android.view.WindowManagerGlobal;
 import android.window.WindowContainerTransaction;
 
+import com.android.systemui.wm.DisplayController;
+import com.android.systemui.wm.DisplayLayout;
+
 import java.io.PrintWriter;
 
 import javax.inject.Inject;
@@ -56,10 +57,10 @@
     private static final float INVALID_SNAP_FRACTION = -1f;
 
     private final Context mContext;
-    private final IWindowManager mWindowManager;
     private final PipSnapAlgorithm mSnapAlgorithm;
     private final DisplayInfo mDisplayInfo = new DisplayInfo();
-    private final Rect mTmpInsets = new Rect();
+    private final DisplayController mDisplayController;
+    private final DisplayLayout mDisplayLayout;
 
     private ComponentName mLastPipComponentName;
     private float mReentrySnapFraction = INVALID_SNAP_FRACTION;
@@ -80,11 +81,24 @@
     private boolean mIsShelfShowing;
     private int mShelfHeight;
 
+    private final DisplayController.OnDisplaysChangedListener mDisplaysChangedListener =
+            new DisplayController.OnDisplaysChangedListener() {
+        @Override
+        public void onDisplayAdded(int displayId) {
+            if (displayId == mContext.getDisplayId()) {
+                mDisplayLayout.set(mDisplayController.getDisplayLayout(displayId));
+            }
+        }
+    };
+
     @Inject
-    public PipBoundsHandler(Context context, PipSnapAlgorithm pipSnapAlgorithm) {
+    public PipBoundsHandler(Context context, PipSnapAlgorithm pipSnapAlgorithm,
+            DisplayController displayController) {
         mContext = context;
         mSnapAlgorithm = pipSnapAlgorithm;
-        mWindowManager = WindowManagerGlobal.getWindowManagerService();
+        mDisplayLayout = new DisplayLayout();
+        mDisplayController = displayController;
+        mDisplayController.addDisplayWindowListener(mDisplaysChangedListener);
         reloadResources();
         // Initialize the aspect ratio to the default aspect ratio.  Don't do this in reload
         // resources as it would clobber mAspectRatio when entering PiP from fullscreen which
@@ -272,8 +286,8 @@
      *
      * @return {@code true} if internal {@link DisplayInfo} is rotated, {@code false} otherwise.
      */
-    public boolean onDisplayRotationChanged(Rect outBounds, Rect oldBounds, int displayId,
-            int fromRotation, int toRotation, WindowContainerTransaction t) {
+    public boolean onDisplayRotationChanged(Rect outBounds, Rect oldBounds, Rect outInsetBounds,
+            int displayId, int fromRotation, int toRotation, WindowContainerTransaction t) {
         // Bail early if the event is not sent to current {@link #mDisplayInfo}
         if ((displayId != mDisplayInfo.displayId) || (fromRotation == toRotation)) {
             return false;
@@ -294,6 +308,9 @@
         final Rect postChangeStackBounds = new Rect(oldBounds);
         final float snapFraction = getSnapFraction(postChangeStackBounds);
 
+        // Update the display layout
+        mDisplayLayout.rotateTo(mContext.getResources(), toRotation);
+
         // Populate the new {@link #mDisplayInfo}.
         // The {@link DisplayInfo} queried from DisplayManager would be the one before rotation,
         // therefore, the width/height may require a swap first.
@@ -308,6 +325,7 @@
         mSnapAlgorithm.applySnapFraction(postChangeStackBounds, postChangeMovementBounds,
                 snapFraction);
 
+        getInsetBounds(outInsetBounds);
         outBounds.set(postChangeStackBounds);
         t.setBounds(pinnedStackInfo.stackToken, outBounds);
         return true;
@@ -425,15 +443,11 @@
      * Populates the bounds on the screen that the PIP can be visible in.
      */
     protected void getInsetBounds(Rect outRect) {
-        try {
-            mWindowManager.getStableInsets(mContext.getDisplayId(), mTmpInsets);
-            outRect.set(mTmpInsets.left + mScreenEdgeInsets.x,
-                    mTmpInsets.top + mScreenEdgeInsets.y,
-                    mDisplayInfo.logicalWidth - mTmpInsets.right - mScreenEdgeInsets.x,
-                    mDisplayInfo.logicalHeight - mTmpInsets.bottom - mScreenEdgeInsets.y);
-        } catch (RemoteException e) {
-            Log.e(TAG, "Failed to get stable insets from WM", e);
-        }
+        Rect insets = mDisplayLayout.stableInsets();
+        outRect.set(insets.left + mScreenEdgeInsets.x,
+                insets.top + mScreenEdgeInsets.y,
+                mDisplayInfo.logicalWidth - insets.right - mScreenEdgeInsets.x,
+                mDisplayInfo.logicalHeight - insets.bottom - mScreenEdgeInsets.y);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java
index 64df2ff..02bf745 100644
--- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java
+++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java
@@ -95,8 +95,21 @@
     private final DisplayChangeController.OnDisplayChangingListener mRotationController = (
             int displayId, int fromRotation, int toRotation, WindowContainerTransaction t) -> {
         final boolean changed = mPipBoundsHandler.onDisplayRotationChanged(mTmpNormalBounds,
-                mPipTaskOrganizer.getLastReportedBounds(), displayId, fromRotation, toRotation, t);
+                mPipTaskOrganizer.getLastReportedBounds(), mTmpInsetBounds, displayId, fromRotation,
+                toRotation, t);
         if (changed) {
+            // If the pip was in the offset zone earlier, adjust the new bounds to the bottom of the
+            // movement bounds
+            mTouchHandler.adjustBoundsForRotation(mTmpNormalBounds,
+                    mPipTaskOrganizer.getLastReportedBounds(), mTmpInsetBounds);
+
+            // The bounds are being applied to a specific snap fraction, so reset any known offsets
+            // for the previous orientation before updating the movement bounds
+            mPipBoundsHandler.setShelfHeight(false , 0);
+            mPipBoundsHandler.onImeVisibilityChanged(false, 0);
+            mTouchHandler.onShelfVisibilityChanged(false, 0);
+            mTouchHandler.onImeVisibilityChanged(false, 0);
+
             updateMovementBounds(mTmpNormalBounds, true /* fromRotation */,
                     false /* fromImeAdjustment */, false /* fromShelfAdjustment */);
         }
@@ -290,9 +303,10 @@
     @Override
     public void setShelfHeight(boolean visible, int height) {
         mHandler.post(() -> {
-            final boolean changed = mPipBoundsHandler.setShelfHeight(visible, height);
+            final int shelfHeight = visible ? height : 0;
+            final boolean changed = mPipBoundsHandler.setShelfHeight(visible, shelfHeight);
             if (changed) {
-                mTouchHandler.onShelfVisibilityChanged(visible, height);
+                mTouchHandler.onShelfVisibilityChanged(visible, shelfHeight);
                 updateMovementBounds(mPipTaskOrganizer.getLastReportedBounds(),
                         false /* fromRotation */, false /* fromImeAdjustment */,
                         true /* fromShelfAdjustment */);
diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java
index 3396f70..a3185a2 100644
--- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java
@@ -99,6 +99,7 @@
     public static final int MESSAGE_ANIMATION_ENDED = 6;
     public static final int MESSAGE_POINTER_EVENT = 7;
     public static final int MESSAGE_MENU_EXPANDED = 8;
+    public static final int MESSAGE_FADE_OUT_MENU = 9;
 
     private static final int INITIAL_DISMISS_DELAY = 3500;
     private static final int POST_INTERACTION_DISMISS_DELAY = 2000;
@@ -182,6 +183,10 @@
                     mMenuContainerAnimator.start();
                     break;
                 }
+                case MESSAGE_FADE_OUT_MENU: {
+                    fadeOutMenu();
+                    break;
+                }
             }
         }
     };
@@ -409,6 +414,18 @@
         }
     }
 
+    /**
+     * Different from {@link #hideMenu()}, this function does not try to finish this menu activity
+     * and instead, it fades out the controls by setting the alpha to 0 directly without menu
+     * visibility callbacks invoked.
+     */
+    private void fadeOutMenu() {
+        mMenuContainer.setAlpha(0f);
+        mSettingsButton.setAlpha(0f);
+        mDismissButton.setAlpha(0f);
+        mResizeHandle.setAlpha(0f);
+    }
+
     private void hideMenu() {
         hideMenu(null);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivityController.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivityController.java
index bf2c3e9..8b4d932 100644
--- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivityController.java
+++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivityController.java
@@ -262,6 +262,9 @@
      */
     public void showMenuWithDelay(int menuState, Rect stackBounds, boolean allowMenuTimeout,
             boolean willResizeMenu, boolean showResizeHandle) {
+        // hide all visible controls including close button and etc. first, this is to ensure
+        // menu is totally invisible during the transition to eliminate unpleasant artifacts
+        fadeOutMenu();
         showMenuInternal(menuState, stackBounds, allowMenuTimeout, willResizeMenu,
                 true /* withDelay */, showResizeHandle);
     }
@@ -347,6 +350,23 @@
         }
     }
 
+    private void fadeOutMenu() {
+        if (DEBUG) {
+            Log.d(TAG, "fadeOutMenu() state=" + mMenuState
+                    + " hasActivity=" + (mToActivityMessenger != null)
+                    + " callers=\n" + Debug.getCallers(5, "    "));
+        }
+        if (mToActivityMessenger != null) {
+            Message m = Message.obtain();
+            m.what = PipMenuActivity.MESSAGE_FADE_OUT_MENU;
+            try {
+                mToActivityMessenger.send(m);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Could not notify menu to fade out", e);
+            }
+        }
+    }
+
     /**
      * Hides the menu activity.
      */
@@ -513,7 +533,8 @@
     private void onMenuStateChanged(int menuState, boolean resize, Runnable callback) {
         if (DEBUG) {
             Log.d(TAG, "onMenuStateChanged() mMenuState=" + mMenuState
-                    + " menuState=" + menuState + " resize=" + resize);
+                    + " menuState=" + menuState + " resize=" + resize
+                    + " callers=\n" + Debug.getCallers(5, "    "));
         }
 
         if (menuState != mMenuState) {
diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipResizeGestureHandler.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipResizeGestureHandler.java
index cc3ab29d..4b23e67 100644
--- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipResizeGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipResizeGestureHandler.java
@@ -37,6 +37,7 @@
 import android.view.InputEventReceiver;
 import android.view.InputMonitor;
 import android.view.MotionEvent;
+import android.view.ViewConfiguration;
 
 import com.android.internal.policy.TaskResizingAlgorithm;
 import com.android.systemui.R;
@@ -77,10 +78,12 @@
     private final Runnable mUpdateMovementBoundsRunnable;
 
     private int mDelta;
+    private float mTouchSlop;
     private boolean mAllowGesture;
     private boolean mIsAttached;
     private boolean mIsEnabled;
     private boolean mEnableUserResize;
+    private boolean mThresholdCrossed;
 
     private InputMonitor mInputMonitor;
     private InputEventReceiver mInputEventReceiver;
@@ -100,6 +103,7 @@
         mPipTaskOrganizer = pipTaskOrganizer;
         mMovementBoundsSupplier = movementBoundsSupplier;
         mUpdateMovementBoundsRunnable = updateMovementBoundsRunnable;
+
         context.getDisplay().getRealSize(mMaxSize);
         reloadResources();
 
@@ -126,6 +130,7 @@
     private void reloadResources() {
         final Resources res = mContext.getResources();
         mDelta = res.getDimensionPixelSize(R.dimen.pip_resize_edge_size);
+        mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();
     }
 
     private void resetDragCorners() {
@@ -270,7 +275,12 @@
                     break;
                 case MotionEvent.ACTION_MOVE:
                     // Capture inputs
-                    mInputMonitor.pilferPointers();
+                    float dx = Math.abs(ev.getX() - mDownPoint.x);
+                    float dy = Math.abs(ev.getY() - mDownPoint.y);
+                    if (!mThresholdCrossed && dx > mTouchSlop && dy > mTouchSlop) {
+                        mThresholdCrossed = true;
+                        mInputMonitor.pilferPointers();
+                    }
                     final Rect currentPipBounds = mMotionHelper.getBounds();
                     mLastResizeBounds.set(TaskResizingAlgorithm.resizeDrag(ev.getX(), ev.getY(),
                             mDownPoint.x, mDownPoint.y, currentPipBounds, mCtrlType, mMinSize.x,
@@ -288,6 +298,7 @@
                             mUpdateMovementBoundsRunnable.run();
                             mCtrlType = CTRL_NONE;
                             mAllowGesture = false;
+                            mThresholdCrossed = false;
                         });
                     });
                     break;
diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java
index c408caa..c274ee9 100644
--- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java
@@ -397,6 +397,15 @@
         mShelfHeight = shelfHeight;
     }
 
+    public void adjustBoundsForRotation(Rect outBounds, Rect curBounds, Rect insetBounds) {
+        final Rect toMovementBounds = new Rect();
+        mSnapAlgorithm.getMovementBounds(outBounds, insetBounds, toMovementBounds, 0);
+        final int prevBottom = mMovementBounds.bottom - mMovementBoundsExtraOffsets;
+        if ((prevBottom - mBottomOffsetBufferPx) <= curBounds.top) {
+            outBounds.offsetTo(outBounds.left, toMovementBounds.bottom);
+        }
+    }
+
     public void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, Rect curBounds,
             boolean fromImeAdjustment, boolean fromShelfAdjustment, int displayRotation) {
         final int bottomOffset = mIsImeShowing ? mImeHeight : 0;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
index b877e87..8f9e9e2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
@@ -427,7 +427,7 @@
                     (ViewGroup.MarginLayoutParams) hostView.getLayoutParams();
             float targetPosition = absoluteBottomPosition - params.bottomMargin
                     - hostView.getHeight();
-            float currentPosition = mediaHost.getCurrentState().getBoundsOnScreen().top
+            float currentPosition = mediaHost.getCurrentBounds().top
                     - hostView.getTranslationY();
             hostView.setTranslationY(targetPosition - currentPosition);
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index cdde06b..4f0b56e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -187,9 +187,9 @@
     }
 
     protected void addMediaHostView() {
-        mMediaHost.init(MediaHierarchyManager.LOCATION_QS);
         mMediaHost.setExpansion(1.0f);
         mMediaHost.setShowsOnlyActiveMedia(false);
+        mMediaHost.init(MediaHierarchyManager.LOCATION_QS);
         ViewGroup hostView = mMediaHost.getHostView();
         addView(hostView);
         int sidePaddings = getResources().getDimensionPixelSize(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
index 2f06c4b..75507be 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
@@ -190,9 +190,9 @@
             switchTileLayout();
             return null;
         });
-        mMediaHost.init(MediaHierarchyManager.LOCATION_QQS);
         mMediaHost.setExpansion(0.0f);
         mMediaHost.setShowsOnlyActiveMedia(true);
+        mMediaHost.init(MediaHierarchyManager.LOCATION_QQS);
         reAttachMediaHost();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
index e88ce5a..f0a81e9 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
@@ -26,7 +26,6 @@
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.UserInfo;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.drawable.Icon;
@@ -146,7 +145,7 @@
             CompletableFuture<List<Notification.Action>> smartActionsFuture =
                     ScreenshotSmartActions.getSmartActionsFuture(
                             mScreenshotId, uri, image, mSmartActionsProvider,
-                            mSmartActionsEnabled, isManagedProfile(mContext));
+                            mSmartActionsEnabled, getUserHandle(mContext));
 
             try {
                 // First, write the actual data for our screenshot
@@ -382,10 +381,9 @@
         }
     }
 
-    private boolean isManagedProfile(Context context) {
+    private UserHandle getUserHandle(Context context) {
         UserManager manager = UserManager.get(context);
-        UserInfo info = manager.getUserInfo(getUserHandleOfForegroundApplication(context));
-        return info.isManagedProfile();
+        return manager.getUserInfo(getUserHandleOfForegroundApplication(context)).getUserHandle();
     }
 
     private List<Notification.Action> buildSmartActions(
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsProvider.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsProvider.java
index 3edb33d..63f323e 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsProvider.java
@@ -20,6 +20,7 @@
 import android.content.ComponentName;
 import android.graphics.Bitmap;
 import android.net.Uri;
+import android.os.UserHandle;
 import android.util.Log;
 
 import java.util.Collections;
@@ -64,14 +65,14 @@
      * @param componentName      Contains package and activity class names where the screenshot was
      *                           taken. This is used as an additional signal to generate and rank
      *                           more relevant actions.
-     * @param isManagedProfile   The screenshot was taken for a work profile app.
+     * @param userHandle         The user handle of the app where the screenshot was taken.
      */
     public CompletableFuture<List<Notification.Action>> getActions(
             String screenshotId,
             Uri screenshotUri,
             Bitmap bitmap,
             ComponentName componentName,
-            boolean isManagedProfile) {
+            UserHandle userHandle) {
         Log.d(TAG, "Returning empty smart action list.");
         return CompletableFuture.completedFuture(Collections.emptyList());
     }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSmartActions.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSmartActions.java
index c228fe2..442b373 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSmartActions.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSmartActions.java
@@ -26,6 +26,7 @@
 import android.net.Uri;
 import android.os.Handler;
 import android.os.SystemClock;
+import android.os.UserHandle;
 import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -48,7 +49,7 @@
     static CompletableFuture<List<Notification.Action>> getSmartActionsFuture(
             String screenshotId, Uri screenshotUri, Bitmap image,
             ScreenshotNotificationSmartActionsProvider smartActionsProvider,
-            boolean smartActionsEnabled, boolean isManagedProfile) {
+            boolean smartActionsEnabled, UserHandle userHandle) {
         if (!smartActionsEnabled) {
             Slog.i(TAG, "Screenshot Intelligence not enabled, returning empty list.");
             return CompletableFuture.completedFuture(Collections.emptyList());
@@ -60,7 +61,7 @@
             return CompletableFuture.completedFuture(Collections.emptyList());
         }
 
-        Slog.d(TAG, "Screenshot from a managed profile: " + isManagedProfile);
+        Slog.d(TAG, "Screenshot from user profile: " + userHandle.getIdentifier());
         CompletableFuture<List<Notification.Action>> smartActionsFuture;
         long startTimeMs = SystemClock.uptimeMillis();
         try {
@@ -71,7 +72,7 @@
                             ? runningTask.topActivity
                             : new ComponentName("", "");
             smartActionsFuture = smartActionsProvider.getActions(
-                    screenshotId, screenshotUri, image, componentName, isManagedProfile);
+                    screenshotId, screenshotUri, image, componentName, userHandle);
         } catch (Throwable e) {
             long waitTimeMs = SystemClock.uptimeMillis() - startTimeMs;
             smartActionsFuture = CompletableFuture.completedFuture(Collections.emptyList());
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java
index b57b22f..8e6398f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java
@@ -133,7 +133,7 @@
     }
 
     public static void fadeIn(View view, float fadeInAmount) {
-        fadeIn(view, fadeInAmount, true /* remap */);
+        fadeIn(view, fadeInAmount, false /* remap */);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInteractionTracker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInteractionTracker.kt
index 2ed04eb..9dbec10 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInteractionTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInteractionTracker.kt
@@ -22,7 +22,9 @@
         entryManager.addCollectionListener(this)
     }
 
-    fun hasUserInteractedWith(key: String): Boolean = key in interactions
+    fun hasUserInteractedWith(key: String): Boolean {
+        return interactions[key] ?: false
+    }
 
     override fun onEntryAdded(entry: NotificationEntry) {
         interactions[entry.key] = false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/CustomInterpolatorTransformation.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/CustomInterpolatorTransformation.java
index dea1a07..cb7da4f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/CustomInterpolatorTransformation.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/CustomInterpolatorTransformation.java
@@ -66,7 +66,7 @@
             return false;
         }
         View view = ownState.getTransformedView();
-        CrossFadeHelper.fadeIn(view, transformationAmount);
+        CrossFadeHelper.fadeIn(view, transformationAmount, true /* remap */);
         ownState.transformViewFullyFrom(otherState, this, transformationAmount);
         otherState.recycle();
         return true;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java
index 27109d2..9a8cff0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java
@@ -95,7 +95,7 @@
         if (sameAs(otherState)) {
             ensureVisible();
         } else {
-            CrossFadeHelper.fadeIn(mTransformedView, transformationAmount);
+            CrossFadeHelper.fadeIn(mTransformedView, transformationAmount, true /* remap */);
         }
         transformViewFullyFrom(otherState, transformationAmount);
     }
@@ -424,7 +424,7 @@
         if (transformationAmount == 0.0f) {
             prepareFadeIn();
         }
-        CrossFadeHelper.fadeIn(mTransformedView, transformationAmount);
+        CrossFadeHelper.fadeIn(mTransformedView, transformationAmount, true /* remap */);
     }
 
     public void disappear(float transformationAmount, TransformableView otherView) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
index da31fe0..71f6dac 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
@@ -146,14 +146,6 @@
             return false;
         }
 
-        if (!entry.isBubble()) {
-            if (DEBUG) {
-                Log.d(TAG, "No bubble up: notification " + sbn.getKey()
-                        + " is bubble? " + entry.isBubble());
-            }
-            return false;
-        }
-
         if (entry.getBubbleMetadata() == null
                 || (entry.getBubbleMetadata().getShortcutId() == null
                     && entry.getBubbleMetadata().getIntent() == null)) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
index 92b597b..f1727ec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
@@ -123,8 +123,6 @@
     private float mAppearAnimationFraction = -1.0f;
     private float mAppearAnimationTranslation;
     private int mNormalColor;
-    private boolean mLastInSection;
-    private boolean mFirstInSection;
     private boolean mIsBelowSpeedBump;
 
     private float mNormalBackgroundVisibilityAmount;
@@ -430,27 +428,21 @@
         mBackgroundDimmed.setDistanceToTopRoundness(distanceToTopRoundness);
     }
 
-    public boolean isLastInSection() {
-        return mLastInSection;
-    }
-
-    public boolean isFirstInSection() {
-        return mFirstInSection;
-    }
-
     /** Sets whether this view is the last notification in a section. */
+    @Override
     public void setLastInSection(boolean lastInSection) {
         if (lastInSection != mLastInSection) {
-            mLastInSection = lastInSection;
+            super.setLastInSection(lastInSection);
             mBackgroundNormal.setLastInSection(lastInSection);
             mBackgroundDimmed.setLastInSection(lastInSection);
         }
     }
 
     /** Sets whether this view is the first notification in a section. */
+    @Override
     public void setFirstInSection(boolean firstInSection) {
         if (firstInSection != mFirstInSection) {
-            mFirstInSection = firstInSection;
+            super.setFirstInSection(firstInSection);
             mBackgroundNormal.setFirstInSection(firstInSection);
             mBackgroundDimmed.setFirstInSection(firstInSection);
         }
@@ -963,6 +955,7 @@
         return false;
     }
 
+    @Override
     public int getHeadsUpHeightWithoutHeader() {
         return getHeight();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
index 049cafa..26ccd72 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
@@ -262,10 +262,7 @@
         setClipToOutline(mAlwaysRoundBothCorners);
     }
 
-    /**
-     * Set the topRoundness of this view.
-     * @return Whether the roundness was changed.
-     */
+    @Override
     public boolean setTopRoundness(float topRoundness, boolean animate) {
         if (mTopRoundness != topRoundness) {
             mTopRoundness = topRoundness;
@@ -302,10 +299,7 @@
         return mCurrentBottomRoundness * mOutlineRadius;
     }
 
-    /**
-     * Set the bottom roundness of this view.
-     * @return Whether the roundness was changed.
-     */
+    @Override
     public boolean setBottomRoundness(float bottomRoundness, boolean animate) {
         if (mBottomRoundness != bottomRoundness) {
             mBottomRoundness = bottomRoundness;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
index 0831c0b..7ed8350 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
@@ -67,6 +67,8 @@
     protected int mContentShift;
     private final ExpandableViewState mViewState;
     private float mContentTranslation;
+    protected boolean mLastInSection;
+    protected boolean mFirstInSection;
 
     public ExpandableView(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -771,6 +773,44 @@
         return true;
     }
 
+    /** Sets whether this view is the first notification in a section. */
+    public void setFirstInSection(boolean firstInSection) {
+        mFirstInSection = firstInSection;
+    }
+
+    /** Sets whether this view is the last notification in a section. */
+    public void setLastInSection(boolean lastInSection) {
+        mLastInSection = lastInSection;
+    }
+
+    public boolean isLastInSection() {
+        return mLastInSection;
+    }
+
+    public boolean isFirstInSection() {
+        return mFirstInSection;
+    }
+
+    /**
+     * Set the topRoundness of this view.
+     * @return Whether the roundness was changed.
+     */
+    public boolean setTopRoundness(float topRoundness, boolean animate) {
+        return false;
+    }
+
+    /**
+     * Set the bottom roundness of this view.
+     * @return Whether the roundness was changed.
+     */
+    public boolean setBottomRoundness(float bottomRoundness, boolean animate) {
+        return false;
+    }
+
+    public int getHeadsUpHeightWithoutHeader() {
+        return getHeight();
+    }
+
     /**
      * A listener notifying when {@link #getActualHeight} changes.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridNotificationView.java
index 2071449..bc2adac3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridNotificationView.java
@@ -92,7 +92,7 @@
                         // We want to transform from the same y location as the title
                         TransformState otherState = notification.getCurrentState(
                                 TRANSFORMING_VIEW_TITLE);
-                        CrossFadeHelper.fadeIn(mTextView, transformationAmount);
+                        CrossFadeHelper.fadeIn(mTextView, transformationAmount, true /* remap */);
                         if (otherState != null) {
                             ownState.transformViewVerticalFrom(otherState, transformationAmount);
                             otherState.recycle();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java
index 2d99ab1..14aab9d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java
@@ -23,7 +23,6 @@
 import android.content.res.ColorStateList;
 import android.graphics.Color;
 import android.graphics.PorterDuffColorFilter;
-import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.service.notification.StatusBarNotification;
 import android.util.ArraySet;
@@ -107,7 +106,7 @@
                         TransformState otherState = notification.getCurrentState(
                                 TRANSFORMING_VIEW_TITLE);
                         final View text = ownState.getTransformedView();
-                        CrossFadeHelper.fadeIn(text, transformationAmount);
+                        CrossFadeHelper.fadeIn(text, transformationAmount, true /* remap */);
                         if (otherState != null) {
                             ownState.transformViewVerticalFrom(otherState, this,
                                     transformationAmount);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java
index fa7f282..02e537d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java
@@ -284,7 +284,7 @@
 
     @Override
     public void transformFrom(TransformableView notification, float transformationAmount) {
-        CrossFadeHelper.fadeIn(mView, transformationAmount);
+        CrossFadeHelper.fadeIn(mView, transformationAmount, true /* remap */);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index ecab188..b4220f1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -64,7 +64,7 @@
     private int mZDistanceBetweenElements;
     private int mBaseZHeight;
     private int mMaxLayoutHeight;
-    private ActivatableNotificationView mLastVisibleBackgroundChild;
+    private ExpandableView mLastVisibleBackgroundChild;
     private float mCurrentScrollVelocity;
     private int mStatusBarState;
     private float mExpandingVelocity;
@@ -346,11 +346,11 @@
      * view in the shade, without the clear all button.
      */
     public void setLastVisibleBackgroundChild(
-            ActivatableNotificationView lastVisibleBackgroundChild) {
+            ExpandableView lastVisibleBackgroundChild) {
         mLastVisibleBackgroundChild = lastVisibleBackgroundChild;
     }
 
-    public ActivatableNotificationView getLastVisibleBackgroundChild() {
+    public ExpandableView getLastVisibleBackgroundChild() {
         return mLastVisibleBackgroundChild;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaHeaderView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaHeaderView.java
index 3ac322f..383f2a2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaHeaderView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaHeaderView.java
@@ -16,45 +16,35 @@
 
 package com.android.systemui.statusbar.notification.stack;
 
+import android.animation.AnimatorListenerAdapter;
 import android.content.Context;
 import android.util.AttributeSet;
-import android.view.View;
 import android.view.ViewGroup;
 
-import com.android.systemui.R;
-import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
+import com.android.systemui.statusbar.notification.row.ExpandableView;
 
 /**
  * Root view to insert Lock screen media controls into the notification stack.
  */
-public class MediaHeaderView extends ActivatableNotificationView {
-
-    private View mContentView;
+public class MediaHeaderView extends ExpandableView {
 
     public MediaHeaderView(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
     @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
+    public long performRemoveAnimation(long duration, long delay, float translationDirection,
+            boolean isHeadsUpAnimation, float endLocation, Runnable onFinishedRunnable,
+            AnimatorListenerAdapter animationListener) {
+        return 0;
     }
 
     @Override
-    protected View getContentView() {
-        return mContentView;
-    }
-
-    /**
-     * Sets the background color, to be used when album art changes.
-     * @param color background
-     */
-    public void setBackgroundColor(int color) {
-        setTintColor(color);
+    public void performAddAnimation(long delay, long duration, boolean isHeadsUpAppear) {
+        // No animation, it doesn't need it, this would be local
     }
 
     public void setContentView(ViewGroup contentView) {
-        mContentView = contentView;
         addView(contentView);
         ViewGroup.LayoutParams layoutParams = contentView.getLayoutParams();
         layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java
index b4f7b59..2c3239a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java
@@ -20,7 +20,6 @@
 
 import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
@@ -37,10 +36,10 @@
 @Singleton
 public class NotificationRoundnessManager implements OnHeadsUpChangedListener {
 
-    private final ActivatableNotificationView[] mFirstInSectionViews;
-    private final ActivatableNotificationView[] mLastInSectionViews;
-    private final ActivatableNotificationView[] mTmpFirstInSectionViews;
-    private final ActivatableNotificationView[] mTmpLastInSectionViews;
+    private final ExpandableView[] mFirstInSectionViews;
+    private final ExpandableView[] mLastInSectionViews;
+    private final ExpandableView[] mTmpFirstInSectionViews;
+    private final ExpandableView[] mTmpLastInSectionViews;
     private final KeyguardBypassController mBypassController;
     private boolean mExpanded;
     private HashSet<ExpandableView> mAnimatedChildren;
@@ -53,10 +52,10 @@
             KeyguardBypassController keyguardBypassController,
             NotificationSectionsFeatureManager sectionsFeatureManager) {
         int numberOfSections = sectionsFeatureManager.getNumberOfBuckets();
-        mFirstInSectionViews = new ActivatableNotificationView[numberOfSections];
-        mLastInSectionViews = new ActivatableNotificationView[numberOfSections];
-        mTmpFirstInSectionViews = new ActivatableNotificationView[numberOfSections];
-        mTmpLastInSectionViews = new ActivatableNotificationView[numberOfSections];
+        mFirstInSectionViews = new ExpandableView[numberOfSections];
+        mLastInSectionViews = new ExpandableView[numberOfSections];
+        mTmpFirstInSectionViews = new ExpandableView[numberOfSections];
+        mTmpLastInSectionViews = new ExpandableView[numberOfSections];
         mBypassController = keyguardBypassController;
     }
 
@@ -80,14 +79,14 @@
         updateView(entry.getRow(), false /* animate */);
     }
 
-    private void updateView(ActivatableNotificationView view, boolean animate) {
+    private void updateView(ExpandableView view, boolean animate) {
         boolean changed = updateViewWithoutCallback(view, animate);
         if (changed) {
             mRoundingChangedCallback.run();
         }
     }
 
-    private boolean updateViewWithoutCallback(ActivatableNotificationView view,
+    private boolean updateViewWithoutCallback(ExpandableView view,
             boolean animate) {
         float topRoundness = getRoundness(view, true /* top */);
         float bottomRoundness = getRoundness(view, false /* top */);
@@ -100,8 +99,7 @@
         return (firstInSection || lastInSection) && (topChanged || bottomChanged);
     }
 
-    private boolean isFirstInSection(ActivatableNotificationView view,
-            boolean includeFirstSection) {
+    private boolean isFirstInSection(ExpandableView view, boolean includeFirstSection) {
         int numNonEmptySections = 0;
         for (int i = 0; i < mFirstInSectionViews.length; i++) {
             if (view == mFirstInSectionViews[i]) {
@@ -114,7 +112,7 @@
         return false;
     }
 
-    private boolean isLastInSection(ActivatableNotificationView view, boolean includeLastSection) {
+    private boolean isLastInSection(ExpandableView view, boolean includeLastSection) {
         int numNonEmptySections = 0;
         for (int i = mLastInSectionViews.length - 1; i >= 0; i--) {
             if (view == mLastInSectionViews[i]) {
@@ -127,7 +125,7 @@
         return false;
     }
 
-    private float getRoundness(ActivatableNotificationView view, boolean top) {
+    private float getRoundness(ExpandableView view, boolean top) {
         if ((view.isPinned() || view.isHeadsUpAnimatingAway()) && !mExpanded) {
             return 1.0f;
         }
@@ -174,14 +172,14 @@
     }
 
     private boolean handleRemovedOldViews(NotificationSection[] sections,
-            ActivatableNotificationView[] oldViews, boolean first) {
+            ExpandableView[] oldViews, boolean first) {
         boolean anyChanged = false;
-        for (ActivatableNotificationView oldView : oldViews) {
+        for (ExpandableView oldView : oldViews) {
             if (oldView != null) {
                 boolean isStillPresent = false;
                 boolean adjacentSectionChanged = false;
                 for (NotificationSection section : sections) {
-                    ActivatableNotificationView newView =
+                    ExpandableView newView =
                             (first ? section.getFirstVisibleChild()
                                     : section.getLastVisibleChild());
                     if (newView == oldView) {
@@ -207,14 +205,14 @@
     }
 
     private boolean handleAddedNewViews(NotificationSection[] sections,
-            ActivatableNotificationView[] oldViews, boolean first) {
+            ExpandableView[] oldViews, boolean first) {
         boolean anyChanged = false;
         for (NotificationSection section : sections) {
-            ActivatableNotificationView newView =
+            ExpandableView newView =
                     (first ? section.getFirstVisibleChild() : section.getLastVisibleChild());
             if (newView != null) {
                 boolean wasAlreadyPresent = false;
-                for (ActivatableNotificationView oldView : oldViews) {
+                for (ExpandableView oldView : oldViews) {
                     if (oldView == newView) {
                         wasAlreadyPresent = true;
                         break;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSection.java
index bad36bf..1131a65 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSection.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSection.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.notification.stack;
 
+import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManagerKt.BUCKET_MEDIA_CONTROLS;
+
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ObjectAnimator;
@@ -26,7 +28,7 @@
 
 import com.android.systemui.Interpolators;
 import com.android.systemui.statusbar.notification.ShadeViewRefactor;
-import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
+import com.android.systemui.statusbar.notification.row.ExpandableView;
 
 /**
  * Represents the bounds of a section of the notification shade and handles animation when the
@@ -41,8 +43,8 @@
     private Rect mEndAnimationRect = new Rect();
     private ObjectAnimator mTopAnimator = null;
     private ObjectAnimator mBottomAnimator = null;
-    private ActivatableNotificationView mFirstVisibleChild;
-    private ActivatableNotificationView mLastVisibleChild;
+    private ExpandableView mFirstVisibleChild;
+    private ExpandableView mLastVisibleChild;
 
     NotificationSection(View owningView, @PriorityBucket int bucket) {
         mOwningView = owningView;
@@ -198,21 +200,21 @@
         mOwningView.invalidate();
     }
 
-    public ActivatableNotificationView getFirstVisibleChild() {
+    public ExpandableView getFirstVisibleChild() {
         return mFirstVisibleChild;
     }
 
-    public ActivatableNotificationView getLastVisibleChild() {
+    public ExpandableView getLastVisibleChild() {
         return mLastVisibleChild;
     }
 
-    public boolean setFirstVisibleChild(ActivatableNotificationView child) {
+    public boolean setFirstVisibleChild(ExpandableView child) {
         boolean changed = mFirstVisibleChild != child;
         mFirstVisibleChild = child;
         return changed;
     }
 
-    public boolean setLastVisibleChild(ActivatableNotificationView child) {
+    public boolean setLastVisibleChild(ExpandableView child) {
         boolean changed = mLastVisibleChild != child;
         mLastVisibleChild = child;
         return changed;
@@ -251,7 +253,7 @@
             boolean shiftBackgroundWithFirst) {
         int top = minTopPosition;
         int bottom = minTopPosition;
-        ActivatableNotificationView firstView = getFirstVisibleChild();
+        ExpandableView firstView = getFirstVisibleChild();
         if (firstView != null) {
             // Round Y up to avoid seeing the background during animation
             int finalTranslationY = (int) Math.ceil(ViewState.getFinalTranslationY(firstView));
@@ -276,7 +278,7 @@
             }
         }
         top = Math.max(minTopPosition, top);
-        ActivatableNotificationView lastView = getLastVisibleChild();
+        ExpandableView lastView = getLastVisibleChild();
         if (lastView != null) {
             float finalTranslationY = ViewState.getFinalTranslationY(lastView);
             int finalHeight = ExpandableViewState.getFinalActualHeight(lastView);
@@ -302,4 +304,8 @@
         mBounds.bottom = bottom;
         return bottom;
     }
+
+    public boolean needsBackground() {
+        return mFirstVisibleChild != null && mBucket != BUCKET_MEDIA_CONTROLS;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
index e39a4a0..ba7675f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
@@ -35,7 +35,6 @@
 import com.android.systemui.statusbar.notification.people.PeopleHubViewBoundary
 import com.android.systemui.statusbar.notification.people.PersonViewModel
 import com.android.systemui.statusbar.notification.people.Subscription
-import com.android.systemui.statusbar.notification.row.ActivatableNotificationView
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.ExpandableView
 import com.android.systemui.statusbar.notification.row.StackScrollerDecorView
@@ -456,14 +455,14 @@
     private sealed class SectionBounds {
 
         data class Many(
-            val first: ActivatableNotificationView,
-            val last: ActivatableNotificationView
+            val first: ExpandableView,
+            val last: ExpandableView
         ) : SectionBounds()
 
-        data class One(val lone: ActivatableNotificationView) : SectionBounds()
+        data class One(val lone: ExpandableView) : SectionBounds()
         object None : SectionBounds()
 
-        fun addNotif(notif: ActivatableNotificationView): SectionBounds = when (this) {
+        fun addNotif(notif: ExpandableView): SectionBounds = when (this) {
             is None -> One(notif)
             is One -> Many(lone, notif)
             is Many -> copy(last = notif)
@@ -476,8 +475,8 @@
         }
 
         private fun NotificationSection.setFirstAndLastVisibleChildren(
-            first: ActivatableNotificationView?,
-            last: ActivatableNotificationView?
+            first: ExpandableView?,
+            last: ExpandableView?
         ): Boolean {
             val firstChanged = setFirstVisibleChild(first)
             val lastChanged = setLastVisibleChild(last)
@@ -492,7 +491,7 @@
      */
     fun updateFirstAndLastViewsForAllSections(
         sections: Array<NotificationSection>,
-        children: List<ActivatableNotificationView>
+        children: List<ExpandableView>
     ): Boolean {
         // Create mapping of bucket to section
         val sectionBounds = children.asSequence()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index e33cc60..bcafd0ee 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -701,7 +701,7 @@
      * @return the height at which we will wake up when pulsing
      */
     public float getWakeUpHeight() {
-        ActivatableNotificationView firstChild = getFirstChildWithBackground();
+        ExpandableView firstChild = getFirstChildWithBackground();
         if (firstChild != null) {
             if (mKeyguardBypassController.getBypassEnabled()) {
                 return firstChild.getHeadsUpHeightWithoutHeader();
@@ -907,7 +907,7 @@
         // TODO(kprevas): this may not be necessary any more since we don't display the shelf in AOD
         boolean anySectionHasVisibleChild = false;
         for (NotificationSection section : mSections) {
-            if (section.getFirstVisibleChild() != null) {
+            if (section.needsBackground()) {
                 anySectionHasVisibleChild = true;
                 break;
             }
@@ -950,7 +950,7 @@
         int currentRight = right;
         boolean first = true;
         for (NotificationSection section : mSections) {
-            if (section.getFirstVisibleChild() == null) {
+            if (!section.needsBackground()) {
                 continue;
             }
             int sectionTop = section.getCurrentBounds().top + animationYOffset;
@@ -2685,40 +2685,40 @@
     }
 
     @ShadeViewRefactor(RefactorComponent.COORDINATOR)
-    private ActivatableNotificationView getLastChildWithBackground() {
+    private ExpandableView getLastChildWithBackground() {
         int childCount = getChildCount();
         for (int i = childCount - 1; i >= 0; i--) {
-            View child = getChildAt(i);
-            if (child.getVisibility() != View.GONE && child instanceof ActivatableNotificationView
+            ExpandableView child = (ExpandableView) getChildAt(i);
+            if (child.getVisibility() != View.GONE && !(child instanceof StackScrollerDecorView)
                     && child != mShelf) {
-                return (ActivatableNotificationView) child;
+                return child;
             }
         }
         return null;
     }
 
     @ShadeViewRefactor(RefactorComponent.COORDINATOR)
-    private ActivatableNotificationView getFirstChildWithBackground() {
+    private ExpandableView getFirstChildWithBackground() {
         int childCount = getChildCount();
         for (int i = 0; i < childCount; i++) {
-            View child = getChildAt(i);
-            if (child.getVisibility() != View.GONE && child instanceof ActivatableNotificationView
+            ExpandableView child = (ExpandableView) getChildAt(i);
+            if (child.getVisibility() != View.GONE && !(child instanceof StackScrollerDecorView)
                     && child != mShelf) {
-                return (ActivatableNotificationView) child;
+                return child;
             }
         }
         return null;
     }
 
     //TODO: We shouldn't have to generate this list every time
-    private List<ActivatableNotificationView> getChildrenWithBackground() {
-        ArrayList<ActivatableNotificationView> children = new ArrayList<>();
+    private List<ExpandableView> getChildrenWithBackground() {
+        ArrayList<ExpandableView> children = new ArrayList<>();
         int childCount = getChildCount();
         for (int i = 0; i < childCount; i++) {
-            View child = getChildAt(i);
-            if (child.getVisibility() != View.GONE && child instanceof ActivatableNotificationView
+            ExpandableView child = (ExpandableView) getChildAt(i);
+            if (child.getVisibility() != View.GONE && !(child instanceof StackScrollerDecorView)
                     && child != mShelf) {
-                children.add((ActivatableNotificationView) child);
+                children.add(child);
             }
         }
 
@@ -3283,13 +3283,13 @@
     private void updateFirstAndLastBackgroundViews() {
         NotificationSection firstSection = getFirstVisibleSection();
         NotificationSection lastSection = getLastVisibleSection();
-        ActivatableNotificationView previousFirstChild =
+        ExpandableView previousFirstChild =
                 firstSection == null ? null : firstSection.getFirstVisibleChild();
-        ActivatableNotificationView previousLastChild =
+        ExpandableView previousLastChild =
                 lastSection == null ? null : lastSection.getLastVisibleChild();
 
-        ActivatableNotificationView firstChild = getFirstChildWithBackground();
-        ActivatableNotificationView lastChild = getLastChildWithBackground();
+        ExpandableView firstChild = getFirstChildWithBackground();
+        ExpandableView lastChild = getLastChildWithBackground();
         boolean sectionViewsChanged = mSectionsManager.updateFirstAndLastViewsForAllSections(
                 mSections, getChildrenWithBackground());
 
@@ -4575,7 +4575,7 @@
                 ? (ExpandableNotificationRow) view
                 : null;
         NotificationSection firstSection = getFirstVisibleSection();
-        ActivatableNotificationView firstVisibleChild =
+        ExpandableView firstVisibleChild =
                 firstSection == null ? null : firstSection.getFirstVisibleChild();
         if (row != null) {
             if (row == firstVisibleChild
@@ -4611,7 +4611,7 @@
                 }
                 int layoutEnd = mMaxLayoutHeight + (int) mStackTranslation;
                 NotificationSection lastSection = getLastVisibleSection();
-                ActivatableNotificationView lastVisibleChild =
+                ExpandableView lastVisibleChild =
                         lastSection == null ? null : lastSection.getLastVisibleChild();
                 if (row != lastVisibleChild && mShelf.getVisibility() != GONE) {
                     layoutEnd -= mShelf.getIntrinsicHeight() + mPaddingBetweenElements;
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 64e5f0a..7bcfb46 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -350,7 +350,6 @@
         }
         Intent fillInIntent = null;
         NotificationEntry entry = row.getEntry();
-        final boolean isBubble = entry.isBubble();
         CharSequence remoteInputText = null;
         if (!TextUtils.isEmpty(entry.remoteInputText)) {
             remoteInputText = entry.remoteInputText;
@@ -359,14 +358,15 @@
             fillInIntent = new Intent().putExtra(Notification.EXTRA_REMOTE_INPUT_DRAFT,
                     remoteInputText.toString());
         }
-        if (isBubble) {
+        final boolean canBubble = entry.canBubble();
+        if (canBubble) {
             mLogger.logExpandingBubble(notificationKey);
-            expandBubbleStackOnMainThread(notificationKey);
+            expandBubbleStackOnMainThread(entry);
         } else {
             startNotificationIntent(
                     intent, fillInIntent, entry, row, wasOccluded, isActivityIntent);
         }
-        if (isActivityIntent || isBubble) {
+        if (isActivityIntent || canBubble) {
             mAssistManagerLazy.get().hideAssist();
         }
         if (shouldCollapse()) {
@@ -381,7 +381,7 @@
                 rank, count, true, location);
         mClickNotifier.onNotificationClick(notificationKey, nv);
 
-        if (!isBubble) {
+        if (!canBubble) {
             if (parentToCancelFinal != null) {
                 // TODO: (b/145659174) remove - this cancels the parent if the notification clicked
                 // on will auto-cancel and is the only child in the group. This won't be
@@ -398,12 +398,12 @@
         mIsCollapsingToShowActivityOverLockscreen = false;
     }
 
-    private void expandBubbleStackOnMainThread(String notificationKey) {
+    private void expandBubbleStackOnMainThread(NotificationEntry entry) {
         if (Looper.getMainLooper().isCurrentThread()) {
-            mBubbleController.expandStackAndSelectBubble(notificationKey);
+            mBubbleController.expandStackAndSelectBubble(entry);
         } else {
             mMainThreadHandler.post(
-                    () -> mBubbleController.expandStackAndSelectBubble(notificationKey));
+                    () -> mBubbleController.expandStackAndSelectBubble(entry));
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
index 669e6a4..72395e6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
@@ -30,7 +30,6 @@
 import android.os.Handler;
 import android.os.RemoteException;
 import android.os.UserHandle;
-import android.util.Log;
 import android.view.View;
 import android.view.ViewParent;
 
@@ -49,8 +48,6 @@
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
-import java.util.concurrent.atomic.AtomicReference;
-
 import javax.inject.Inject;
 import javax.inject.Singleton;
 
@@ -58,8 +55,7 @@
  */
 @Singleton
 public class StatusBarRemoteInputCallback implements Callback, Callbacks,
-        StatusBarStateController.StateListener, KeyguardStateController.Callback {
-    private static final String TAG = StatusBarRemoteInputCallback.class.getSimpleName();
+        StatusBarStateController.StateListener {
 
     private final KeyguardStateController mKeyguardStateController;
     private final SysuiStatusBarStateController mStatusBarStateController;
@@ -78,7 +74,6 @@
     private int mDisabled2;
     protected BroadcastReceiver mChallengeReceiver = new ChallengeReceiver();
     private Handler mMainHandler = new Handler();
-    private final AtomicReference<Intent> mPendingConfirmCredentialIntent = new AtomicReference();
 
     /**
      */
@@ -107,9 +102,6 @@
         mActionClickLogger = clickLogger;
         mActivityIntentHelper = new ActivityIntentHelper(mContext);
         mGroupManager = groupManager;
-        // Listen to onKeyguardShowingChanged in case a managed profile needs to be unlocked
-        // once the primary profile's keyguard is no longer shown.
-        mKeyguardStateController.addCallback(this);
     }
 
     @Override
@@ -213,39 +205,12 @@
         // Clear pending remote view, as we do not want to trigger pending remote input view when
         // it's called by other code
         mPendingWorkRemoteInputView = null;
-
-        final Intent newIntent = createConfirmDeviceCredentialIntent(
-                userId, intendSender, notificationKey);
-        if (newIntent == null) {
-            Log.w(TAG, String.format("Cannot create intent to unlock user %d", userId));
-            return false;
-        }
-
-        mPendingConfirmCredentialIntent.set(newIntent);
-
-        // If the Keyguard is currently showing, starting the ConfirmDeviceCredentialActivity
-        // would cause it to pause, not letting the user actually unlock the managed profile.
-        // Instead, wait until we receive a callback indicating it is no longer showing and
-        // then start the pending intent.
-        if (mKeyguardStateController.isShowing()) {
-            // Do nothing, since the callback will get the pending intent and start it.
-            Log.w(TAG, String.format("Keyguard is showing, waiting until it's not"));
-        } else {
-            startPendingConfirmDeviceCredentialIntent();
-        }
-
-        return true;
-    }
-
-    private Intent createConfirmDeviceCredentialIntent(
-            int userId, IntentSender intendSender, String notificationKey) {
+        // Begin old BaseStatusBar.startWorkChallengeIfNecessary.
         final Intent newIntent = mKeyguardManager.createConfirmDeviceCredentialIntent(null,
                 null, userId);
-
         if (newIntent == null) {
-            return null;
+            return false;
         }
-
         final Intent callBackIntent = new Intent(NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION);
         callBackIntent.putExtra(Intent.EXTRA_INTENT, intendSender);
         callBackIntent.putExtra(Intent.EXTRA_INDEX, notificationKey);
@@ -261,40 +226,14 @@
         newIntent.putExtra(
                 Intent.EXTRA_INTENT,
                 callBackPendingIntent.getIntentSender());
-
-        return newIntent;
-    }
-
-    private void startPendingConfirmDeviceCredentialIntent() {
-        final Intent pendingIntent = mPendingConfirmCredentialIntent.getAndSet(null);
-        if (pendingIntent == null) {
-            return;
-        }
-
         try {
-            if (mKeyguardStateController.isShowing()) {
-                Log.w(TAG, "Keyguard is showing while starting confirm device credential intent.");
-            }
-            ActivityManager.getService().startConfirmDeviceCredentialIntent(pendingIntent,
+            ActivityManager.getService().startConfirmDeviceCredentialIntent(newIntent,
                     null /*options*/);
         } catch (RemoteException ex) {
             // ignore
         }
-    }
-
-    @Override
-    public void onKeyguardShowingChanged() {
-        if (mKeyguardStateController.isShowing()) {
-            // In order to avoid jarring UX where/ the managed profile challenge is shown and
-            // immediately dismissed, do not attempt to start the confirm device credential
-            // activity if the keyguard is still showing.
-            if (mPendingConfirmCredentialIntent.get() != null) {
-                Log.w(TAG, "There's a pending unlock intent but keyguard is still showing, abort.");
-            }
-            return;
-        }
-
-        startPendingConfirmDeviceCredentialIntent();
+        return true;
+        // End old BaseStatusBar.startWorkChallengeIfNecessary.
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/MeasurementCache.kt b/packages/SystemUI/src/com/android/systemui/util/animation/MeasurementCache.kt
deleted file mode 100644
index 2be698b..0000000
--- a/packages/SystemUI/src/com/android/systemui/util/animation/MeasurementCache.kt
+++ /dev/null
@@ -1,87 +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
- */
-
-package com.android.systemui.util.animation
-
-/**
- * A class responsible for caching view Measurements which guarantees that we always obtain a value
- */
-class GuaranteedMeasurementCache constructor(
-    private val baseCache : MeasurementCache,
-    private val inputMapper: (MeasurementInput) -> MeasurementInput,
-    private val measurementProvider: (MeasurementInput) -> MeasurementOutput?
-) : MeasurementCache {
-
-    override fun obtainMeasurement(input: MeasurementInput) : MeasurementOutput {
-        val mappedInput = inputMapper.invoke(input)
-        if (!baseCache.contains(mappedInput)) {
-            var measurement = measurementProvider.invoke(mappedInput)
-            if (measurement != null) {
-                // Only cache measurings that actually have a size
-                baseCache.putMeasurement(mappedInput, measurement)
-            } else {
-                measurement = MeasurementOutput(0, 0)
-            }
-            return measurement
-        } else {
-            return baseCache.obtainMeasurement(mappedInput)
-        }
-    }
-
-    override fun contains(input: MeasurementInput): Boolean {
-        return baseCache.contains(inputMapper.invoke(input))
-    }
-
-    override fun putMeasurement(input: MeasurementInput, output: MeasurementOutput) {
-        if (output.measuredWidth == 0 || output.measuredHeight == 0) {
-            // Only cache measurings that actually have a size
-            return;
-        }
-        val remappedInput = inputMapper.invoke(input)
-        baseCache.putMeasurement(remappedInput, output)
-    }
-}
-
-/**
- * A base implementation class responsible for caching view Measurements
- */
-class BaseMeasurementCache : MeasurementCache {
-    private val dataCache: MutableMap<MeasurementInput, MeasurementOutput> = mutableMapOf()
-
-    override fun obtainMeasurement(input: MeasurementInput) : MeasurementOutput {
-        val measurementOutput = dataCache[input]
-        if (measurementOutput == null) {
-            return MeasurementOutput(0, 0)
-        } else {
-            return measurementOutput
-        }
-    }
-
-    override fun contains(input: MeasurementInput) : Boolean {
-        return dataCache[input] != null
-    }
-
-    override fun putMeasurement(input: MeasurementInput, output: MeasurementOutput) {
-        dataCache[input] = output
-    }
-}
-
-interface MeasurementCache {
-    fun obtainMeasurement(input: MeasurementInput) : MeasurementOutput
-    fun contains(input: MeasurementInput) : Boolean
-    fun putMeasurement(input: MeasurementInput, output: MeasurementOutput)
-}
-
diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/MeasurementInput.kt b/packages/SystemUI/src/com/android/systemui/util/animation/MeasurementInput.kt
new file mode 100644
index 0000000..c7edd51
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/animation/MeasurementInput.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.animation
+
+import android.view.View
+
+/**
+ * The output of a view measurement
+ */
+data class MeasurementOutput(
+    var measuredWidth: Int,
+    var measuredHeight: Int
+)
+
+/**
+ * The data object holding a basic view measurement input
+ */
+data class MeasurementInput(
+    var widthMeasureSpec: Int,
+    var heightMeasureSpec: Int
+) {
+    val width: Int
+        get() {
+            return View.MeasureSpec.getSize(widthMeasureSpec)
+        }
+    val height: Int
+        get() {
+            return View.MeasureSpec.getSize(heightMeasureSpec)
+        }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt
new file mode 100644
index 0000000..701ff5e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt
@@ -0,0 +1,294 @@
+/*
+ * 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.animation
+
+import android.content.Context
+import android.graphics.Rect
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewTreeObserver
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet
+import com.android.systemui.statusbar.CrossFadeHelper
+
+/**
+ * A view that handles displaying of children and transitions of them in an optimized way,
+ * minimizing the number of measure passes, while allowing for maximum flexibility
+ * and interruptibility.
+ */
+class TransitionLayout @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0
+) : ConstraintLayout(context, attrs, defStyleAttr) {
+
+    private val originalGoneChildrenSet: MutableSet<Int> = mutableSetOf()
+    private var measureAsConstraint: Boolean = false
+    private var currentState: TransitionViewState = TransitionViewState()
+    private var updateScheduled = false
+
+    /**
+     * The measured state of this view which is the one we will lay ourselves out with. This
+     * may differ from the currentState if there is an external animation or transition running.
+     * This state will not be used to measure the widgets, where the current state is preferred.
+     */
+    var measureState: TransitionViewState = TransitionViewState()
+    private val preDrawApplicator = object : ViewTreeObserver.OnPreDrawListener {
+        override fun onPreDraw(): Boolean {
+            updateScheduled = false
+            viewTreeObserver.removeOnPreDrawListener(this)
+            applyCurrentState()
+            return true
+        }
+    }
+
+    override fun onFinishInflate() {
+        super.onFinishInflate()
+        val childCount = childCount
+        for (i in 0 until childCount) {
+            val child = getChildAt(i)
+            if (child.id == View.NO_ID) {
+                child.id = i
+            }
+            if (child.visibility == GONE) {
+                originalGoneChildrenSet.add(child.id)
+            }
+        }
+    }
+
+    /**
+     * Apply the current state to the view and its widgets
+     */
+    private fun applyCurrentState() {
+        val childCount = childCount
+        for (i in 0 until childCount) {
+            val child = getChildAt(i)
+            val widgetState = currentState.widgetStates.get(child.id) ?: continue
+            if (child.measuredWidth != widgetState.measureWidth ||
+                    child.measuredHeight != widgetState.measureHeight) {
+                val measureWidthSpec = MeasureSpec.makeMeasureSpec(widgetState.measureWidth,
+                        MeasureSpec.EXACTLY)
+                val measureHeightSpec = MeasureSpec.makeMeasureSpec(widgetState.measureHeight,
+                        MeasureSpec.EXACTLY)
+                child.measure(measureWidthSpec, measureHeightSpec)
+                child.layout(0, 0, child.measuredWidth, child.measuredHeight)
+            }
+            val left = widgetState.x.toInt()
+            val top = widgetState.y.toInt()
+            child.setLeftTopRightBottom(left, top, left + widgetState.width,
+                    top + widgetState.height)
+            child.scaleX = widgetState.scale
+            child.scaleY = widgetState.scale
+            val clipBounds = child.clipBounds ?: Rect()
+            clipBounds.set(0, 0, widgetState.width, widgetState.height)
+            child.clipBounds = clipBounds
+            CrossFadeHelper.fadeIn(child, widgetState.alpha)
+            child.visibility = if (widgetState.gone || widgetState.alpha == 0.0f) {
+                View.INVISIBLE
+            } else {
+                View.VISIBLE
+            }
+        }
+        updateBounds()
+    }
+
+    private fun applyCurrentStateOnPredraw() {
+        if (!updateScheduled) {
+            updateScheduled = true
+            viewTreeObserver.addOnPreDrawListener(preDrawApplicator)
+        }
+    }
+
+    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+        if (measureAsConstraint) {
+            super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+        } else {
+            for (i in 0 until childCount) {
+                val child = getChildAt(i)
+                val widgetState = currentState.widgetStates.get(child.id) ?: continue
+                val measureWidthSpec = MeasureSpec.makeMeasureSpec(widgetState.measureWidth,
+                        MeasureSpec.EXACTLY)
+                val measureHeightSpec = MeasureSpec.makeMeasureSpec(widgetState.measureHeight,
+                        MeasureSpec.EXACTLY)
+                child.measure(measureWidthSpec, measureHeightSpec)
+            }
+            setMeasuredDimension(measureState.width, measureState.height)
+        }
+    }
+
+    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
+        if (measureAsConstraint) {
+            super.onLayout(changed, left, top, right, bottom)
+        } else {
+            val childCount = childCount
+            for (i in 0 until childCount) {
+                val child = getChildAt(i)
+                child.layout(0, 0, child.measuredWidth, child.measuredHeight)
+            }
+            // Reapply the bounds to update the background
+            applyCurrentState()
+        }
+    }
+
+    private fun updateBounds() {
+        val layoutLeft = left
+        val layoutTop = top
+        setLeftTopRightBottom(layoutLeft, layoutTop, layoutLeft + currentState.width,
+                layoutTop + currentState.height)
+    }
+
+    /**
+     * Calculates a view state for a given ConstraintSet and measurement, saving all positions
+     * of all widgets.
+     *
+     * @param input the measurement input this should be done with
+     * @param constraintSet the constraint set to apply
+     * @param resusableState the result that we can reuse to minimize memory impact
+     */
+    fun calculateViewState(
+        input: MeasurementInput,
+        constraintSet: ConstraintSet,
+        existing: TransitionViewState? = null
+    ): TransitionViewState {
+
+        val result = existing ?: TransitionViewState()
+        // Reset gone children to the original state
+        applySetToFullLayout(constraintSet)
+        val previousHeight = measuredHeight
+        val previousWidth = measuredWidth
+
+        // Let's measure outselves as a ConstraintLayout
+        measureAsConstraint = true
+        measure(input.widthMeasureSpec, input.heightMeasureSpec)
+        val layoutLeft = left
+        val layoutTop = top
+        layout(layoutLeft, layoutTop, layoutLeft + measuredWidth, layoutTop + measuredHeight)
+        measureAsConstraint = false
+        result.initFromLayout(this)
+        ensureViewsNotGone()
+
+        // Let's reset our layout to have the right size again
+        setMeasuredDimension(previousWidth, previousHeight)
+        applyCurrentStateOnPredraw()
+        return result
+    }
+
+    private fun applySetToFullLayout(constraintSet: ConstraintSet) {
+        // Let's reset our views to the initial gone state of the layout, since the constraintset
+        // might only be a subset of the views. Otherwise the gone state would be calculated
+        // wrongly later if we made this invisible in the layout (during apply we make sure they
+        // are invisible instead
+        val childCount = childCount
+        for (i in 0 until childCount) {
+            val child = getChildAt(i)
+            if (originalGoneChildrenSet.contains(child.id)) {
+                child.visibility = View.GONE
+            }
+        }
+        // Let's now apply the constraintSet to get the full state
+        constraintSet.applyTo(this)
+    }
+
+    /**
+     * Ensures that our views are never gone but invisible instead, this allows us to animate them
+     * without remeasuring.
+     */
+    private fun ensureViewsNotGone() {
+        val childCount = childCount
+        for (i in 0 until childCount) {
+            val child = getChildAt(i)
+            val widgetState = currentState.widgetStates.get(child.id)
+            child.visibility = if (widgetState?.gone != false) View.INVISIBLE else View.VISIBLE
+        }
+    }
+
+    /**
+     * Set the state that should be applied to this View
+     *
+     */
+    fun setState(state: TransitionViewState) {
+        currentState = state
+        applyCurrentState()
+    }
+}
+
+class TransitionViewState {
+    var widgetStates: MutableMap<Int, WidgetState> = mutableMapOf()
+    var width: Int = 0
+    var height: Int = 0
+    fun copy(reusedState: TransitionViewState? = null): TransitionViewState {
+        // we need a deep copy of this, so we can't use a data class
+        val copy = reusedState ?: TransitionViewState()
+        copy.width = width
+        copy.height = height
+        for (entry in widgetStates) {
+            copy.widgetStates[entry.key] = entry.value.copy()
+        }
+        return copy
+    }
+
+    fun initFromLayout(transitionLayout: TransitionLayout) {
+        val childCount = transitionLayout.childCount
+        for (i in 0 until childCount) {
+            val child = transitionLayout.getChildAt(i)
+            val widgetState = widgetStates.getOrPut(child.id, {
+                WidgetState(0.0f, 0.0f, 0, 0, 0, 0, 0.0f)
+            })
+            widgetState.initFromLayout(child)
+        }
+        width = transitionLayout.measuredWidth
+        height = transitionLayout.measuredHeight
+    }
+}
+
+data class WidgetState(
+    var x: Float = 0.0f,
+    var y: Float = 0.0f,
+    var width: Int = 0,
+    var height: Int = 0,
+    var measureWidth: Int = 0,
+    var measureHeight: Int = 0,
+    var alpha: Float = 1.0f,
+    var scale: Float = 1.0f,
+    var gone: Boolean = false
+) {
+    fun initFromLayout(view: View) {
+        gone = view.visibility == View.GONE
+        if (gone) {
+            val layoutParams = view.layoutParams as ConstraintLayout.LayoutParams
+            x = layoutParams.constraintWidget.left.toFloat()
+            y = layoutParams.constraintWidget.top.toFloat()
+            width = layoutParams.constraintWidget.width
+            height = layoutParams.constraintWidget.height
+            measureHeight = height
+            measureWidth = width
+            alpha = 0.0f
+            scale = 0.0f
+        } else {
+            x = view.left.toFloat()
+            y = view.top.toFloat()
+            width = view.width
+            height = view.height
+            measureWidth = width
+            measureHeight = height
+            gone = view.visibility == View.GONE
+            alpha = view.alpha
+            // No scale by default. Only during transitions!
+            scale = 1.0f
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt
new file mode 100644
index 0000000..9ee1410
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt
@@ -0,0 +1,233 @@
+/*
+ * 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.animation
+
+import android.animation.ValueAnimator
+import android.util.MathUtils
+import com.android.systemui.Interpolators
+
+/**
+ * The fraction after which we start fading in when going from a gone widget to a visible one
+ */
+private const val GONE_FADE_FRACTION = 0.8f
+
+/**
+ * The amont we're scaling appearing views
+ */
+private const val GONE_SCALE_AMOUNT = 0.8f
+
+/**
+ * A controller for a [TransitionLayout] which handles state transitions and keeps the transition
+ * layout up to date with the desired state.
+ */
+open class TransitionLayoutController {
+
+    /**
+     * The layout that this controller controls
+     */
+    private var transitionLayout: TransitionLayout? = null
+    private var currentState = TransitionViewState()
+    private var animationStartState: TransitionViewState? = null
+    private var state = TransitionViewState()
+    private var animator: ValueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f)
+
+    init {
+        animator.apply {
+            addUpdateListener {
+                updateStateFromAnimation()
+            }
+            interpolator = Interpolators.FAST_OUT_SLOW_IN
+        }
+    }
+
+    private fun updateStateFromAnimation() {
+        if (animationStartState == null || !animator.isRunning) {
+            return
+        }
+        val view = transitionLayout ?: return
+        getInterpolatedState(
+                startState = animationStartState!!,
+                endState = state,
+                progress = animator.animatedFraction,
+                resultState = currentState)
+        view.setState(currentState)
+    }
+
+    /**
+     * Get an interpolated state between two viewstates. This interpolates all positions for all
+     * widgets as well as it's bounds based on the given input.
+     */
+    fun getInterpolatedState(
+        startState: TransitionViewState,
+        endState: TransitionViewState,
+        progress: Float,
+        resultState: TransitionViewState
+    ) {
+        val view = transitionLayout ?: return
+        val childCount = view.childCount
+        for (i in 0 until childCount) {
+            val id = view.getChildAt(i).id
+            val resultWidgetState = resultState.widgetStates[id] ?: WidgetState()
+            val widgetStart = startState.widgetStates[id] ?: continue
+            val widgetEnd = endState.widgetStates[id] ?: continue
+            var alphaProgress = progress
+            var widthProgress = progress
+            val resultMeasureWidth: Int
+            val resultMeasureHeight: Int
+            val newScale: Float
+            val resultX: Float
+            val resultY: Float
+            if (widgetStart.gone != widgetEnd.gone) {
+                // A view is appearing or disappearing. Let's not just interpolate between them as
+                // this looks quite ugly
+                val nowGone: Boolean
+                if (widgetStart.gone) {
+
+                    // Only fade it in at the very end
+                    alphaProgress = MathUtils.map(GONE_FADE_FRACTION, 1.0f, 0.0f, 1.0f, progress)
+                    nowGone = progress < GONE_FADE_FRACTION
+
+                    // Scale it just a little, not all the way
+                    val endScale = widgetEnd.scale
+                    newScale = MathUtils.lerp(GONE_SCALE_AMOUNT * endScale, endScale, progress)
+
+                    // don't clip
+                    widthProgress = 1.0f
+
+                    // Let's directly measure it with the end state
+                    resultMeasureWidth = widgetEnd.measureWidth
+                    resultMeasureHeight = widgetEnd.measureHeight
+
+                    // Let's make sure we're centering the view in the gone view instead of having
+                    // the left at 0
+                    resultX = MathUtils.lerp(widgetStart.x - resultMeasureWidth / 2.0f,
+                            widgetEnd.x,
+                            progress)
+                    resultY = MathUtils.lerp(widgetStart.y - resultMeasureHeight / 2.0f,
+                            widgetEnd.y,
+                            progress)
+                } else {
+
+                    // Fadeout in the very beginning
+                    alphaProgress = MathUtils.map(0.0f, 1.0f - GONE_FADE_FRACTION, 0.0f, 1.0f,
+                            progress)
+                    nowGone = progress > 1.0f - GONE_FADE_FRACTION
+
+                    // Scale it just a little, not all the way
+                    val startScale = widgetStart.scale
+                    newScale = MathUtils.lerp(startScale, startScale * GONE_SCALE_AMOUNT, progress)
+
+                    // Don't clip
+                    widthProgress = 0.0f
+
+                    // Let's directly measure it with the start state
+                    resultMeasureWidth = widgetStart.measureWidth
+                    resultMeasureHeight = widgetStart.measureHeight
+
+                    // Let's make sure we're centering the view in the gone view instead of having
+                    // the left at 0
+                    resultX = MathUtils.lerp(widgetStart.x,
+                            widgetEnd.x - resultMeasureWidth / 2.0f,
+                            progress)
+                    resultY = MathUtils.lerp(widgetStart.y,
+                            widgetEnd.y - resultMeasureHeight / 2.0f,
+                            progress)
+                }
+                resultWidgetState.gone = nowGone
+            } else {
+                resultWidgetState.gone = widgetStart.gone
+                // Let's directly measure it with the end state
+                resultMeasureWidth = widgetEnd.measureWidth
+                resultMeasureHeight = widgetEnd.measureHeight
+                newScale = MathUtils.lerp(widgetStart.scale, widgetEnd.scale, progress)
+                resultX = MathUtils.lerp(widgetStart.x, widgetEnd.x, progress)
+                resultY = MathUtils.lerp(widgetStart.y, widgetEnd.y, progress)
+            }
+            resultWidgetState.apply {
+                x = resultX
+                y = resultY
+                alpha = MathUtils.lerp(widgetStart.alpha, widgetEnd.alpha, alphaProgress)
+                width = MathUtils.lerp(widgetStart.width.toFloat(), widgetEnd.width.toFloat(),
+                        widthProgress).toInt()
+                height = MathUtils.lerp(widgetStart.height.toFloat(), widgetEnd.height.toFloat(),
+                        widthProgress).toInt()
+                scale = newScale
+
+                // Let's directly measure it with the end state
+                measureWidth = resultMeasureWidth
+                measureHeight = resultMeasureHeight
+            }
+            resultState.widgetStates[id] = resultWidgetState
+        }
+        resultState.apply {
+            width = MathUtils.lerp(startState.width.toFloat(), endState.width.toFloat(),
+                    progress).toInt()
+            height = MathUtils.lerp(startState.height.toFloat(), endState.height.toFloat(),
+                    progress).toInt()
+        }
+    }
+
+    fun attach(transitionLayout: TransitionLayout) {
+        this.transitionLayout = transitionLayout
+    }
+
+    /**
+     * Set a new state to be applied to the dynamic view.
+     *
+     * @param state the state to be applied
+     * @param animate should this change be animated. If [false] the we will either apply the
+     * state immediately if no animation is running, and if one is running, we will update the end
+     * value to match the new state.
+     * @param applyImmediately should this change be applied immediately, canceling all running
+     * animations
+     */
+    fun setState(
+        state: TransitionViewState,
+        applyImmediately: Boolean,
+        animate: Boolean,
+        duration: Long = 0,
+        delay: Long = 0
+    ) {
+        val animated = animate && currentState.width != 0
+        this.state = state.copy()
+        if (applyImmediately || transitionLayout == null) {
+            animator.cancel()
+            transitionLayout?.setState(this.state)
+            currentState = state.copy(reusedState = currentState)
+        } else if (animated) {
+            animationStartState = currentState.copy()
+            animator.duration = duration
+            animator.startDelay = delay
+            animator.start()
+        } else if (!animator.isRunning) {
+            transitionLayout?.setState(this.state)
+            currentState = state.copy(reusedState = currentState)
+        }
+        // otherwise the desired state was updated and the animation will go to the new target
+    }
+
+    /**
+     * Set a new state that will be used to measure the view itself and is useful during
+     * transitions, where the state set via [setState] may differ from how the view
+     * should be measured.
+     */
+    fun setMeasureState(
+        state: TransitionViewState
+    ) {
+        transitionLayout?.measureState = state
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt b/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt
index bf94c5d..5b6444d 100644
--- a/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt
@@ -19,7 +19,9 @@
 import android.annotation.SuppressLint
 import android.content.Context
 import android.view.View
+import android.view.ViewGroup
 import android.widget.FrameLayout
+import com.android.systemui.R
 
 /**
  * A special view that is designed to host a single "unique object". The unique object is
@@ -34,8 +36,7 @@
 class UniqueObjectHostView(
     context: Context
 ) : FrameLayout(context) {
-    lateinit var measurementCache : GuaranteedMeasurementCache
-    var onMeasureListener: ((MeasurementInput) -> Unit)? = null
+    lateinit var measurementManager: MeasurementManager
 
     @SuppressLint("DrawAllocation")
     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
@@ -45,64 +46,63 @@
         val widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.getMode(widthMeasureSpec))
         val height = MeasureSpec.getSize(heightMeasureSpec) - paddingVertical
         val heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.getMode(heightMeasureSpec))
-        val measurementInput = MeasurementInputData(widthSpec, heightSpec)
-        onMeasureListener?.apply {
-            invoke(measurementInput)
-        }
+        val measurementInput = MeasurementInput(widthSpec, heightSpec)
+
+        // Let's make sure the measurementManager knows about our size, to ensure that we have
+        // a value available. This might perform a measure internally if we don't have a cached
+        // size.
+        val (cachedWidth, cachedHeight) = measurementManager.onMeasure(measurementInput)
+
         if (!isCurrentHost()) {
-            // We're not currently the host, let's get the dimension from our cache (this might
-            // perform a measuring if the cache doesn't have it yet)
+            // We're not currently the host, let's use the dimension from our cache
             // The goal here is that the view will always have a consistent measuring, regardless
             // if it's attached or not.
             // The behavior is therefore very similar to the view being persistently attached to
             // this host, which can prevent flickers. It also makes sure that we always know
             // the size of the view during transitions even if it has never been attached here
             // before.
-            val (cachedWidth, cachedHeight) = measurementCache.obtainMeasurement(measurementInput)
             setMeasuredDimension(cachedWidth + paddingHorizontal, cachedHeight + paddingVertical)
         } else {
             super.onMeasure(widthMeasureSpec, heightMeasureSpec)
             // Let's update our cache
-            val child = getChildAt(0)!!
-            val output = MeasurementOutput(child.measuredWidth, child.measuredHeight)
-            measurementCache.putMeasurement(measurementInput, output)
+            getChildAt(0)?.requiresRemeasuring = false
         }
     }
 
+    override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
+        if (child?.measuredWidth == 0 || measuredWidth == 0 || child?.requiresRemeasuring == true) {
+            super.addView(child, index, params)
+            return
+        }
+        // Suppress layouts when adding a view. The view should already be laid out with the
+        // right size when being attached to this view
+        invalidate()
+        addViewInLayout(child, index, params, true /* preventRequestLayout */)
+        val left = paddingLeft
+        val top = paddingTop
+        val paddingHorizontal = paddingStart + paddingEnd
+        val paddingVertical = paddingTop + paddingBottom
+        child!!.layout(left,
+                top,
+                left + measuredWidth - paddingHorizontal,
+                top + measuredHeight - paddingVertical)
+    }
+
     private fun isCurrentHost() = childCount != 0
-}
 
-/**
- * A basic view measurement input
- */
-interface MeasurementInput {
-    fun sameAs(input: MeasurementInput?): Boolean {
-        return equals(input)
+    interface MeasurementManager {
+        fun onMeasure(input: MeasurementInput): MeasurementOutput
     }
-    val width : Int
-        get() {
-            return View.MeasureSpec.getSize(widthMeasureSpec)
-        }
-    val height : Int
-        get() {
-            return View.MeasureSpec.getSize(heightMeasureSpec)
-        }
-    var widthMeasureSpec: Int
-    var heightMeasureSpec: Int
 }
 
 /**
- * The output of a view measurement
+ * Does this view require remeasuring currently outside of the regular measure flow?
  */
-data class MeasurementOutput(
-    val measuredWidth: Int,
-    val measuredHeight: Int
-)
-
-/**
- * The data object holding a basic view measurement input
- */
-data class MeasurementInputData(
-    override var widthMeasureSpec: Int,
-    override var heightMeasureSpec: Int
-) : MeasurementInput
+var View.requiresRemeasuring: Boolean
+    get() {
+        val required = getTag(R.id.requires_remeasuring)
+        return required?.equals(true) ?: false
+    }
+    set(value) {
+        setTag(R.id.requires_remeasuring, value)
+    }
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 7729965..7c9ea6b 100644
--- a/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java
+++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java
@@ -92,6 +92,15 @@
     }
 
     /**
+     * @deprecated Please specify @Main or @Background when injecting a Handler or use an Executor.
+     */
+    @Deprecated
+    @Provides
+    public static Handler provideHandler() {
+        return new Handler();
+    }
+
+    /**
      * Provide a Background-Thread Executor by default.
      */
     @Provides
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 96e868d..9b377ca 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
@@ -50,6 +50,7 @@
 import android.os.Handler;
 import android.os.PowerManager;
 import android.service.dreams.IDreamManager;
+import android.service.notification.NotificationListenerService;
 import android.service.notification.ZenModeConfig;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
@@ -68,6 +69,7 @@
 import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationRemoveInterceptor;
+import com.android.systemui.statusbar.RankingBuilder;
 import com.android.systemui.statusbar.SuperStatusBarViewFactory;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.NotificationEntryListener;
@@ -674,7 +676,7 @@
         mRemoveInterceptor.onNotificationRemoveRequested(
                 mRow.getEntry().getKey(), mRow.getEntry(), REASON_APP_CANCEL);
 
-        mBubbleController.expandStackAndSelectBubble(key);
+        mBubbleController.expandStackAndSelectBubble(mRow.getEntry());
 
         assertTrue(mSysUiStateBubblesExpanded);
     }
@@ -727,6 +729,9 @@
         assertTrue(mBubbleController.hasBubbles());
 
         mRow.getEntry().getSbn().getNotification().flags &= ~FLAG_BUBBLE;
+        NotificationListenerService.Ranking ranking = new RankingBuilder(
+                mRow.getEntry().getRanking()).setCanBubble(false).build();
+        mRow.getEntry().setRanking(ranking);
         mEntryListener.onPreEntryUpdated(mRow.getEntry());
 
         assertFalse(mBubbleController.hasBubbles());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java
index 66f119a..eca78ec 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java
@@ -20,11 +20,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
@@ -258,14 +255,15 @@
         assertThat(update.updatedBubble.showFlyout()).isFalse();
     }
 
-    // COLLAPSED / ADD
+    //
+    // Overflow
+    //
 
     /**
-     * Verifies that the number of bubbles is not allowed to exceed the maximum. The limit is
-     * enforced by expiring the bubble which was least recently updated (lowest timestamp).
+     * Verifies that when the bubble stack reaches its maximum, the oldest bubble is overflowed.
      */
     @Test
-    public void test_collapsed_addBubble_atMaxBubbles_overflowsOldest() {
+    public void testOverflow_add_stackAtMaxBubbles_overflowsOldest() {
         // Setup
         sendUpdatedEntryAtTime(mEntryA1, 1000);
         sendUpdatedEntryAtTime(mEntryA2, 2000);
@@ -288,8 +286,12 @@
         assertOverflowChangedTo(ImmutableList.of(mBubbleA2));
     }
 
+    /**
+     * Verifies that once the number of overflowed bubbles reaches its maximum, the oldest
+     * overflow bubble is removed.
+     */
     @Test
-    public void testOverflowBubble_maxReached_bubbleRemoved() {
+    public void testOverflow_maxReached_bubbleRemoved() {
         // Setup
         sendUpdatedEntryAtTime(mEntryA1, 1000);
         sendUpdatedEntryAtTime(mEntryA2, 2000);
@@ -308,16 +310,41 @@
     }
 
     /**
-     * Verifies that new bubbles insert to the left when collapsed, carrying along grouped bubbles.
-     * <p>
-     * Placement within the list is based on lastUpdate (post time of the notification), descending
-     * order (with most recent first).
-     *
-     * @see #test_expanded_addBubble_sortAndGrouping_newGroup()
-     * @see #test_expanded_addBubble_sortAndGrouping_existingGroup()
+     * Verifies that overflow bubbles are canceled on notif entry removal.
      */
     @Test
-    public void test_collapsed_addBubble_sortAndGrouping() {
+    public void testOverflow_notifCanceled_removesOverflowBubble() {
+        // Setup
+        sendUpdatedEntryAtTime(mEntryA1, 1000);
+        sendUpdatedEntryAtTime(mEntryA2, 2000);
+        sendUpdatedEntryAtTime(mEntryA3, 3000);
+        sendUpdatedEntryAtTime(mEntryB1, 4000);
+        sendUpdatedEntryAtTime(mEntryB2, 5000);
+        sendUpdatedEntryAtTime(mEntryB3, 6000); // [A2, A3, B1, B2, B3], overflow: [A1]
+        sendUpdatedEntryAtTime(mEntryC1, 7000); // [A3, B1, B2, B3, C1], overflow: [A2, A1]
+        mBubbleData.setListener(mListener);
+
+        // Test
+        mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_NOTIF_CANCEL);
+        verifyUpdateReceived();
+        assertOverflowChangedTo(ImmutableList.of(mBubbleA2));
+
+        // Test
+        mBubbleData.notificationEntryRemoved(mEntryA2, BubbleController.DISMISS_GROUP_CANCELLED);
+        verifyUpdateReceived();
+        assertOverflowChangedTo(ImmutableList.of());
+    }
+
+    // COLLAPSED / ADD
+
+    /**
+     * Verifies that new bubbles insert to the left when collapsed.
+     * <p>
+     * Placement within the list is based on {@link Bubble#getLastActivity()}, descending
+     * order (with most recent first).
+     */
+    @Test
+    public void test_collapsed_addBubble() {
         // Setup
         mBubbleData.setListener(mListener);
 
@@ -336,41 +363,7 @@
 
         sendUpdatedEntryAtTime(mEntryA2, 4000);
         verifyUpdateReceived();
-        assertOrderChangedTo(mBubbleA2, mBubbleA1, mBubbleB2, mBubbleB1);
-    }
-
-    /**
-     * Verifies that new bubbles insert to the left when collapsed, carrying along grouped bubbles.
-     * Additionally, any bubble which is ongoing is considered "newer" than any non-ongoing bubble.
-     * <p>
-     * Because of the ongoing bubble, the new bubble cannot be placed in the first position. This
-     * causes the 'B' group to remain last, despite having a new button added.
-     *
-     * @see #test_expanded_addBubble_sortAndGrouping_newGroup()
-     * @see #test_expanded_addBubble_sortAndGrouping_existingGroup()
-     */
-    @Test
-    public void test_collapsed_addBubble_sortAndGrouping_withOngoing() {
-        // Setup
-        mBubbleData.setListener(mListener);
-
-        // Test
-        setOngoing(mEntryA1, true);
-        sendUpdatedEntryAtTime(mEntryA1, 1000);
-        verifyUpdateReceived();
-        assertOrderNotChanged();
-
-        sendUpdatedEntryAtTime(mEntryB1, 2000);
-        verifyUpdateReceived();
-        assertOrderNotChanged();
-
-        sendUpdatedEntryAtTime(mEntryB2, 3000);
-        verifyUpdateReceived();
-        assertOrderChangedTo(mBubbleA1, mBubbleB2, mBubbleB1);
-
-        sendUpdatedEntryAtTime(mEntryA2, 4000);
-        verifyUpdateReceived();
-        assertOrderChangedTo(mBubbleA1, mBubbleA2, mBubbleB2, mBubbleB1);
+        assertOrderChangedTo(mBubbleA2, mBubbleB2, mBubbleB1, mBubbleA1);
     }
 
     /**
@@ -378,7 +371,6 @@
      * the collapsed state.
      *
      * @see #test_collapsed_updateBubble_selectionChanges()
-     * @see #test_collapsed_updateBubble_noSelectionChanges_withOngoing()
      */
     @Test
     public void test_collapsed_addBubble_selectionChanges() {
@@ -403,58 +395,27 @@
         assertSelectionChangedTo(mBubbleA2);
     }
 
-    /**
-     * Verifies that while collapsed, the selection will not change if the selected bubble is
-     * ongoing. It remains the top bubble and as such remains selected.
-     *
-     * @see #test_collapsed_addBubble_selectionChanges()
-     */
-    @Test
-    public void test_collapsed_addBubble_noSelectionChanges_withOngoing() {
-        // Setup
-        setOngoing(mEntryA1, true);
-        sendUpdatedEntryAtTime(mEntryA1, 1000);
-        assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleA1);
-        mBubbleData.setListener(mListener);
-
-        // Test
-        sendUpdatedEntryAtTime(mEntryB1, 2000);
-        verifyUpdateReceived();
-        assertSelectionNotChanged();
-
-        sendUpdatedEntryAtTime(mEntryB2, 3000);
-        verifyUpdateReceived();
-        assertSelectionNotChanged();
-
-        sendUpdatedEntryAtTime(mEntryA2, 4000);
-        verifyUpdateReceived();
-        assertSelectionNotChanged();
-
-        assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleA1); // selection unchanged
-    }
-
     // COLLAPSED / REMOVE
 
     /**
-     * Verifies that groups may reorder when bubbles are removed, while the stack is in the
-     * collapsed state.
+     * Verifies order of bubbles after a removal.
      */
     @Test
-    public void test_collapsed_removeBubble_sortAndGrouping() {
+    public void test_collapsed_removeBubble_sort() {
         // Setup
         sendUpdatedEntryAtTime(mEntryA1, 1000);
         sendUpdatedEntryAtTime(mEntryB1, 2000);
         sendUpdatedEntryAtTime(mEntryB2, 3000);
-        sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, A1, B2, B1]
+        sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, B2, B1, A1]
         mBubbleData.setListener(mListener);
 
         // Test
         mBubbleData.notificationEntryRemoved(mEntryA2, BubbleController.DISMISS_USER_GESTURE);
         verifyUpdateReceived();
+        // TODO: this should fail if things work as I expect them to?
         assertOrderChangedTo(mBubbleB2, mBubbleB1, mBubbleA1);
     }
 
-
     /**
      * Verifies that onOrderChanged is not called when a bubble is removed if the removal does not
      * cause other bubbles to change position.
@@ -465,62 +426,16 @@
         sendUpdatedEntryAtTime(mEntryA1, 1000);
         sendUpdatedEntryAtTime(mEntryB1, 2000);
         sendUpdatedEntryAtTime(mEntryB2, 3000);
-        sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, A1, B2, B1]
+        sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, B2, B1, A1]
         mBubbleData.setListener(mListener);
 
         // Test
-        mBubbleData.notificationEntryRemoved(mEntryB1, BubbleController.DISMISS_USER_GESTURE);
+        mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_USER_GESTURE);
         verifyUpdateReceived();
         assertOrderNotChanged();
     }
 
     /**
-     * Verifies that bubble ordering reverts to normal when an ongoing bubble is removed. A group
-     * which has a newer bubble may move to the front after the ongoing bubble is removed.
-     */
-    @Test
-    public void test_collapsed_removeBubble_sortAndGrouping_withOngoing() {
-        // Setup
-        setOngoing(mEntryA1, true);
-        sendUpdatedEntryAtTime(mEntryA1, 1000);
-        sendUpdatedEntryAtTime(mEntryA2, 2000);
-        sendUpdatedEntryAtTime(mEntryB1, 3000);
-        sendUpdatedEntryAtTime(mEntryB2, 4000); // [A1*, A2, B2, B1]
-        mBubbleData.setListener(mListener);
-
-        // Test
-        mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_NOTIF_CANCEL);
-        verifyUpdateReceived();
-        assertOrderChangedTo(mBubbleB2, mBubbleB1, mBubbleA2);
-    }
-
-    /**
-     * Verifies that overflow bubbles are canceled on notif entry removal.
-     */
-    @Test
-    public void test_removeOverflowBubble_forCanceledNotif() {
-        // Setup
-        sendUpdatedEntryAtTime(mEntryA1, 1000);
-        sendUpdatedEntryAtTime(mEntryA2, 2000);
-        sendUpdatedEntryAtTime(mEntryA3, 3000);
-        sendUpdatedEntryAtTime(mEntryB1, 4000);
-        sendUpdatedEntryAtTime(mEntryB2, 5000);
-        sendUpdatedEntryAtTime(mEntryB3, 6000); // [A2, A3, B1, B2, B3], overflow: [A1]
-        sendUpdatedEntryAtTime(mEntryC1, 7000); // [A3, B1, B2, B3, C1], overflow: [A2, A1]
-        mBubbleData.setListener(mListener);
-
-        // Test
-        mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_NOTIF_CANCEL);
-        verifyUpdateReceived();
-        assertOverflowChangedTo(ImmutableList.of(mBubbleA2));
-
-        // Test
-        mBubbleData.notificationEntryRemoved(mEntryA2, BubbleController.DISMISS_GROUP_CANCELLED);
-        verifyUpdateReceived();
-        assertOverflowChangedTo(ImmutableList.of());
-    }
-
-    /**
      * Verifies that when the selected bubble is removed with the stack in the collapsed state,
      * the selection moves to the next most-recently updated bubble.
      */
@@ -530,7 +445,7 @@
         sendUpdatedEntryAtTime(mEntryA1, 1000);
         sendUpdatedEntryAtTime(mEntryB1, 2000);
         sendUpdatedEntryAtTime(mEntryB2, 3000);
-        sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, A1, B2, B1]
+        sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, B2, B1, A1]
         mBubbleData.setListener(mListener);
 
         // Test
@@ -542,26 +457,26 @@
     // COLLAPSED / UPDATE
 
     /**
-     * Verifies that bubble and group ordering may change with updates while the stack is in the
+     * Verifies that bubble ordering changes with updates while the stack is in the
      * collapsed state.
      */
     @Test
-    public void test_collapsed_updateBubble_orderAndGrouping() {
+    public void test_collapsed_updateBubble() {
         // Setup
         sendUpdatedEntryAtTime(mEntryA1, 1000);
         sendUpdatedEntryAtTime(mEntryB1, 2000);
         sendUpdatedEntryAtTime(mEntryB2, 3000);
-        sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, A1, B2, B1]
+        sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, B2, B1, A1]
         mBubbleData.setListener(mListener);
 
         // Test
         sendUpdatedEntryAtTime(mEntryB1, 5000);
         verifyUpdateReceived();
-        assertOrderChangedTo(mBubbleB1, mBubbleB2, mBubbleA2, mBubbleA1);
+        assertOrderChangedTo(mBubbleB1, mBubbleA2, mBubbleB2, mBubbleA1);
 
         sendUpdatedEntryAtTime(mEntryA1, 6000);
         verifyUpdateReceived();
-        assertOrderChangedTo(mBubbleA1, mBubbleA2, mBubbleB1, mBubbleB2);
+        assertOrderChangedTo(mBubbleA1, mBubbleB1, mBubbleA2, mBubbleB2);
     }
 
     /**
@@ -573,7 +488,7 @@
         sendUpdatedEntryAtTime(mEntryA1, 1000);
         sendUpdatedEntryAtTime(mEntryB1, 2000);
         sendUpdatedEntryAtTime(mEntryB2, 3000);
-        sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, A1, B2, B1]
+        sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, B2, B1, A1]
         mBubbleData.setListener(mListener);
 
         // Test
@@ -587,26 +502,6 @@
     }
 
     /**
-     * Verifies that selection does not change in response to updates when collapsed, if the
-     * selected bubble is ongoing.
-     */
-    @Test
-    public void test_collapsed_updateBubble_noSelectionChanges_withOngoing() {
-        // Setup
-        setOngoing(mEntryA1, true);
-        sendUpdatedEntryAtTime(mEntryA1, 1000);
-        sendUpdatedEntryAtTime(mEntryB1, 2000);
-        sendUpdatedEntryAtTime(mEntryB2, 3000);
-        sendUpdatedEntryAtTime(mEntryA2, 4000); // [A1*, A2, B2, B1]
-        mBubbleData.setListener(mListener);
-
-        // Test
-        sendUpdatedEntryAtTime(mEntryB2, 5000); // [A1*, A2, B2, B1]
-        verifyUpdateReceived();
-        assertSelectionNotChanged();
-    }
-
-    /**
      * Verifies that a request to expand the stack has no effect if there are no bubbles.
      */
     @Test
@@ -618,6 +513,9 @@
         verifyZeroInteractions(mListener);
     }
 
+    /**
+     * Verifies that removing the last bubble clears the selected bubble and collapses the stack.
+     */
     @Test
     public void test_collapsed_removeLastBubble_clearsSelectedBubble() {
         // Setup
@@ -629,23 +527,22 @@
 
         // Verify the selection was cleared.
         verifyUpdateReceived();
+        assertThat(mBubbleData.isExpanded()).isFalse();
         assertSelectionCleared();
     }
 
-    // EXPANDED / ADD
+    // EXPANDED / ADD / UPDATE
 
     /**
-     * Verifies that bubbles added as part of a new group insert before existing groups while
-     * expanded.
+     * Verifies that bubbles are added at the front of the stack.
      * <p>
-     * Placement within the list is based on lastUpdate (post time of the notification), descending
+     * Placement within the list is based on {@link Bubble#getLastActivity()}, descending
      * order (with most recent first).
      *
-     * @see #test_collapsed_addBubble_sortAndGrouping()
-     * @see #test_expanded_addBubble_sortAndGrouping_existingGroup()
+     * @see #test_collapsed_addBubble()
      */
     @Test
-    public void test_expanded_addBubble_sortAndGrouping_newGroup() {
+    public void test_expanded_addBubble() {
         // Setup
         sendUpdatedEntryAtTime(mEntryA1, 1000);
         sendUpdatedEntryAtTime(mEntryA2, 2000);
@@ -656,65 +553,15 @@
         // Test
         sendUpdatedEntryAtTime(mEntryC1, 4000);
         verifyUpdateReceived();
-        assertOrderChangedTo(mBubbleB1, mBubbleC1, mBubbleA2, mBubbleA1);
+        assertOrderChangedTo(mBubbleC1, mBubbleB1, mBubbleA2, mBubbleA1);
     }
 
     /**
-     * Verifies that bubbles added as part of a new group insert before existing groups while
-     * expanded, but not before any groups with ongoing bubbles.
-     *
-     * @see #test_collapsed_addBubble_sortAndGrouping_withOngoing()
-     * @see #test_expanded_addBubble_sortAndGrouping_existingGroup()
-     */
-    @Test
-    public void test_expanded_addBubble_sortAndGrouping_newGroup_withOngoing() {
-        // Setup
-        setOngoing(mEntryA1, true);
-        sendUpdatedEntryAtTime(mEntryA1, 1000);
-        sendUpdatedEntryAtTime(mEntryA2, 2000);
-        sendUpdatedEntryAtTime(mEntryB1, 3000); // [A1*, A2, B1]
-        changeExpandedStateAtTime(true, 4000L);
-        mBubbleData.setListener(mListener);
-
-        // Test
-        sendUpdatedEntryAtTime(mEntryC1, 4000);
-        verifyUpdateReceived();
-        assertOrderChangedTo(mBubbleA1, mBubbleA2, mBubbleC1, mBubbleB1);
-    }
-
-    /**
-     * Verifies that bubbles added as part of an existing group insert to the beginning of that
-     * group. The order of groups within the list must not change while in the expanded state.
-     *
-     * @see #test_collapsed_addBubble_sortAndGrouping()
-     * @see #test_expanded_addBubble_sortAndGrouping_newGroup()
-     */
-    @Test
-    public void test_expanded_addBubble_sortAndGrouping_existingGroup() {
-        // Setup
-        sendUpdatedEntryAtTime(mEntryA1, 1000);
-        sendUpdatedEntryAtTime(mEntryA2, 2000);
-        sendUpdatedEntryAtTime(mEntryB1, 3000); // [B1, A2, A1]
-        changeExpandedStateAtTime(true, 4000L);
-        mBubbleData.setListener(mListener);
-
-        // Test
-        sendUpdatedEntryAtTime(mEntryA3, 4000);
-        verifyUpdateReceived();
-        assertOrderChangedTo(mBubbleB1, mBubbleA3, mBubbleA2, mBubbleA1);
-    }
-
-    // EXPANDED / UPDATE
-
-    /**
      * Verifies that updates to bubbles while expanded do not result in any change to sorting
-     * or grouping of bubbles or sorting of groups.
-     *
-     * @see #test_collapsed_addBubble_sortAndGrouping()
-     * @see #test_expanded_addBubble_sortAndGrouping_existingGroup()
+     * of bubbles.
      */
     @Test
-    public void test_expanded_updateBubble_sortAndGrouping_noChanges() {
+    public void test_expanded_updateBubble_noChanges() {
         // Setup
         sendUpdatedEntryAtTime(mEntryA1, 1000);
         sendUpdatedEntryAtTime(mEntryA2, 2000);
@@ -733,7 +580,6 @@
      * Verifies that updates to bubbles while expanded do not result in any change to selection.
      *
      * @see #test_collapsed_addBubble_selectionChanges()
-     * @see #test_collapsed_updateBubble_noSelectionChanges_withOngoing()
      */
     @Test
     public void test_expanded_updateBubble_noSelectionChanges() {
@@ -762,26 +608,24 @@
     // EXPANDED / REMOVE
 
     /**
-     * Verifies that removing a bubble while expanded does not result in reordering of groups
-     * or any of the remaining bubbles.
+     * Verifies that removing a bubble while expanded does not result in reordering of bubbles.
      *
-     * @see #test_collapsed_addBubble_sortAndGrouping()
-     * @see #test_expanded_addBubble_sortAndGrouping_existingGroup()
+     * @see #test_collapsed_addBubble()
      */
     @Test
-    public void test_expanded_removeBubble_sortAndGrouping() {
+    public void test_expanded_removeBubble() {
         // Setup
         sendUpdatedEntryAtTime(mEntryA1, 1000);
         sendUpdatedEntryAtTime(mEntryB1, 2000);
         sendUpdatedEntryAtTime(mEntryA2, 3000);
-        sendUpdatedEntryAtTime(mEntryB2, 4000); // [B2, B1, A2, A1]
+        sendUpdatedEntryAtTime(mEntryB2, 4000); // [B2, A2, B1, A1]
         changeExpandedStateAtTime(true, 5000L);
         mBubbleData.setListener(mListener);
 
         // Test
         mBubbleData.notificationEntryRemoved(mEntryB2, BubbleController.DISMISS_USER_GESTURE);
         verifyUpdateReceived();
-        assertOrderChangedTo(mBubbleB1, mBubbleA2, mBubbleA1);
+        assertOrderChangedTo(mBubbleA2, mBubbleB1, mBubbleA1);
     }
 
     /**
@@ -789,8 +633,7 @@
      * selected. The replacement selection is the bubble which appears at the same index as the
      * previous one, or the previous index if this was the last position.
      *
-     * @see #test_collapsed_addBubble_sortAndGrouping()
-     * @see #test_expanded_addBubble_sortAndGrouping_existingGroup()
+     * @see #test_collapsed_addBubble()
      */
     @Test
     public void test_expanded_removeBubble_selectionChanges_whenSelectedRemoved() {
@@ -800,17 +643,17 @@
         sendUpdatedEntryAtTime(mEntryA2, 3000);
         sendUpdatedEntryAtTime(mEntryB2, 4000);
         changeExpandedStateAtTime(true, 5000L);
-        mBubbleData.setSelectedBubble(mBubbleA2);  // [B2, B1, ^A2, A1]
+        mBubbleData.setSelectedBubble(mBubbleA2);  // [B2, A2^, B1, A1]
         mBubbleData.setListener(mListener);
 
         // Test
         mBubbleData.notificationEntryRemoved(mEntryA2, BubbleController.DISMISS_USER_GESTURE);
         verifyUpdateReceived();
-        assertSelectionChangedTo(mBubbleA1);
-
-        mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_USER_GESTURE);
-        verifyUpdateReceived();
         assertSelectionChangedTo(mBubbleB1);
+
+        mBubbleData.notificationEntryRemoved(mEntryB1, BubbleController.DISMISS_USER_GESTURE);
+        verifyUpdateReceived();
+        assertSelectionChangedTo(mBubbleA1);
     }
 
     @Test
@@ -838,7 +681,6 @@
      * expanded.
      * <p>
      * When the stack transitions to the collapsed state, the selected bubble is brought to the top.
-     * Bubbles within the same group should move up with it.
      * <p>
      * When the stack transitions back to the expanded state, this new order is kept as is.
      */
@@ -849,13 +691,13 @@
         sendUpdatedEntryAtTime(mEntryB1, 2000);
         sendUpdatedEntryAtTime(mEntryA2, 3000);
         sendUpdatedEntryAtTime(mEntryB2, 4000);
-        changeExpandedStateAtTime(true, 5000L); // [B2=4000, B1=2000, A2=3000, A1=1000]
-        sendUpdatedEntryAtTime(mEntryB1, 6000); // [B2=4000, B1=6000*, A2=3000, A1=1000]
+        changeExpandedStateAtTime(true, 5000L); // [B2=4000, A2=3000, B1=2000, A1=1000]
+        sendUpdatedEntryAtTime(mEntryB1, 6000); // [B2=4000, A2=3000, B1=6000, A1=1000]
         setCurrentTime(7000);
         mBubbleData.setSelectedBubble(mBubbleA2);
         mBubbleData.setListener(mListener);
         assertThat(mBubbleData.getBubbles()).isEqualTo(
-                ImmutableList.of(mBubbleB2, mBubbleB1, mBubbleA2, mBubbleA1));
+                ImmutableList.of(mBubbleB2, mBubbleA2, mBubbleB1, mBubbleA1));
 
         // Test
 
@@ -863,18 +705,17 @@
         // stack is expanded. When next collapsed, sorting will be applied and saved, just prior
         // to moving the selected bubble to the top (first).
         //
-        // In this case, the expected re-expand state will be: [A2, A1, B1, B2]
+        // In this case, the expected re-expand state will be: [A2^, B1, B2, A1]
         //
         // collapse -> selected bubble (A2) moves first.
         changeExpandedStateAtTime(false, 8000L);
         verifyUpdateReceived();
-        assertOrderChangedTo(mBubbleA2, mBubbleA1, mBubbleB1, mBubbleB2);
+        assertOrderChangedTo(mBubbleA2, mBubbleB1, mBubbleB2, mBubbleA1);
     }
 
     /**
      * When a change occurs while collapsed (any update, add, remove), the previous expanded
-     * order and grouping becomes invalidated, and the order and grouping when next expanded will
-     * remain the same as collapsed.
+     * order becomes invalidated, the stack is resorted and will reflect that when next expanded.
      */
     @Test
     public void test_expansionChanges_withUpdatesWhileCollapsed() {
@@ -883,10 +724,10 @@
         sendUpdatedEntryAtTime(mEntryB1, 2000);
         sendUpdatedEntryAtTime(mEntryA2, 3000);
         sendUpdatedEntryAtTime(mEntryB2, 4000);
-        changeExpandedStateAtTime(true, 5000L); // [B2=4000, B1=2000, A2=3000, A1=1000]
-        sendUpdatedEntryAtTime(mEntryB1, 6000); // [B2=4000, B1=*6000, A2=3000, A1=1000]
+        changeExpandedStateAtTime(true, 5000L); // [B2=4000, A2=3000,  B1=2000, A1=1000]
+        sendUpdatedEntryAtTime(mEntryB1, 6000); // [B2=4000, A2=3000,  B1=6000, A1=1000]
         setCurrentTime(7000);
-        mBubbleData.setSelectedBubble(mBubbleA2); // [B2, B1, ^A2, A1]
+        mBubbleData.setSelectedBubble(mBubbleA2); // [B2, A2^, B1, A1]
         mBubbleData.setListener(mListener);
 
         // Test
@@ -895,7 +736,7 @@
         // stack is expanded. When next collapsed, sorting will be applied and saved, just prior
         // to moving the selected bubble to the top (first).
         //
-        // In this case, the expected re-expand state will be: [B1, B2, A2*, A1]
+        // In this case, the expected re-expand state will be: [A2^, B1, B2, A1]
         //
         // That state is restored as long as no changes occur (add/remove/update) while in
         // the collapsed state.
@@ -903,11 +744,12 @@
         // collapse -> selected bubble (A2) moves first.
         changeExpandedStateAtTime(false, 8000L);
         verifyUpdateReceived();
-        assertOrderChangedTo(mBubbleA2, mBubbleA1, mBubbleB1, mBubbleB2);
+        assertOrderChangedTo(mBubbleA2, mBubbleB1, mBubbleB2, mBubbleA1);
 
         // An update occurs, which causes sorting, and this invalidates the previously saved order.
-        sendUpdatedEntryAtTime(mEntryA2, 9000);
+        sendUpdatedEntryAtTime(mEntryA1, 9000);
         verifyUpdateReceived();
+        assertOrderChangedTo(mBubbleA1, mBubbleA2, mBubbleB1, mBubbleB2);
 
         // No order changes when expanding because the new sorted order remains.
         changeExpandedStateAtTime(true, 10000L);
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 73b8760..b18d67b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java
@@ -46,6 +46,7 @@
 import android.os.Handler;
 import android.os.PowerManager;
 import android.service.dreams.IDreamManager;
+import android.service.notification.NotificationListenerService;
 import android.service.notification.ZenModeConfig;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
@@ -62,6 +63,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
+import com.android.systemui.statusbar.RankingBuilder;
 import com.android.systemui.statusbar.SuperStatusBarViewFactory;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
@@ -640,6 +642,9 @@
         assertTrue(mBubbleController.hasBubbles());
 
         mRow.getEntry().getSbn().getNotification().flags &= ~FLAG_BUBBLE;
+        NotificationListenerService.Ranking ranking = new RankingBuilder(
+                mRow.getEntry().getRanking()).setCanBubble(false).build();
+        mRow.getEntry().setRanking(ranking);
         mEntryListener.onEntryUpdated(mRow.getEntry());
 
         assertFalse(mBubbleController.hasBubbles());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogTest.java
index 8db57cd..487452b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogTest.java
@@ -65,6 +65,7 @@
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.GlobalActions;
 import com.android.systemui.plugins.GlobalActionsPanelPlugin;
+import com.android.systemui.settings.CurrentUserContextTracker;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
 import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -119,6 +120,7 @@
     @Mock GlobalActionsPanelPlugin mWalletPlugin;
     @Mock GlobalActionsPanelPlugin.PanelViewController mWalletController;
     @Mock private Handler mHandler;
+    @Mock private CurrentUserContextTracker mCurrentUserContextTracker;
 
     private TestableLooper mTestableLooper;
 
@@ -129,6 +131,7 @@
         allowTestableLooperAsMainThread();
 
         when(mRingerModeTracker.getRingerMode()).thenReturn(mRingerModeLiveData);
+        when(mCurrentUserContextTracker.getCurrentUserContext()).thenReturn(mContext);
         mGlobalActionsDialog = new GlobalActionsDialog(mContext,
                 mWindowManagerFuncs,
                 mAudioManager,
@@ -161,7 +164,8 @@
                 mUiEventLogger,
                 mRingerModeTracker,
                 mSysUiState,
-                mHandler
+                mHandler,
+                mCurrentUserContextTracker
         );
         mGlobalActionsDialog.setZeroDialogPressDelayForTesting();
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsImeTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsImeTest.java
index eb43b81..f38c722 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsImeTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsImeTest.java
@@ -18,10 +18,13 @@
 
 import static android.view.WindowInsets.Type.ime;
 
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import android.app.Activity;
 import android.os.Bundle;
+import android.os.PowerManager;
 import android.os.SystemClock;
 import android.view.View;
 import android.view.WindowInsets;
@@ -54,16 +57,15 @@
      * doesn't interfere with the IME, i.e. soft-keyboard state.
      */
     @Test
-    public void testGlobalActions_doesntStealImeControl() {
+    public void testGlobalActions_doesntStealImeControl() throws Exception {
+        turnScreenOn();
         final TestActivity activity = mActivityTestRule.launchActivity(null);
 
-        activity.waitFor(() -> activity.mHasFocus && activity.mControlsIme && activity.mImeVisible);
+        waitUntil("Ime is visible", activity::isImeVisible);
 
-        InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
-                "input keyevent --longpress POWER"
-        );
+        executeShellCommand("input keyevent --longpress POWER");
 
-        activity.waitFor(() -> !activity.mHasFocus);
+        waitUntil("activity loses focus", () -> !activity.mHasFocus);
         // Give the dialog time to animate in, and steal IME focus. Unfortunately, there's currently
         // no better way to wait for this.
         SystemClock.sleep(TimeUnit.SECONDS.toMillis(2));
@@ -76,7 +78,39 @@
         });
     }
 
-    /** Like Instrumentation.runOnMainThread(), but forwards AssertionErrors to the caller. */
+    private void turnScreenOn() throws Exception {
+        PowerManager powerManager = mContext.getSystemService(PowerManager.class);
+        assertNotNull(powerManager);
+        if (powerManager.isInteractive()) {
+            return;
+        }
+        executeShellCommand("input keyevent KEYCODE_WAKEUP");
+        waitUntil("Device not interactive", powerManager::isInteractive);
+        executeShellCommand("am wait-for-broadcast-idle");
+    }
+
+    private static void waitUntil(String message, BooleanSupplier predicate)
+            throws Exception {
+        int sleep = 125;
+        final long timeout = SystemClock.uptimeMillis() + 10_000;  // 10 second timeout
+        while (SystemClock.uptimeMillis() < timeout) {
+            if (predicate.getAsBoolean()) {
+                return; // okay
+            }
+            Thread.sleep(sleep);
+            sleep *= 5;
+            sleep = Math.min(2000, sleep);
+        }
+        fail(message);
+    }
+
+    private static void executeShellCommand(String cmd) {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(cmd);
+    }
+
+    /**
+     * Like Instrumentation.runOnMainThread(), but forwards AssertionErrors to the caller.
+     */
     private static void runAssertionOnMainThread(Runnable r) {
         AssertionError[] t = new AssertionError[1];
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
@@ -96,7 +130,6 @@
             WindowInsetsController.OnControllableInsetsChangedListener,
             View.OnApplyWindowInsetsListener {
 
-        private EditText mContent;
         boolean mHasFocus;
         boolean mControlsIme;
         boolean mImeVisible;
@@ -105,13 +138,13 @@
         protected void onCreate(@Nullable Bundle savedInstanceState) {
             super.onCreate(savedInstanceState);
 
-            mContent = new EditText(this);
-            mContent.setCursorVisible(false);  // Otherwise, main thread doesn't go idle.
-            setContentView(mContent);
-            mContent.requestFocus();
+            EditText content = new EditText(this);
+            content.setCursorVisible(false);  // Otherwise, main thread doesn't go idle.
+            setContentView(content);
+            content.requestFocus();
 
             getWindow().getDecorView().setOnApplyWindowInsetsListener(this);
-            WindowInsetsController wic = mContent.getWindowInsetsController();
+            WindowInsetsController wic = content.getWindowInsetsController();
             wic.addOnControllableInsetsChangedListener(this);
             wic.show(ime());
         }
@@ -133,16 +166,8 @@
             }
         }
 
-        void waitFor(BooleanSupplier condition) {
-            synchronized (this) {
-                while (!condition.getAsBoolean()) {
-                    try {
-                        wait(TimeUnit.SECONDS.toMillis(5));
-                    } catch (InterruptedException e) {
-                        throw new RuntimeException(e);
-                    }
-                }
-            }
+        boolean isImeVisible() {
+            return mHasFocus && mControlsIme && mImeVisible;
         }
 
         @Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
index 4d30500..b71a62c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
@@ -31,30 +31,24 @@
 import android.widget.ImageView
 import android.widget.SeekBar
 import android.widget.TextView
-
-import androidx.constraintlayout.motion.widget.MotionLayout
-import androidx.constraintlayout.motion.widget.MotionScene
-import androidx.constraintlayout.widget.ConstraintSet
 import androidx.test.filters.SmallTest
-
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.util.animation.TransitionLayout
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.time.FakeSystemClock
-
 import com.google.common.truth.Truth.assertThat
-
 import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
 import org.mockito.Mock
 import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when` as whenever
 
-import java.util.ArrayList
-
 private const val KEY = "TEST_KEY"
 private const val APP = "APP"
 private const val BG_COLOR = Color.RED
@@ -78,8 +72,8 @@
     @Mock private lateinit var activityStarter: ActivityStarter
 
     @Mock private lateinit var holder: PlayerViewHolder
-    @Mock private lateinit var motion: MotionLayout
-    private lateinit var background: TextView
+    @Mock private lateinit var view: TransitionLayout
+    @Mock private lateinit var mediaHostStatesManager: MediaHostStatesManager
     private lateinit var appIcon: ImageView
     private lateinit var appName: TextView
     private lateinit var albumView: ImageView
@@ -107,21 +101,15 @@
         bgExecutor = FakeExecutor(FakeSystemClock())
 
         activityStarter = mock(ActivityStarter::class.java)
+        mediaHostStatesManager = mock(MediaHostStatesManager::class.java)
 
-        player = MediaControlPanel(context, fgExecutor, bgExecutor, activityStarter)
+        player = MediaControlPanel(context, fgExecutor, bgExecutor, activityStarter,
+                mediaHostStatesManager)
 
         // Mock out a view holder for the player to attach to.
         holder = mock(PlayerViewHolder::class.java)
-        motion = mock(MotionLayout::class.java)
-        val trans: ArrayList<MotionScene.Transition> = ArrayList()
-        trans.add(mock(MotionScene.Transition::class.java))
-        whenever(motion.definedTransitions).thenReturn(trans)
-        val constraintSet = mock(ConstraintSet::class.java)
-        whenever(motion.getConstraintSet(R.id.expanded)).thenReturn(constraintSet)
-        whenever(motion.getConstraintSet(R.id.collapsed)).thenReturn(constraintSet)
-        whenever(holder.player).thenReturn(motion)
-        background = TextView(context)
-        whenever(holder.background).thenReturn(background)
+        view = mock(TransitionLayout::class.java)
+        whenever(holder.player).thenReturn(view)
         appIcon = ImageView(context)
         whenever(holder.appIcon).thenReturn(appIcon)
         appName = TextView(context)
@@ -205,7 +193,9 @@
         val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
                 emptyList(), PACKAGE, session.getSessionToken(), null, device)
         player.bind(state)
-        assertThat(background.getBackgroundTintList()).isEqualTo(ColorStateList.valueOf(BG_COLOR))
+        val list = ArgumentCaptor.forClass(ColorStateList::class.java)
+        verify(view).setBackgroundTintList(list.capture())
+        assertThat(list.value).isEqualTo(ColorStateList.valueOf(BG_COLOR))
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
index 7b80a6e..c0aef8a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.media
 
 import android.app.Notification
+import android.graphics.drawable.Drawable
 import android.media.MediaMetadata
 import android.media.MediaRouter2Manager
 import android.media.RoutingSessionInfo
@@ -73,6 +74,7 @@
     private lateinit var fakeExecutor: FakeExecutor
     @Mock private lateinit var listener: MediaDeviceManager.Listener
     @Mock private lateinit var device: MediaDevice
+    @Mock private lateinit var icon: Drawable
     @Mock private lateinit var route: RoutingSessionInfo
     private lateinit var session: MediaSession
     private lateinit var metadataBuilder: MediaMetadata.Builder
@@ -89,6 +91,7 @@
 
         // Configure mocks.
         whenever(device.name).thenReturn(DEVICE_NAME)
+        whenever(device.iconWithoutBackground).thenReturn(icon)
         whenever(lmmFactory.create(PACKAGE)).thenReturn(lmm)
         whenever(lmm.getCurrentConnectedDevice()).thenReturn(device)
         whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(route)
@@ -157,6 +160,7 @@
         val data = captureDeviceData(KEY)
         assertThat(data.enabled).isTrue()
         assertThat(data.name).isEqualTo(DEVICE_NAME)
+        assertThat(data.icon).isEqualTo(icon)
     }
 
     @Test
@@ -170,6 +174,7 @@
         val data = captureDeviceData(KEY)
         assertThat(data.enabled).isTrue()
         assertThat(data.name).isEqualTo(DEVICE_NAME)
+        assertThat(data.icon).isEqualTo(icon)
     }
 
     @Test
@@ -183,6 +188,7 @@
         val data = captureDeviceData(KEY)
         assertThat(data.enabled).isTrue()
         assertThat(data.name).isEqualTo(DEVICE_NAME)
+        assertThat(data.icon).isEqualTo(icon)
     }
 
     @Test
@@ -204,6 +210,7 @@
         val data = captureDeviceData(KEY)
         assertThat(data.enabled).isFalse()
         assertThat(data.name).isNull()
+        assertThat(data.icon).isNull()
     }
 
     @Test
@@ -221,6 +228,7 @@
         val data = captureDeviceData(KEY)
         assertThat(data.enabled).isFalse()
         assertThat(data.name).isNull()
+        assertThat(data.icon).isNull()
     }
 
     @Test
@@ -238,6 +246,7 @@
         val data = captureDeviceData(KEY)
         assertThat(data.enabled).isFalse()
         assertThat(data.name).isNull()
+        assertThat(data.icon).isNull()
     }
 
     fun captureCallback(): LocalMediaManager.DeviceCallback {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt
new file mode 100644
index 0000000..c9e6f55
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt
@@ -0,0 +1,145 @@
+/*
+ * 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.media
+
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.ViewGroup
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.controller.ControlsControllerImplTest.Companion.eq
+import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.statusbar.NotificationLockscreenUserManager
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.SysuiStatusBarStateController
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.animation.UniqueObjectHostView
+import org.junit.Assert.assertNotNull
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.anyLong
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
[email protected]
+class MediaHierarchyManagerTest : SysuiTestCase() {
+
+    @Mock
+    private lateinit var lockHost: MediaHost
+    @Mock
+    private lateinit var qsHost: MediaHost
+    @Mock
+    private lateinit var qqsHost: MediaHost
+    @Mock
+    private lateinit var bypassController: KeyguardBypassController
+    @Mock
+    private lateinit var mediaFrame: ViewGroup
+    @Mock
+    private lateinit var keyguardStateController: KeyguardStateController
+    @Mock
+    private lateinit var statusBarStateController: SysuiStatusBarStateController
+    @Mock
+    private lateinit var notificationLockscreenUserManager: NotificationLockscreenUserManager
+    @Mock
+    private lateinit var mediaViewManager: MediaViewManager
+    @Mock
+    private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
+    @Captor
+    private lateinit var wakefullnessObserver: ArgumentCaptor<(WakefulnessLifecycle.Observer)>
+    @JvmField
+    @Rule
+    val mockito = MockitoJUnit.rule()
+    private lateinit var mediaHiearchyManager: MediaHierarchyManager
+
+    @Before
+    fun setup() {
+        `when`(mediaViewManager.mediaFrame).thenReturn(mediaFrame)
+        mediaHiearchyManager = MediaHierarchyManager(
+                context,
+                statusBarStateController,
+                keyguardStateController,
+                bypassController,
+                mediaViewManager,
+                notificationLockscreenUserManager,
+                wakefulnessLifecycle)
+        verify(wakefulnessLifecycle).addObserver(wakefullnessObserver.capture())
+        setupHost(lockHost, MediaHierarchyManager.LOCATION_LOCKSCREEN)
+        setupHost(qsHost, MediaHierarchyManager.LOCATION_QS)
+        setupHost(qqsHost, MediaHierarchyManager.LOCATION_QQS)
+        `when`(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
+        // We'll use the viewmanager to verify a few calls below, let's reset this.
+        clearInvocations(mediaViewManager)
+
+    }
+
+    private fun setupHost(host: MediaHost, location: Int) {
+        `when`(host.location).thenReturn(location)
+        `when`(host.currentBounds).thenReturn(Rect())
+        `when`(host.hostView).thenReturn(UniqueObjectHostView(context))
+        mediaHiearchyManager.register(host)
+    }
+
+    @Test
+    fun testHostViewSetOnRegister() {
+        val host = mediaHiearchyManager.register(lockHost)
+        verify(lockHost).hostView = eq(host)
+    }
+
+    @Test
+    fun testBlockedWhenScreenTurningOff() {
+        // Let's set it onto QS:
+        mediaHiearchyManager.qsExpansion = 1.0f
+        verify(mediaViewManager).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
+                any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
+        val observer = wakefullnessObserver.value
+        assertNotNull("lifecycle observer wasn't registered", observer)
+        observer.onStartedGoingToSleep()
+        clearInvocations(mediaViewManager)
+        mediaHiearchyManager.qsExpansion = 0.0f
+        verify(mediaViewManager, times(0)).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
+                any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
+    }
+
+    @Test
+    fun testAllowedWhenNotTurningOff() {
+        // Let's set it onto QS:
+        mediaHiearchyManager.qsExpansion = 1.0f
+        verify(mediaViewManager).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
+                any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
+        val observer = wakefullnessObserver.value
+        assertNotNull("lifecycle observer wasn't registered", observer)
+        clearInvocations(mediaViewManager)
+        mediaHiearchyManager.qsExpansion = 0.0f
+        verify(mediaViewManager).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
+                any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/PlayerViewHolderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/PlayerViewHolderTest.kt
index 7678525..d6849bf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/PlayerViewHolderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/PlayerViewHolderTest.kt
@@ -57,6 +57,6 @@
     @Test
     fun backgroundIsIlluminationDrawable() {
         val holder = PlayerViewHolder.create(inflater, parent)
-        assertThat(holder.background.background as IlluminationDrawable).isNotNull()
+        assertThat(holder.player.background as IlluminationDrawable).isNotNull()
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/pip/PipBoundsHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/pip/PipBoundsHandlerTest.java
index 425bf88..f404f04 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/pip/PipBoundsHandlerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/pip/PipBoundsHandlerTest.java
@@ -19,6 +19,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
 
 import android.content.ComponentName;
 import android.graphics.Rect;
@@ -32,6 +33,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.wm.DisplayController;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -63,7 +65,8 @@
     @Before
     public void setUp() throws Exception {
         initializeMockResources();
-        mPipBoundsHandler = new PipBoundsHandler(mContext, new PipSnapAlgorithm(mContext));
+        mPipBoundsHandler = new PipBoundsHandler(mContext, new PipSnapAlgorithm(mContext),
+                mock(DisplayController.class));
         mTestComponentName1 = new ComponentName(mContext, "component1");
         mTestComponentName2 = new ComponentName(mContext, "component2");
 
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 2c4304d..d3b3399 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java
@@ -22,7 +22,6 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -37,6 +36,7 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Looper;
+import android.os.UserHandle;
 import android.testing.AndroidTestingRunner;
 
 import androidx.test.filters.SmallTest;
@@ -79,13 +79,12 @@
         when(bitmap.getConfig()).thenReturn(Bitmap.Config.HARDWARE);
         ScreenshotNotificationSmartActionsProvider smartActionsProvider = mock(
                 ScreenshotNotificationSmartActionsProvider.class);
-        when(smartActionsProvider.getActions(any(), any(), any(), any(),
-                eq(false))).thenThrow(
-                RuntimeException.class);
+        when(smartActionsProvider.getActions(any(), any(), any(), any(), any()))
+            .thenThrow(RuntimeException.class);
         CompletableFuture<List<Notification.Action>> smartActionsFuture =
                 ScreenshotSmartActions.getSmartActionsFuture(
                         "", Uri.parse("content://authority/data"), bitmap, smartActionsProvider,
-                        true, false);
+                        true, UserHandle.getUserHandleForUid(UserHandle.myUserId()));
         assertNotNull(smartActionsFuture);
         List<Notification.Action> smartActions = smartActionsFuture.get(5, TimeUnit.MILLISECONDS);
         assertEquals(Collections.emptyList(), smartActions);
@@ -125,9 +124,8 @@
         CompletableFuture<List<Notification.Action>> smartActionsFuture =
                 ScreenshotSmartActions.getSmartActionsFuture(
                         "", Uri.parse("content://autority/data"), bitmap, mSmartActionsProvider,
-                        true, true);
-        verify(mSmartActionsProvider, never()).getActions(any(), any(), any(), any(),
-                eq(false));
+                        true, UserHandle.getUserHandleForUid(UserHandle.myUserId()));
+        verify(mSmartActionsProvider, never()).getActions(any(), any(), any(), any(), any());
         assertNotNull(smartActionsFuture);
         List<Notification.Action> smartActions = smartActionsFuture.get(5, TimeUnit.MILLISECONDS);
         assertEquals(Collections.emptyList(), smartActions);
@@ -140,9 +138,8 @@
         when(bitmap.getConfig()).thenReturn(Bitmap.Config.HARDWARE);
         ScreenshotSmartActions.getSmartActionsFuture(
                 "", Uri.parse("content://autority/data"), bitmap, mSmartActionsProvider, true,
-                true);
-        verify(mSmartActionsProvider, times(1))
-                .getActions(any(), any(), any(), any(), eq(true));
+                UserHandle.getUserHandleForUid(UserHandle.myUserId()));
+        verify(mSmartActionsProvider, times(1)).getActions(any(), any(), any(), any(), any());
     }
 
     // Tests for a hardware bitmap, a completed future is returned.
@@ -157,7 +154,7 @@
         CompletableFuture<List<Notification.Action>> smartActionsFuture =
                 ScreenshotSmartActions.getSmartActionsFuture("", null, bitmap,
                         actionsProvider,
-                        true, true);
+                        true, UserHandle.getUserHandleForUid(UserHandle.myUserId()));
         assertNotNull(smartActionsFuture);
         List<Notification.Action> smartActions = smartActionsFuture.get(5, TimeUnit.MILLISECONDS);
         assertEquals(smartActions.size(), 0);
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 53a1773..acdb2c5 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
@@ -282,7 +282,7 @@
         mNotificationActivityStarter.onNotificationClicked(sbn, mBubbleNotificationRow);
 
         // Then
-        verify(mBubbleController).expandStackAndSelectBubble(eq(sbn.getKey()));
+        verify(mBubbleController).expandStackAndSelectBubble(eq(mBubbleNotificationRow.getEntry()));
 
         // This is called regardless, and simply short circuits when there is nothing to do.
         verify(mShadeController, atLeastOnce()).collapsePanel();
@@ -313,7 +313,7 @@
         mNotificationActivityStarter.onNotificationClicked(sbn, mBubbleNotificationRow);
 
         // Then
-        verify(mBubbleController).expandStackAndSelectBubble(eq(sbn.getKey()));
+        verify(mBubbleController).expandStackAndSelectBubble(mBubbleNotificationRow.getEntry());
 
         verify(mShadeController, atLeastOnce()).collapsePanel();
 
@@ -343,7 +343,7 @@
         mNotificationActivityStarter.onNotificationClicked(sbn, mBubbleNotificationRow);
 
         // Then
-        verify(mBubbleController).expandStackAndSelectBubble(eq(sbn.getKey()));
+        verify(mBubbleController).expandStackAndSelectBubble(mBubbleNotificationRow.getEntry());
 
         verify(mShadeController, atLeastOnce()).collapsePanel();
 
diff --git a/packages/Tethering/tests/integration/Android.bp b/packages/Tethering/tests/integration/Android.bp
index 3305ed0..ed69b7d 100644
--- a/packages/Tethering/tests/integration/Android.bp
+++ b/packages/Tethering/tests/integration/Android.bp
@@ -63,7 +63,6 @@
 // NetworkStackTests.
 android_test {
     name: "TetheringCoverageTests",
-    certificate: "platform",
     platform_apis: true,
     test_suites: ["device-tests", "mts"],
     test_config: "AndroidTest_Coverage.xml",
diff --git a/packages/Tethering/tests/unit/Android.bp b/packages/Tethering/tests/unit/Android.bp
index 08cfb30..e00435b 100644
--- a/packages/Tethering/tests/unit/Android.bp
+++ b/packages/Tethering/tests/unit/Android.bp
@@ -83,7 +83,7 @@
 
 android_test {
     name: "TetheringTests",
-    certificate: "platform",
+    platform_apis: true,
     test_suites: [
         "device-tests",
         "mts",
diff --git a/services/core/java/com/android/server/SystemService.java b/services/core/java/com/android/server/SystemService.java
index e5c0540..45d53a1 100644
--- a/services/core/java/com/android/server/SystemService.java
+++ b/services/core/java/com/android/server/SystemService.java
@@ -68,8 +68,7 @@
 public abstract class SystemService {
 
     /** @hide */
-    // TODO(b/133242016) STOPSHIP: change to false before R ships
-    protected static final boolean DEBUG_USER = true;
+    protected static final boolean DEBUG_USER = false;
 
     /*
      * The earliest boot phase the system send to system services on boot.
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index cca6046..ec12aeb 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -4914,8 +4914,13 @@
 
     // TODO: remove this toast after feature development is done
     void showWhileInUseDebugToastLocked(int uid, int op, int mode) {
-        for (int i = mAm.mProcessList.mLruProcesses.size() - 1; i >= 0; i--) {
-            ProcessRecord pr = mAm.mProcessList.mLruProcesses.get(i);
+        final UidRecord uidRec = mAm.mProcessList.getUidRecordLocked(uid);
+        if (uidRec == null) {
+            return;
+        }
+
+        for (int i = uidRec.procRecords.size() - 1; i >= 0; i--) {
+            ProcessRecord pr = uidRec.procRecords.valueAt(i);
             if (pr.uid != uid) {
                 continue;
             }
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 671733b..7ef527c 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -11112,6 +11112,14 @@
             }
             pw.print("    UID "); UserHandle.formatUid(pw, uidRec.uid);
             pw.print(": "); pw.println(uidRec);
+            pw.print("      curProcState="); pw.print(uidRec.mCurProcState);
+            pw.print(" curCapability=");
+            ActivityManager.printCapabilitiesFull(pw, uidRec.curCapability);
+            pw.println();
+            for (int j = uidRec.procRecords.size() - 1; j >= 0; j--) {
+                pw.print("      proc=");
+                pw.println(uidRec.procRecords.valueAt(j));
+            }
         }
         return printed;
     }
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index f7a158a..a81590c 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -324,8 +324,21 @@
         boolean success = applyOomAdjLocked(app, doingAll, now, SystemClock.elapsedRealtime());
 
         if (uidRec != null) {
-            updateAppUidRecLocked(app);
-            // If this proc state is changed, need to update its uid record here
+            // After uidRec.reset() above, for UidRecord that has multiple processes (ProcessRecord)
+            // , We need to apply all ProcessRecord into UidRecord.
+            final ArraySet<ProcessRecord> procRecords = app.uidRecord.procRecords;
+            for (int i = procRecords.size() - 1; i >= 0; i--) {
+                final ProcessRecord pr = procRecords.valueAt(i);
+                if (!pr.killedByAm && pr.thread != null) {
+                    if (pr.isolated && pr.numberOfRunningServices() <= 0
+                            && pr.isolatedEntryPoint == null) {
+                        // No op.
+                    } else {
+                        // Keeping this process, update its uid.
+                        updateAppUidRecLocked(pr);
+                    }
+                }
+            }
             if (uidRec.getCurProcState() != PROCESS_STATE_NONEXISTENT
                     && (uidRec.setProcState != uidRec.getCurProcState()
                     || uidRec.setCapability != uidRec.curCapability
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index 108fb7d..9f2a77c 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -2808,6 +2808,7 @@
                     uidRec.curCapability);
         }
         proc.uidRecord = uidRec;
+        uidRec.procRecords.add(proc);
 
         // Reset render thread tid if it was already set, so new process can set it again.
         proc.renderThreadTid = 0;
@@ -2901,6 +2902,7 @@
         }
         if (old != null && old.uidRecord != null) {
             old.uidRecord.numProcs--;
+            old.uidRecord.procRecords.remove(old);
             if (old.uidRecord.numProcs == 0) {
                 // No more processes using this uid, tell clients it is gone.
                 if (DEBUG_UID_OBSERVERS) Slog.i(TAG_UID_OBSERVERS,
diff --git a/services/core/java/com/android/server/am/UidRecord.java b/services/core/java/com/android/server/am/UidRecord.java
index acf8b2e..c84ccb2 100644
--- a/services/core/java/com/android/server/am/UidRecord.java
+++ b/services/core/java/com/android/server/am/UidRecord.java
@@ -21,6 +21,7 @@
 import android.content.pm.PackageManager;
 import android.os.SystemClock;
 import android.os.UserHandle;
+import android.util.ArraySet;
 import android.util.TimeUtils;
 import android.util.proto.ProtoOutputStream;
 import android.util.proto.ProtoUtils;
@@ -32,7 +33,7 @@
  */
 public final class UidRecord {
     final int uid;
-    private int mCurProcState;
+    int mCurProcState;
     int setProcState = ActivityManager.PROCESS_STATE_NONEXISTENT;
     int curCapability;
     int setCapability;
@@ -44,6 +45,7 @@
     boolean idle;
     boolean setIdle;
     int numProcs;
+    ArraySet<ProcessRecord> procRecords = new ArraySet<>();
 
     /**
      * Sequence number associated with the {@link #mCurProcState}. This is incremented using
diff --git a/services/core/java/com/android/server/notification/BubbleExtractor.java b/services/core/java/com/android/server/notification/BubbleExtractor.java
index d7d413c..0fa339f 100644
--- a/services/core/java/com/android/server/notification/BubbleExtractor.java
+++ b/services/core/java/com/android/server/notification/BubbleExtractor.java
@@ -75,9 +75,19 @@
                 mConfig.getBubblePreference(
                         record.getSbn().getPackageName(), record.getSbn().getUid());
         NotificationChannel recordChannel = record.getChannel();
+        boolean canPresentAsBubble = canPresentAsBubble(record)
+                && !mActivityManager.isLowRamDevice()
+                && record.isConversation()
+                && (record.getNotification().flags & FLAG_FOREGROUND_SERVICE) == 0;
 
-        if (!mConfig.bubblesEnabled() || bubblePreference == BUBBLE_PREFERENCE_NONE) {
+        if (!mConfig.bubblesEnabled()
+                || bubblePreference == BUBBLE_PREFERENCE_NONE
+                || !canPresentAsBubble) {
             record.setAllowBubble(false);
+            if (!canPresentAsBubble) {
+                // clear out bubble metadata since it can't be used
+                record.getNotification().setBubbleMetadata(null);
+            }
         } else if (recordChannel == null) {
             // the app is allowed but there's no channel to check
             record.setAllowBubble(true);
@@ -86,14 +96,15 @@
         } else if (bubblePreference == BUBBLE_PREFERENCE_SELECTED) {
             record.setAllowBubble(recordChannel.canBubble());
         }
+        if (DBG) {
+            Slog.d(TAG, "record: " + record.getKey()
+                    + " appPref: " + bubblePreference
+                    + " canBubble: " + record.canBubble()
+                    + " canPresentAsBubble: " + canPresentAsBubble
+                    + " flagRemoved: " + record.isFlagBubbleRemoved());
+        }
 
-        final boolean fulfillsPolicy = record.canBubble()
-                && record.isConversation()
-                && !mActivityManager.isLowRamDevice()
-                && (record.getNotification().flags & FLAG_FOREGROUND_SERVICE) == 0;
-        final boolean applyFlag = fulfillsPolicy
-                && canPresentAsBubble(record)
-                && !record.isFlagBubbleRemoved();
+        final boolean applyFlag = record.canBubble() && !record.isFlagBubbleRemoved();
         if (applyFlag) {
             record.getNotification().flags |= FLAG_BUBBLE;
         } else {
diff --git a/services/core/java/com/android/server/notification/ShortcutHelper.java b/services/core/java/com/android/server/notification/ShortcutHelper.java
index e79d33f..0fa522c 100644
--- a/services/core/java/com/android/server/notification/ShortcutHelper.java
+++ b/services/core/java/com/android/server/notification/ShortcutHelper.java
@@ -28,6 +28,7 @@
 import android.os.Binder;
 import android.os.Handler;
 import android.os.UserHandle;
+import android.text.TextUtils;
 import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -111,6 +112,15 @@
                     }
                     if (!foundShortcut) {
                         bubbleKeysToRemove.add(shortcutBubbles.get(shortcutId));
+                        shortcutBubbles.remove(shortcutId);
+                        if (shortcutBubbles.isEmpty()) {
+                            mActiveShortcutBubbles.remove(packageName);
+                            if (mLauncherAppsCallbackRegistered
+                                    && mActiveShortcutBubbles.isEmpty()) {
+                                mLauncherAppsService.unregisterCallback(mLauncherAppsCallback);
+                                mLauncherAppsCallbackRegistered = false;
+                            }
+                        }
                     }
                 }
             }
@@ -209,15 +219,16 @@
      * @param removedNotification true if this notification is being removed
      * @param handler handler to register the callback with
      */
-    void maybeListenForShortcutChangesForBubbles(NotificationRecord r, boolean removedNotification,
+    void maybeListenForShortcutChangesForBubbles(NotificationRecord r,
+            boolean removedNotification,
             Handler handler) {
         final String shortcutId = r.getNotification().getBubbleMetadata() != null
                 ? r.getNotification().getBubbleMetadata().getShortcutId()
                 : null;
-        if (shortcutId == null) {
-            return;
-        }
-        if (r.getNotification().isBubbleNotification() && !removedNotification) {
+        if (!removedNotification
+                && !TextUtils.isEmpty(shortcutId)
+                && r.getShortcutInfo() != null
+                && r.getShortcutInfo().getId().equals(shortcutId)) {
             // Must track shortcut based bubbles in case the shortcut is removed
             HashMap<String, String> packageBubbles = mActiveShortcutBubbles.get(
                     r.getSbn().getPackageName());
@@ -235,10 +246,21 @@
             HashMap<String, String> packageBubbles = mActiveShortcutBubbles.get(
                     r.getSbn().getPackageName());
             if (packageBubbles != null) {
-                packageBubbles.remove(shortcutId);
-            }
-            if (packageBubbles != null && packageBubbles.isEmpty()) {
-                mActiveShortcutBubbles.remove(r.getSbn().getPackageName());
+                if (!TextUtils.isEmpty(shortcutId)) {
+                    packageBubbles.remove(shortcutId);
+                } else {
+                    // Check if there was a matching entry
+                    for (String pkgShortcutId : packageBubbles.keySet()) {
+                        String entryKey = packageBubbles.get(pkgShortcutId);
+                        if (r.getKey().equals(entryKey)) {
+                            // No longer has shortcut id so remove it
+                            packageBubbles.remove(pkgShortcutId);
+                        }
+                    }
+                }
+                if (packageBubbles.isEmpty()) {
+                    mActiveShortcutBubbles.remove(r.getSbn().getPackageName());
+                }
             }
             if (mLauncherAppsCallbackRegistered && mActiveShortcutBubbles.isEmpty()) {
                 mLauncherAppsService.unregisterCallback(mLauncherAppsCallback);
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index f8278de..7ab05c4 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -105,8 +105,10 @@
 import android.os.RevocableFileDescriptor;
 import android.os.SystemProperties;
 import android.os.UserHandle;
+import android.os.incremental.IStorageHealthListener;
 import android.os.incremental.IncrementalFileStorages;
 import android.os.incremental.IncrementalManager;
+import android.os.incremental.StorageHealthCheckParams;
 import android.os.storage.StorageManager;
 import android.provider.Settings.Secure;
 import android.stats.devicepolicy.DevicePolicyEnums;
@@ -231,6 +233,10 @@
 
     private static final String SYSTEM_DATA_LOADER_PACKAGE = "android";
 
+    private static final int INCREMENTAL_STORAGE_BLOCKED_TIMEOUT_MS = 2000;
+    private static final int INCREMENTAL_STORAGE_UNHEALTHY_TIMEOUT_MS = 7000;
+    private static final int INCREMENTAL_STORAGE_UNHEALTHY_MONITORING_MS = 60000;
+
     // TODO: enforce INSTALL_ALLOW_TEST
     // TODO: enforce INSTALL_ALLOW_DOWNGRADE
 
@@ -1568,7 +1574,7 @@
         dispatchSessionFinished(error, detailMessage, null);
     }
 
-    private void onDataLoaderUnrecoverable() {
+    private void onStorageUnhealthy() {
         if (TextUtils.isEmpty(mPackageName)) {
             // The package has not been installed.
             return;
@@ -2745,7 +2751,7 @@
 
         final DataLoaderParams params = this.params.dataLoaderParams;
         final boolean manualStartAndDestroy = !isIncrementalInstallation();
-        final IDataLoaderStatusListener listener = new IDataLoaderStatusListener.Stub() {
+        final IDataLoaderStatusListener statusListener = new IDataLoaderStatusListener.Stub() {
             @Override
             public void onStatusChanged(int dataLoaderId, int status) {
                 switch (status) {
@@ -2757,7 +2763,7 @@
                 if (mDestroyed || mDataLoaderFinished) {
                     switch (status) {
                         case IDataLoaderStatusListener.DATA_LOADER_UNRECOVERABLE:
-                            onDataLoaderUnrecoverable();
+                            onStorageUnhealthy();
                             return;
                     }
                     return;
@@ -2840,9 +2846,49 @@
         };
 
         if (!manualStartAndDestroy) {
+            final StorageHealthCheckParams healthCheckParams = new StorageHealthCheckParams();
+            healthCheckParams.blockedTimeoutMs = INCREMENTAL_STORAGE_BLOCKED_TIMEOUT_MS;
+            healthCheckParams.unhealthyTimeoutMs = INCREMENTAL_STORAGE_UNHEALTHY_TIMEOUT_MS;
+            healthCheckParams.unhealthyMonitoringMs = INCREMENTAL_STORAGE_UNHEALTHY_MONITORING_MS;
+
+            final boolean systemDataLoader =
+                    params.getComponentName().getPackageName() == SYSTEM_DATA_LOADER_PACKAGE;
+            final IStorageHealthListener healthListener = new IStorageHealthListener.Stub() {
+                @Override
+                public void onHealthStatus(int storageId, int status) {
+                    if (mDestroyed || mDataLoaderFinished) {
+                        // App's installed.
+                        switch (status) {
+                            case IStorageHealthListener.HEALTH_STATUS_UNHEALTHY:
+                                onStorageUnhealthy();
+                                return;
+                        }
+                        return;
+                    }
+
+                    switch (status) {
+                        case IStorageHealthListener.HEALTH_STATUS_OK:
+                            break;
+                        case IStorageHealthListener.HEALTH_STATUS_READS_PENDING:
+                        case IStorageHealthListener.HEALTH_STATUS_BLOCKED:
+                            if (systemDataLoader) {
+                                // It's OK for ADB data loader to wait for pages.
+                                break;
+                            }
+                            // fallthrough
+                        case IStorageHealthListener.HEALTH_STATUS_UNHEALTHY:
+                            // Even ADB installation can't wait for missing pages for too long.
+                            mDataLoaderFinished = true;
+                            dispatchSessionVerificationFailure(INSTALL_FAILED_MEDIA_UNAVAILABLE,
+                                    "Image is missing pages required for installation.");
+                            break;
+                    }
+                }
+            };
+
             try {
                 mIncrementalFileStorages = IncrementalFileStorages.initialize(mContext, stageDir,
-                        params, listener, addedFiles);
+                        params, statusListener, healthCheckParams, healthListener, addedFiles);
                 return false;
             } catch (IOException e) {
                 throw new PackageManagerException(INSTALL_FAILED_MEDIA_UNAVAILABLE, e.getMessage(),
@@ -2850,8 +2896,7 @@
             }
         }
 
-        if (!dataLoaderManager.bindToDataLoader(
-                sessionId, params.getData(), listener)) {
+        if (!dataLoaderManager.bindToDataLoader(sessionId, params.getData(), statusListener)) {
             throw new PackageManagerException(INSTALL_FAILED_MEDIA_UNAVAILABLE,
                     "Failed to initialize data loader");
         }
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index fbe8100..aa7a1ad 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -369,6 +369,7 @@
 import com.android.server.policy.PermissionPolicyInternal;
 import com.android.server.security.VerityUtils;
 import com.android.server.storage.DeviceStorageMonitorInternal;
+import com.android.server.uri.UriGrantsManagerInternal;
 import com.android.server.utils.TimingsTraceAndSlog;
 import com.android.server.wm.ActivityTaskManagerInternal;
 
@@ -4406,6 +4407,11 @@
         if (getInstantAppPackageName(callingUid) != null) {
             throw new SecurityException("Instant applications don't have access to this method");
         }
+        if (!mUserManager.exists(userId)) {
+            throw new SecurityException("User doesn't exist");
+        }
+        mPermissionManager.enforceCrossUserPermission(
+                callingUid, userId, false, false, "checkPackageStartable");
         final boolean userKeyUnlocked = StorageManager.isUserKeyUnlocked(userId);
         synchronized (mLock) {
             final PackageSetting ps = mSettings.mPackages.get(packageName);
@@ -5778,9 +5784,15 @@
 
     @Override
     public ChangedPackages getChangedPackages(int sequenceNumber, int userId) {
-        if (getInstantAppPackageName(Binder.getCallingUid()) != null) {
+        final int callingUid = Binder.getCallingUid();
+        if (getInstantAppPackageName(callingUid) != null) {
             return null;
         }
+        if (!mUserManager.exists(userId)) {
+            return null;
+        }
+        mPermissionManager.enforceCrossUserPermission(
+                callingUid, userId, false, false, "getChangedPackages");
         synchronized (mLock) {
             if (sequenceNumber >= mChangedPackagesSequenceNumber) {
                 return null;
@@ -8800,9 +8812,23 @@
 
     private ProviderInfo resolveContentProviderInternal(String name, int flags, int userId) {
         if (!mUserManager.exists(userId)) return null;
-        flags = updateFlagsForComponent(flags, userId);
         final int callingUid = Binder.getCallingUid();
+        flags = updateFlagsForComponent(flags, userId);
         final ProviderInfo providerInfo = mComponentResolver.queryProvider(name, flags, userId);
+        boolean checkedGrants = false;
+        if (providerInfo != null) {
+            // Looking for cross-user grants before enforcing the typical cross-users permissions
+            if (userId != UserHandle.getUserId(callingUid)) {
+                final UriGrantsManagerInternal mUgmInternal =
+                        LocalServices.getService(UriGrantsManagerInternal.class);
+                checkedGrants =
+                        mUgmInternal.checkAuthorityGrants(callingUid, providerInfo, userId, true);
+            }
+        }
+        if (!checkedGrants) {
+            mPermissionManager.enforceCrossUserPermission(
+                    callingUid, userId, false, false, "resolveContentProvider");
+        }
         if (providerInfo == null) {
             return null;
         }
diff --git a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
index 1fec8aa..14d043c 100644
--- a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
+++ b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
@@ -206,21 +206,9 @@
      */
     private static final Set<String> PROFILE_OWNER_ORGANIZATION_OWNED_GLOBAL_RESTRICTIONS =
             Sets.newArraySet(
-                    UserManager.DISALLOW_CONFIG_DATE_TIME,
-                    UserManager.DISALLOW_CAMERA,
-                    UserManager.DISALLOW_BLUETOOTH,
-                    UserManager.DISALLOW_BLUETOOTH_SHARING,
-                    UserManager.DISALLOW_CONFIG_CELL_BROADCASTS,
-                    UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS,
-                    UserManager.DISALLOW_CONFIG_PRIVATE_DNS,
-                    UserManager.DISALLOW_CONFIG_TETHERING,
-                    UserManager.DISALLOW_DATA_ROAMING,
-                    UserManager.DISALLOW_SAFE_BOOT,
-                    UserManager.DISALLOW_SMS,
-                    UserManager.DISALLOW_USB_FILE_TRANSFER,
                     UserManager.DISALLOW_AIRPLANE_MODE,
-                    UserManager.DISALLOW_MOUNT_PHYSICAL_MEDIA,
-                    UserManager.DISALLOW_UNMUTE_MICROPHONE
+                    UserManager.DISALLOW_CONFIG_DATE_TIME,
+                    UserManager.DISALLOW_CONFIG_PRIVATE_DNS
     );
 
     /**
@@ -236,7 +224,19 @@
                     UserManager.DISALLOW_CONTENT_SUGGESTIONS,
                     UserManager.DISALLOW_DEBUGGING_FEATURES,
                     UserManager.DISALLOW_SHARE_LOCATION,
-                    UserManager.DISALLOW_OUTGOING_CALLS
+                    UserManager.DISALLOW_OUTGOING_CALLS,
+                    UserManager.DISALLOW_CAMERA,
+                    UserManager.DISALLOW_BLUETOOTH,
+                    UserManager.DISALLOW_BLUETOOTH_SHARING,
+                    UserManager.DISALLOW_CONFIG_CELL_BROADCASTS,
+                    UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS,
+                    UserManager.DISALLOW_CONFIG_TETHERING,
+                    UserManager.DISALLOW_DATA_ROAMING,
+                    UserManager.DISALLOW_SAFE_BOOT,
+                    UserManager.DISALLOW_SMS,
+                    UserManager.DISALLOW_USB_FILE_TRANSFER,
+                    UserManager.DISALLOW_MOUNT_PHYSICAL_MEDIA,
+                    UserManager.DISALLOW_UNMUTE_MICROPHONE
     );
 
     /**
diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java
index e3b1152c..323ac7b 100755
--- a/services/core/java/com/android/server/tv/TvInputManagerService.java
+++ b/services/core/java/com/android/server/tv/TvInputManagerService.java
@@ -16,6 +16,7 @@
 
 package com.android.server.tv;
 
+import static android.media.AudioManager.DEVICE_NONE;
 import static android.media.tv.TvInputManager.INPUT_STATE_CONNECTED;
 import static android.media.tv.TvInputManager.INPUT_STATE_CONNECTED_STANDBY;
 
@@ -2047,6 +2048,36 @@
             return clientPid;
         }
 
+        /**
+         * Add a hardware device in the TvInputHardwareManager for CTS testing
+         * purpose.
+         *
+         * @param device id of the adding hardware device.
+         */
+        @Override
+        public void addHardwareDevice(int deviceId) {
+            TvInputHardwareInfo info = new TvInputHardwareInfo.Builder()
+                        .deviceId(deviceId)
+                        .type(TvInputHardwareInfo.TV_INPUT_TYPE_HDMI)
+                        .audioType(DEVICE_NONE)
+                        .audioAddress("0")
+                        .hdmiPortId(0)
+                        .build();
+            mTvInputHardwareManager.onDeviceAvailable(info, null);
+            return;
+        }
+
+        /**
+         * Remove a hardware device in the TvInputHardwareManager for CTS testing
+         * purpose.
+         *
+         * @param device id of the removing hardware device.
+         */
+        @Override
+        public void removeHardwareDevice(int deviceId) {
+            mTvInputHardwareManager.onDeviceUnavailable(deviceId);
+        }
+
         private int getClientPidLocked(String sessionId)
                 throws IllegalStateException {
             if (mSessionIdToSessionStateMap.get(sessionId) == null) {
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 578bcb6..3272a5d 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -499,6 +499,8 @@
      */
     private ActivityRecord mFixedRotationLaunchingApp;
 
+    private FixedRotationAnimationController mFixedRotationAnimationController;
+
     final FixedRotationTransitionListener mFixedRotationTransitionListener =
             new FixedRotationTransitionListener();
 
@@ -1107,12 +1109,14 @@
     }
 
     void removeShellRoot(int windowType) {
-        ShellRoot root = mShellRoots.get(windowType);
-        if (root == null) {
-            return;
+        synchronized(mWmService.mGlobalLock) {
+            ShellRoot root = mShellRoots.get(windowType);
+            if (root == null) {
+                return;
+            }
+            root.clear();
+            mShellRoots.remove(windowType);
         }
-        root.clear();
-        mShellRoots.remove(windowType);
     }
 
     void setRemoteInsetsController(IDisplayWindowInsetsController controller) {
@@ -1486,6 +1490,11 @@
         return mFixedRotationLaunchingApp;
     }
 
+    @VisibleForTesting
+    @Nullable FixedRotationAnimationController getFixedRotationAnimationController() {
+        return mFixedRotationAnimationController;
+    }
+
     void setFixedRotationLaunchingAppUnchecked(@Nullable ActivityRecord r) {
         setFixedRotationLaunchingAppUnchecked(r, ROTATION_UNDEFINED);
     }
@@ -1493,8 +1502,13 @@
     void setFixedRotationLaunchingAppUnchecked(@Nullable ActivityRecord r, int rotation) {
         if (mFixedRotationLaunchingApp == null && r != null) {
             mWmService.mDisplayNotificationController.dispatchFixedRotationStarted(this, rotation);
+            if (mFixedRotationAnimationController == null) {
+                mFixedRotationAnimationController = new FixedRotationAnimationController(this);
+                mFixedRotationAnimationController.hide();
+            }
         } else if (mFixedRotationLaunchingApp != null && r == null) {
             mWmService.mDisplayNotificationController.dispatchFixedRotationFinished(this);
+            finishFixedRotationAnimationIfPossible();
         }
         mFixedRotationLaunchingApp = r;
     }
@@ -1583,6 +1597,15 @@
         }
     }
 
+    /** Re-show the previously hidden windows if all seamless rotated windows are done. */
+    void finishFixedRotationAnimationIfPossible() {
+        final FixedRotationAnimationController controller = mFixedRotationAnimationController;
+        if (controller != null && !mDisplayRotation.hasSeamlessRotatingWindow()) {
+            controller.show();
+            mFixedRotationAnimationController = null;
+        }
+    }
+
     /**
      * Update rotation of the display.
      *
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index 702df2a..96f2363 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -560,6 +560,7 @@
         }, true /* traverseTopToBottom */);
         mSeamlessRotationCount = 0;
         mRotatingSeamlessly = false;
+        mDisplayContent.finishFixedRotationAnimationIfPossible();
     }
 
     private void prepareSeamlessRotation() {
@@ -573,6 +574,10 @@
         return mRotatingSeamlessly;
     }
 
+    boolean hasSeamlessRotatingWindow() {
+        return mSeamlessRotationCount > 0;
+    }
+
     @VisibleForTesting
     boolean shouldRotateSeamlessly(int oldRotation, int newRotation, boolean forceUpdate) {
         // Display doesn't need to be frozen because application has been started in correct
@@ -646,6 +651,7 @@
                     "Performing post-rotate rotation after seamless rotation");
             // Finish seamless rotation.
             mRotatingSeamlessly = false;
+            mDisplayContent.finishFixedRotationAnimationIfPossible();
 
             updateRotationAndSendNewConfigIfChanged();
         }
diff --git a/services/core/java/com/android/server/wm/FixedRotationAnimationController.java b/services/core/java/com/android/server/wm/FixedRotationAnimationController.java
new file mode 100644
index 0000000..cc02e99
--- /dev/null
+++ b/services/core/java/com/android/server/wm/FixedRotationAnimationController.java
@@ -0,0 +1,166 @@
+/*
+ * 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.wm;
+
+import static com.android.server.wm.AnimationSpecProto.WINDOW;
+import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_FIXED_TRANSFORM;
+import static com.android.server.wm.WindowAnimationSpecProto.ANIMATION;
+
+import android.content.Context;
+import android.util.ArrayMap;
+import android.util.proto.ProtoOutputStream;
+import android.view.SurfaceControl;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Transformation;
+
+import com.android.internal.R;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/**
+ * Controller to fade out and in system ui when applying a fixed rotation transform to a window
+ * token.
+ *
+ * The system bars will be fade out when the fixed rotation transform starts and will be fade in
+ * once all surfaces have been rotated.
+ */
+public class FixedRotationAnimationController {
+
+    private final Context mContext;
+    private final WindowState mStatusBar;
+    private final WindowState mNavigationBar;
+    private final ArrayList<WindowToken> mAnimatedWindowToken = new ArrayList<>(2);
+    private final ArrayMap<WindowToken, Runnable> mDeferredFinishCallbacks = new ArrayMap<>();
+
+    public FixedRotationAnimationController(DisplayContent displayContent) {
+        mContext = displayContent.mWmService.mContext;
+        final DisplayPolicy displayPolicy = displayContent.getDisplayPolicy();
+        mStatusBar = displayPolicy.getStatusBar();
+        // Do not animate movable navigation bar (e.g. non-gesture mode).
+        mNavigationBar = !displayPolicy.navigationBarCanMove()
+                ? displayPolicy.getNavigationBar()
+                : null;
+    }
+
+    /** Applies show animation on the previously hidden window tokens. */
+    void show() {
+        for (int i = mAnimatedWindowToken.size() - 1; i >= 0; i--) {
+            final WindowToken windowToken = mAnimatedWindowToken.get(i);
+            fadeWindowToken(true /* show */, windowToken);
+        }
+    }
+
+    /** Applies hide animation on the window tokens which may be seamlessly rotated later. */
+    void hide() {
+        if (mNavigationBar != null) {
+            fadeWindowToken(false /* show */, mNavigationBar.mToken);
+        }
+        if (mStatusBar != null) {
+            fadeWindowToken(false /* show */, mStatusBar.mToken);
+        }
+    }
+
+    private void fadeWindowToken(boolean show, WindowToken windowToken) {
+        if (windowToken == null || windowToken.getParent() == null) {
+            return;
+        }
+
+        final Animation animation = AnimationUtils.loadAnimation(mContext,
+                show ? R.anim.fade_in : R.anim.fade_out);
+        final LocalAnimationAdapter.AnimationSpec windowAnimationSpec =
+                createAnimationSpec(animation);
+
+        final FixedRotationAnimationAdapter animationAdapter = new FixedRotationAnimationAdapter(
+                windowAnimationSpec, windowToken.getSurfaceAnimationRunner(), show, windowToken);
+
+        // We deferred the end of the animation when hiding the token, so we need to end it now that
+        // it's shown again.
+        final SurfaceAnimator.OnAnimationFinishedCallback finishedCallback = show ? (t, r) -> {
+            final Runnable runnable = mDeferredFinishCallbacks.remove(windowToken);
+            if (runnable != null) {
+                runnable.run();
+            }
+        } : null;
+        windowToken.startAnimation(windowToken.getPendingTransaction(), animationAdapter,
+                show /* hidden */, ANIMATION_TYPE_FIXED_TRANSFORM, finishedCallback);
+        mAnimatedWindowToken.add(windowToken);
+    }
+
+    private LocalAnimationAdapter.AnimationSpec createAnimationSpec(Animation animation) {
+        return new LocalAnimationAdapter.AnimationSpec() {
+
+            final Transformation mTransformation = new Transformation();
+
+            @Override
+            public boolean getShowWallpaper() {
+                return true;
+            }
+
+            @Override
+            public long getDuration() {
+                return animation.getDuration();
+            }
+
+            @Override
+            public void apply(SurfaceControl.Transaction t, SurfaceControl leash,
+                    long currentPlayTime) {
+                mTransformation.clear();
+                animation.getTransformation(currentPlayTime, mTransformation);
+                t.setAlpha(leash, mTransformation.getAlpha());
+            }
+
+            @Override
+            public void dump(PrintWriter pw, String prefix) {
+                pw.print(prefix);
+                pw.println(animation);
+            }
+
+            @Override
+            public void dumpDebugInner(ProtoOutputStream proto) {
+                final long token = proto.start(WINDOW);
+                proto.write(ANIMATION, animation.toString());
+                proto.end(token);
+            }
+        };
+    }
+
+    private class FixedRotationAnimationAdapter extends LocalAnimationAdapter {
+        private final boolean mShow;
+        private final WindowToken mToken;
+
+        FixedRotationAnimationAdapter(AnimationSpec windowAnimationSpec,
+                SurfaceAnimationRunner surfaceAnimationRunner, boolean show,
+                WindowToken token) {
+            super(windowAnimationSpec, surfaceAnimationRunner);
+            mShow = show;
+            mToken = token;
+        }
+
+        @Override
+        public boolean shouldDeferAnimationFinish(Runnable endDeferFinishCallback) {
+            // We defer the end of the hide animation to ensure the tokens stay hidden until
+            // we show them again.
+            if (!mShow) {
+                mDeferredFinishCallbacks.put(mToken, endDeferFinishCallback);
+                return true;
+            }
+            return false;
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/wm/ShellRoot.java b/services/core/java/com/android/server/wm/ShellRoot.java
index 99f710b..7a38bb6 100644
--- a/services/core/java/com/android/server/wm/ShellRoot.java
+++ b/services/core/java/com/android/server/wm/ShellRoot.java
@@ -156,4 +156,3 @@
         }
     }
 }
-
diff --git a/services/core/java/com/android/server/wm/SurfaceAnimator.java b/services/core/java/com/android/server/wm/SurfaceAnimator.java
index 42342a6..0143eb1 100644
--- a/services/core/java/com/android/server/wm/SurfaceAnimator.java
+++ b/services/core/java/com/android/server/wm/SurfaceAnimator.java
@@ -489,6 +489,12 @@
     static final int ANIMATION_TYPE_INSETS_CONTROL = 1 << 5;
 
     /**
+     * Animation when a fixed rotation transform is applied to a window token.
+     * @hide
+     */
+    static final int ANIMATION_TYPE_FIXED_TRANSFORM = 1 << 6;
+
+    /**
      * Bitmask to include all animation types. This is NOT an {@link AnimationType}
      * @hide
      */
@@ -505,7 +511,8 @@
             ANIMATION_TYPE_DIMMER,
             ANIMATION_TYPE_RECENTS,
             ANIMATION_TYPE_WINDOW_ANIMATION,
-            ANIMATION_TYPE_INSETS_CONTROL
+            ANIMATION_TYPE_INSETS_CONTROL,
+            ANIMATION_TYPE_FIXED_TRANSFORM
     })
     @Retention(RetentionPolicy.SOURCE)
     @interface AnimationType {}
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 58f8579..4f1893e 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -335,6 +335,8 @@
     private boolean mDragResizing;
     private boolean mDragResizingChangeReported = true;
     private int mResizeMode;
+    private boolean mResizeForBlastSyncReported;
+
     /**
      * Special mode that is intended only for the rounded corner overlay: during rotation
      * transition, we un-rotate the window token such that the window appears as it did before the
@@ -1371,11 +1373,14 @@
         // variables, because mFrameSizeChanged only tracks the width and height changing.
         updateLastFrames();
 
+        // Add a window that is using blastSync to the resizing list if it hasn't been reported
+        // already. This because the window is waiting on a finishDrawing from the client.
         if (didFrameInsetsChange
                 || winAnimator.mSurfaceResized
                 || configChanged
                 || dragResizingChanged
-                || mReportOrientationChanged) {
+                || mReportOrientationChanged
+                || requestResizeForBlastSync()) {
             ProtoLog.v(WM_DEBUG_RESIZE,
                         "Resize reasons for w=%s:  %s surfaceResized=%b configChanged=%b "
                                 + "dragResizingChanged=%b reportOrientationChanged=%b",
@@ -3484,6 +3489,7 @@
         mReportOrientationChanged = false;
         mDragResizingChangeReported = true;
         mWinAnimator.mSurfaceResized = false;
+        mResizeForBlastSyncReported = true;
         mWindowFrames.resetInsetsChanged();
 
         final Rect frame = mWindowFrames.mCompatFrame;
@@ -5503,7 +5509,9 @@
     }
 
     long getFrameNumber() {
-        return mFrameNumber;
+        // Return the frame number in which changes requested in this layout will be rendered or
+        // -1 if we do not expect the frame to be rendered.
+        return getFrameLw().isEmpty() ? -1 : mFrameNumber;
     }
 
     void setFrameNumber(long frameNumber) {
@@ -5736,6 +5744,7 @@
         if (!willSync) {
             return false;
         }
+        mResizeForBlastSyncReported = false;
 
         mLocalSyncId = mBLASTSyncEngine.startSyncSet(this);
         addChildrenToSyncSet(mLocalSyncId);
@@ -5780,4 +5789,8 @@
         mWaitingListener = null;
         return mWinAnimator.finishDrawingLocked(null);
     }
+
+    private boolean requestResizeForBlastSync() {
+        return useBLASTSync() && !mResizeForBlastSyncReported;
+    }
 }
diff --git a/services/incremental/BinderIncrementalService.cpp b/services/incremental/BinderIncrementalService.cpp
index 8476674..6018b9e 100644
--- a/services/incremental/BinderIncrementalService.cpp
+++ b/services/incremental/BinderIncrementalService.cpp
@@ -118,14 +118,18 @@
 }
 
 binder::Status BinderIncrementalService::createStorage(
-        const std::string& path, const content::pm::DataLoaderParamsParcel& params,
-        const ::android::sp<::android::content::pm::IDataLoaderStatusListener>& listener,
-        int32_t createMode, int32_t* _aidl_return) {
+        const ::std::string& path, const ::android::content::pm::DataLoaderParamsParcel& params,
+        int32_t createMode,
+        const ::android::sp<::android::content::pm::IDataLoaderStatusListener>& statusListener,
+        const ::android::os::incremental::StorageHealthCheckParams& healthCheckParams,
+        const ::android::sp<::android::os::incremental::IStorageHealthListener>& healthListener,
+        int32_t* _aidl_return) {
     *_aidl_return =
             mImpl.createStorage(path, const_cast<content::pm::DataLoaderParamsParcel&&>(params),
-                                listener,
-                                android::incremental::IncrementalService::CreateOptions(
-                                        createMode));
+                                android::incremental::IncrementalService::CreateOptions(createMode),
+                                statusListener,
+                                const_cast<StorageHealthCheckParams&&>(healthCheckParams),
+                                healthListener);
     return ok();
 }
 
diff --git a/services/incremental/BinderIncrementalService.h b/services/incremental/BinderIncrementalService.h
index 5a7d5da..af11363 100644
--- a/services/incremental/BinderIncrementalService.h
+++ b/services/incremental/BinderIncrementalService.h
@@ -41,8 +41,11 @@
     binder::Status openStorage(const std::string& path, int32_t* _aidl_return) final;
     binder::Status createStorage(
             const ::std::string& path, const ::android::content::pm::DataLoaderParamsParcel& params,
-            const ::android::sp<::android::content::pm::IDataLoaderStatusListener>& listener,
-            int32_t createMode, int32_t* _aidl_return) final;
+            int32_t createMode,
+            const ::android::sp<::android::content::pm::IDataLoaderStatusListener>& statusListener,
+            const ::android::os::incremental::StorageHealthCheckParams& healthCheckParams,
+            const ::android::sp<IStorageHealthListener>& healthListener,
+            int32_t* _aidl_return) final;
     binder::Status createLinkedStorage(const std::string& path, int32_t otherStorageId,
                                        int32_t createMode, int32_t* _aidl_return) final;
     binder::Status makeBindMount(int32_t storageId, const std::string& sourcePath,
@@ -55,8 +58,7 @@
     binder::Status makeDirectories(int32_t storageId, const std::string& path,
                                    int32_t* _aidl_return) final;
     binder::Status makeFile(int32_t storageId, const std::string& path,
-                            const ::android::os::incremental::IncrementalNewFileParams& params,
-                            int32_t* _aidl_return) final;
+                            const IncrementalNewFileParams& params, int32_t* _aidl_return) final;
     binder::Status makeFileFromRange(int32_t storageId, const std::string& targetPath,
                                      const std::string& sourcePath, int64_t start, int64_t end,
                                      int32_t* _aidl_return) final;
diff --git a/services/incremental/IncrementalService.cpp b/services/incremental/IncrementalService.cpp
index f0dca77..b03d1ea 100644
--- a/services/incremental/IncrementalService.cpp
+++ b/services/incremental/IncrementalService.cpp
@@ -410,9 +410,12 @@
     }
 }
 
-StorageId IncrementalService::createStorage(
-        std::string_view mountPoint, DataLoaderParamsParcel&& dataLoaderParams,
-        const DataLoaderStatusListener& dataLoaderStatusListener, CreateOptions options) {
+StorageId IncrementalService::createStorage(std::string_view mountPoint,
+                                            content::pm::DataLoaderParamsParcel&& dataLoaderParams,
+                                            CreateOptions options,
+                                            const DataLoaderStatusListener& statusListener,
+                                            StorageHealthCheckParams&& healthCheckParams,
+                                            const StorageHealthListener& healthListener) {
     LOG(INFO) << "createStorage: " << mountPoint << " | " << int(options);
     if (!path::isAbsolute(mountPoint)) {
         LOG(ERROR) << "path is not absolute: " << mountPoint;
@@ -545,8 +548,8 @@
     // Done here as well, all data structures are in good state.
     secondCleanupOnFailure.release();
 
-    auto dataLoaderStub =
-            prepareDataLoader(*ifs, std::move(dataLoaderParams), &dataLoaderStatusListener);
+    auto dataLoaderStub = prepareDataLoader(*ifs, std::move(dataLoaderParams), &statusListener,
+                                            std::move(healthCheckParams), &healthListener);
     CHECK(dataLoaderStub);
 
     mountIt->second = std::move(ifs);
@@ -1254,7 +1257,7 @@
         dataLoaderParams.arguments = loader.arguments();
     }
 
-    prepareDataLoader(*ifs, std::move(dataLoaderParams), nullptr);
+    prepareDataLoader(*ifs, std::move(dataLoaderParams));
     CHECK(ifs->dataLoaderStub);
 
     std::vector<std::pair<std::string, metadata::BindPoint>> bindPoints;
@@ -1338,14 +1341,18 @@
 
 IncrementalService::DataLoaderStubPtr IncrementalService::prepareDataLoader(
         IncFsMount& ifs, DataLoaderParamsParcel&& params,
-        const DataLoaderStatusListener* externalListener) {
+        const DataLoaderStatusListener* statusListener,
+        StorageHealthCheckParams&& healthCheckParams, const StorageHealthListener* healthListener) {
     std::unique_lock l(ifs.lock);
-    prepareDataLoaderLocked(ifs, std::move(params), externalListener);
+    prepareDataLoaderLocked(ifs, std::move(params), statusListener, std::move(healthCheckParams),
+                            healthListener);
     return ifs.dataLoaderStub;
 }
 
 void IncrementalService::prepareDataLoaderLocked(IncFsMount& ifs, DataLoaderParamsParcel&& params,
-                                                 const DataLoaderStatusListener* externalListener) {
+                                                 const DataLoaderStatusListener* statusListener,
+                                                 StorageHealthCheckParams&& healthCheckParams,
+                                                 const StorageHealthListener* healthListener) {
     if (ifs.dataLoaderStub) {
         LOG(INFO) << "Skipped data loader preparation because it already exists";
         return;
@@ -1360,7 +1367,8 @@
 
     ifs.dataLoaderStub =
             new DataLoaderStub(*this, ifs.mountId, std::move(params), std::move(fsControlParcel),
-                               externalListener, path::join(ifs.root, constants().mount));
+                               statusListener, std::move(healthCheckParams), healthListener,
+                               path::join(ifs.root, constants().mount));
 }
 
 template <class Duration>
@@ -1680,19 +1688,24 @@
 IncrementalService::DataLoaderStub::DataLoaderStub(IncrementalService& service, MountId id,
                                                    DataLoaderParamsParcel&& params,
                                                    FileSystemControlParcel&& control,
-                                                   const DataLoaderStatusListener* externalListener,
+                                                   const DataLoaderStatusListener* statusListener,
+                                                   StorageHealthCheckParams&& healthCheckParams,
+                                                   const StorageHealthListener* healthListener,
                                                    std::string&& healthPath)
       : mService(service),
         mId(id),
         mParams(std::move(params)),
         mControl(std::move(control)),
-        mListener(externalListener ? *externalListener : DataLoaderStatusListener()),
+        mStatusListener(statusListener ? *statusListener : DataLoaderStatusListener()),
+        mHealthListener(healthListener ? *healthListener : StorageHealthListener()),
         mHealthPath(std::move(healthPath)) {
+    // TODO(b/153874006): enable external health listener.
+    mHealthListener = {};
     healthStatusOk();
 }
 
 IncrementalService::DataLoaderStub::~DataLoaderStub() {
-    if (mId != kInvalidStorageId) {
+    if (isValid()) {
         cleanupResources();
     }
 }
@@ -1710,13 +1723,14 @@
     mStatusCondition.wait_until(lock, now + 60s, [this] {
         return mCurrentStatus == IDataLoaderStatusListener::DATA_LOADER_DESTROYED;
     });
-    mListener = {};
+    mStatusListener = {};
+    mHealthListener = {};
     mId = kInvalidStorageId;
 }
 
 sp<content::pm::IDataLoader> IncrementalService::DataLoaderStub::getDataLoader() {
     sp<IDataLoader> dataloader;
-    auto status = mService.mDataLoaderManager->getDataLoader(mId, &dataloader);
+    auto status = mService.mDataLoaderManager->getDataLoader(id(), &dataloader);
     if (!status.isOk()) {
         LOG(ERROR) << "Failed to get dataloader: " << status.toString8();
         return {};
@@ -1752,15 +1766,15 @@
     auto oldStatus = mTargetStatus;
     mTargetStatus = status;
     mTargetStatusTs = Clock::now();
-    LOG(DEBUG) << "Target status update for DataLoader " << mId << ": " << oldStatus << " -> "
+    LOG(DEBUG) << "Target status update for DataLoader " << id() << ": " << oldStatus << " -> "
                << status << " (current " << mCurrentStatus << ")";
 }
 
 bool IncrementalService::DataLoaderStub::bind() {
     bool result = false;
-    auto status = mService.mDataLoaderManager->bindToDataLoader(mId, mParams, this, &result);
+    auto status = mService.mDataLoaderManager->bindToDataLoader(id(), mParams, this, &result);
     if (!status.isOk() || !result) {
-        LOG(ERROR) << "Failed to bind a data loader for mount " << mId;
+        LOG(ERROR) << "Failed to bind a data loader for mount " << id();
         return false;
     }
     return true;
@@ -1771,9 +1785,9 @@
     if (!dataloader) {
         return false;
     }
-    auto status = dataloader->create(mId, mParams, mControl, this);
+    auto status = dataloader->create(id(), mParams, mControl, this);
     if (!status.isOk()) {
-        LOG(ERROR) << "Failed to start DataLoader: " << status.toString8();
+        LOG(ERROR) << "Failed to create DataLoader: " << status.toString8();
         return false;
     }
     return true;
@@ -1784,7 +1798,7 @@
     if (!dataloader) {
         return false;
     }
-    auto status = dataloader->start(mId);
+    auto status = dataloader->start(id());
     if (!status.isOk()) {
         LOG(ERROR) << "Failed to start DataLoader: " << status.toString8();
         return false;
@@ -1793,7 +1807,7 @@
 }
 
 bool IncrementalService::DataLoaderStub::destroy() {
-    return mService.mDataLoaderManager->unbindFromDataLoader(mId).isOk();
+    return mService.mDataLoaderManager->unbindFromDataLoader(id()).isOk();
 }
 
 bool IncrementalService::DataLoaderStub::fsmStep() {
@@ -1852,8 +1866,8 @@
         return binder::Status::
                 fromServiceSpecificError(-EINVAL, "onStatusChange came to invalid DataLoaderStub");
     }
-    if (mId != mountId) {
-        LOG(ERROR) << "Mount ID mismatch: expected " << mId << ", but got: " << mountId;
+    if (id() != mountId) {
+        LOG(ERROR) << "Mount ID mismatch: expected " << id() << ", but got: " << mountId;
         return binder::Status::fromServiceSpecificError(-EPERM, "Mount ID mismatch.");
     }
 
@@ -1869,7 +1883,7 @@
         mCurrentStatus = newStatus;
         targetStatus = mTargetStatus;
 
-        listener = mListener;
+        listener = mStatusListener;
 
         if (mCurrentStatus == IDataLoaderStatusListener::DATA_LOADER_UNAVAILABLE) {
             // For unavailable, unbind from DataLoader to ensure proper re-commit.
@@ -1877,7 +1891,7 @@
         }
     }
 
-    LOG(DEBUG) << "Current status update for DataLoader " << mId << ": " << oldStatus << " -> "
+    LOG(DEBUG) << "Current status update for DataLoader " << id() << ": " << oldStatus << " -> "
                << newStatus << " (target " << targetStatus << ")";
 
     if (listener) {
diff --git a/services/incremental/IncrementalService.h b/services/incremental/IncrementalService.h
index f3fde2a..bde4ef6 100644
--- a/services/incremental/IncrementalService.h
+++ b/services/incremental/IncrementalService.h
@@ -21,6 +21,8 @@
 #include <android/content/pm/FileSystemControlParcel.h>
 #include <android/content/pm/IDataLoaderStatusListener.h>
 #include <android/os/incremental/BnIncrementalServiceConnector.h>
+#include <android/os/incremental/BnStorageHealthListener.h>
+#include <android/os/incremental/StorageHealthCheckParams.h>
 #include <binder/IAppOpsCallback.h>
 #include <utils/String16.h>
 #include <utils/StrongPointer.h>
@@ -56,10 +58,15 @@
 using Clock = std::chrono::steady_clock;
 using TimePoint = std::chrono::time_point<Clock>;
 using Seconds = std::chrono::seconds;
+using BootClockTsUs = uint64_t;
 
 using IDataLoaderStatusListener = ::android::content::pm::IDataLoaderStatusListener;
 using DataLoaderStatusListener = ::android::sp<IDataLoaderStatusListener>;
 
+using StorageHealthCheckParams = ::android::os::incremental::StorageHealthCheckParams;
+using IStorageHealthListener = ::android::os::incremental::IStorageHealthListener;
+using StorageHealthListener = ::android::sp<IStorageHealthListener>;
+
 class IncrementalService final {
 public:
     explicit IncrementalService(ServiceManagerWrapper&& sm, std::string_view rootDir);
@@ -72,6 +79,8 @@
     static constexpr StorageId kInvalidStorageId = -1;
     static constexpr StorageId kMaxStorageId = std::numeric_limits<int>::max();
 
+    static constexpr BootClockTsUs kMaxBootClockTsUs = std::numeric_limits<BootClockTsUs>::max();
+
     enum CreateOptions {
         TemporaryBind = 1,
         PermanentBind = 2,
@@ -97,8 +106,9 @@
 
     StorageId createStorage(std::string_view mountPoint,
                             content::pm::DataLoaderParamsParcel&& dataLoaderParams,
-                            const DataLoaderStatusListener& dataLoaderStatusListener,
-                            CreateOptions options = CreateOptions::Default);
+                            CreateOptions options, const DataLoaderStatusListener& statusListener,
+                            StorageHealthCheckParams&& healthCheckParams,
+                            const StorageHealthListener& healthListener);
     StorageId createLinkedStorage(std::string_view mountPoint, StorageId linkedStorage,
                                   CreateOptions options = CreateOptions::Default);
     StorageId openStorage(std::string_view path);
@@ -161,7 +171,9 @@
         DataLoaderStub(IncrementalService& service, MountId id,
                        content::pm::DataLoaderParamsParcel&& params,
                        content::pm::FileSystemControlParcel&& control,
-                       const DataLoaderStatusListener* externalListener, std::string&& healthPath);
+                       const DataLoaderStatusListener* statusListener,
+                       StorageHealthCheckParams&& healthCheckParams,
+                       const StorageHealthListener* healthListener, std::string&& healthPath);
         ~DataLoaderStub();
         // Cleans up the internal state and invalidates DataLoaderStub. Any subsequent calls will
         // result in an error.
@@ -212,7 +224,8 @@
         MountId mId = kInvalidStorageId;
         content::pm::DataLoaderParamsParcel mParams;
         content::pm::FileSystemControlParcel mControl;
-        DataLoaderStatusListener mListener;
+        DataLoaderStatusListener mStatusListener;
+        StorageHealthListener mHealthListener;
 
         std::condition_variable mStatusCondition;
         int mCurrentStatus = content::pm::IDataLoaderStatusListener::DATA_LOADER_DESTROYED;
@@ -291,9 +304,13 @@
 
     DataLoaderStubPtr prepareDataLoader(IncFsMount& ifs,
                                         content::pm::DataLoaderParamsParcel&& params,
-                                        const DataLoaderStatusListener* externalListener = nullptr);
+                                        const DataLoaderStatusListener* statusListener = nullptr,
+                                        StorageHealthCheckParams&& healthCheckParams = {},
+                                        const StorageHealthListener* healthListener = nullptr);
     void prepareDataLoaderLocked(IncFsMount& ifs, content::pm::DataLoaderParamsParcel&& params,
-                                 const DataLoaderStatusListener* externalListener = nullptr);
+                                 const DataLoaderStatusListener* statusListener = nullptr,
+                                 StorageHealthCheckParams&& healthCheckParams = {},
+                                 const StorageHealthListener* healthListener = nullptr);
 
     BindPathMap::const_iterator findStorageLocked(std::string_view path) const;
     StorageId findStorageId(std::string_view path) const;
diff --git a/services/incremental/ServiceWrappers.cpp b/services/incremental/ServiceWrappers.cpp
index 08fb486..a76aa62 100644
--- a/services/incremental/ServiceWrappers.cpp
+++ b/services/incremental/ServiceWrappers.cpp
@@ -175,6 +175,10 @@
     ErrorCode writeBlocks(std::span<const incfs::DataBlock> blocks) const final {
         return incfs::writeBlocks({blocks.data(), size_t(blocks.size())});
     }
+    WaitResult waitForPendingReads(const Control& control, std::chrono::milliseconds timeout,
+                                   std::vector<incfs::ReadInfo>* pendingReadsBuffer) const final {
+        return incfs::waitForPendingReads(control, timeout, pendingReadsBuffer);
+    }
 };
 
 RealServiceManager::RealServiceManager(sp<IServiceManager> serviceManager, JNIEnv* env)
diff --git a/services/incremental/ServiceWrappers.h b/services/incremental/ServiceWrappers.h
index abbf2f4..a935ab9 100644
--- a/services/incremental/ServiceWrappers.h
+++ b/services/incremental/ServiceWrappers.h
@@ -69,6 +69,7 @@
     using Control = incfs::Control;
     using FileId = incfs::FileId;
     using ErrorCode = incfs::ErrorCode;
+    using WaitResult = incfs::WaitResult;
 
     using ExistingMountCallback =
             std::function<void(std::string_view root, std::string_view backingDir,
@@ -90,6 +91,9 @@
     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(std::span<const incfs::DataBlock> blocks) const = 0;
+    virtual WaitResult waitForPendingReads(
+            const Control& control, std::chrono::milliseconds timeout,
+            std::vector<incfs::ReadInfo>* pendingReadsBuffer) const = 0;
 };
 
 class AppOpsManagerWrapper {
diff --git a/services/incremental/test/IncrementalServiceTest.cpp b/services/incremental/test/IncrementalServiceTest.cpp
index 2e4625c..2948b6a 100644
--- a/services/incremental/test/IncrementalServiceTest.cpp
+++ b/services/incremental/test/IncrementalServiceTest.cpp
@@ -284,6 +284,9 @@
     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(std::span<const DataBlock> blocks));
+    MOCK_CONST_METHOD3(waitForPendingReads,
+                       WaitResult(const Control& control, std::chrono::milliseconds timeout,
+                                  std::vector<incfs::ReadInfo>* pendingReadsBuffer));
 
     MockIncFs() { ON_CALL(*this, listExistingMounts(_)).WillByDefault(Return()); }
 
@@ -292,12 +295,23 @@
     void openMountSuccess() {
         ON_CALL(*this, openMount(_)).WillByDefault(Invoke(this, &MockIncFs::openMountForHealth));
     }
+    void waitForPendingReadsSuccess() {
+        ON_CALL(*this, waitForPendingReads(_, _, _))
+                .WillByDefault(Invoke(this, &MockIncFs::waitForPendingReadsForHealth));
+    }
 
     static constexpr auto kPendingReadsFd = 42;
     Control openMountForHealth(std::string_view) {
         return UniqueControl(IncFs_CreateControl(-1, kPendingReadsFd, -1));
     }
 
+    WaitResult waitForPendingReadsForHealth(
+            const Control& control, std::chrono::milliseconds timeout,
+            std::vector<incfs::ReadInfo>* pendingReadsBuffer) const {
+        pendingReadsBuffer->push_back({.bootClockTsUs = 0});
+        return android::incfs::WaitResult::HaveData;
+    }
+
     RawMetadata getMountInfoMetadata(const Control& control, std::string_view path) {
         metadata::Mount m;
         m.mutable_storage()->set_id(100);
@@ -499,9 +513,9 @@
     mVold->mountIncFsFails();
     EXPECT_CALL(*mDataLoaderManager, bindToDataLoader(_, _, _, _)).Times(0);
     TemporaryDir tempDir;
-    int storageId =
-            mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel), {},
-                                               IncrementalService::CreateOptions::CreateNew);
+    int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
+                                                       IncrementalService::CreateOptions::CreateNew,
+                                                       {}, {}, {});
     ASSERT_LT(storageId, 0);
 }
 
@@ -510,9 +524,9 @@
     EXPECT_CALL(*mDataLoaderManager, bindToDataLoader(_, _, _, _)).Times(0);
     EXPECT_CALL(*mDataLoaderManager, unbindFromDataLoader(_)).Times(0);
     TemporaryDir tempDir;
-    int storageId =
-            mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel), {},
-                                               IncrementalService::CreateOptions::CreateNew);
+    int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
+                                                       IncrementalService::CreateOptions::CreateNew,
+                                                       {}, {}, {});
     ASSERT_LT(storageId, 0);
 }
 
@@ -523,9 +537,9 @@
     EXPECT_CALL(*mDataLoaderManager, unbindFromDataLoader(_)).Times(0);
     EXPECT_CALL(*mVold, unmountIncFs(_));
     TemporaryDir tempDir;
-    int storageId =
-            mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel), {},
-                                               IncrementalService::CreateOptions::CreateNew);
+    int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
+                                                       IncrementalService::CreateOptions::CreateNew,
+                                                       {}, {}, {});
     ASSERT_LT(storageId, 0);
 }
 
@@ -537,9 +551,9 @@
     EXPECT_CALL(*mDataLoaderManager, unbindFromDataLoader(_)).Times(0);
     EXPECT_CALL(*mVold, unmountIncFs(_));
     TemporaryDir tempDir;
-    int storageId =
-            mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel), {},
-                                               IncrementalService::CreateOptions::CreateNew);
+    int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
+                                                       IncrementalService::CreateOptions::CreateNew,
+                                                       {}, {}, {});
     ASSERT_LT(storageId, 0);
 }
 
@@ -555,9 +569,9 @@
     EXPECT_CALL(*mDataLoader, destroy(_)).Times(0);
     EXPECT_CALL(*mVold, unmountIncFs(_)).Times(2);
     TemporaryDir tempDir;
-    int storageId =
-            mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel), {},
-                                               IncrementalService::CreateOptions::CreateNew);
+    int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
+                                                       IncrementalService::CreateOptions::CreateNew,
+                                                       {}, {}, {});
     ASSERT_LT(storageId, 0);
 }
 
@@ -574,9 +588,9 @@
     EXPECT_CALL(*mDataLoader, destroy(_)).Times(1);
     EXPECT_CALL(*mVold, unmountIncFs(_)).Times(2);
     TemporaryDir tempDir;
-    int storageId =
-            mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel), {},
-                                               IncrementalService::CreateOptions::CreateNew);
+    int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
+                                                       IncrementalService::CreateOptions::CreateNew,
+                                                       {}, {}, {});
     ASSERT_GE(storageId, 0);
     mIncrementalService->deleteStorage(storageId);
 }
@@ -594,9 +608,9 @@
     EXPECT_CALL(*mDataLoader, destroy(_)).Times(1);
     EXPECT_CALL(*mVold, unmountIncFs(_)).Times(2);
     TemporaryDir tempDir;
-    int storageId =
-            mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel), {},
-                                               IncrementalService::CreateOptions::CreateNew);
+    int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
+                                                       IncrementalService::CreateOptions::CreateNew,
+                                                       {}, {}, {});
     ASSERT_GE(storageId, 0);
     // Simulated crash/other connection breakage.
     mDataLoaderManager->setDataLoaderStatusDestroyed();
@@ -616,9 +630,9 @@
     EXPECT_CALL(*mDataLoader, destroy(_)).Times(1);
     EXPECT_CALL(*mVold, unmountIncFs(_)).Times(2);
     TemporaryDir tempDir;
-    int storageId =
-            mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel), {},
-                                               IncrementalService::CreateOptions::CreateNew);
+    int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
+                                                       IncrementalService::CreateOptions::CreateNew,
+                                                       {}, {}, {});
     ASSERT_GE(storageId, 0);
     mDataLoaderManager->setDataLoaderStatusCreated();
     ASSERT_TRUE(mIncrementalService->startLoading(storageId));
@@ -639,9 +653,9 @@
     EXPECT_CALL(*mDataLoader, destroy(_)).Times(1);
     EXPECT_CALL(*mVold, unmountIncFs(_)).Times(2);
     TemporaryDir tempDir;
-    int storageId =
-            mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel), {},
-                                               IncrementalService::CreateOptions::CreateNew);
+    int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
+                                                       IncrementalService::CreateOptions::CreateNew,
+                                                       {}, {}, {});
     ASSERT_GE(storageId, 0);
     ASSERT_TRUE(mIncrementalService->startLoading(storageId));
     mDataLoaderManager->setDataLoaderStatusCreated();
@@ -661,9 +675,9 @@
     EXPECT_CALL(*mDataLoader, destroy(_)).Times(1);
     EXPECT_CALL(*mVold, unmountIncFs(_)).Times(2);
     TemporaryDir tempDir;
-    int storageId =
-            mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel), {},
-                                               IncrementalService::CreateOptions::CreateNew);
+    int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
+                                                       IncrementalService::CreateOptions::CreateNew,
+                                                       {}, {}, {});
     ASSERT_GE(storageId, 0);
     mDataLoaderManager->setDataLoaderStatusUnavailable();
 }
@@ -672,6 +686,7 @@
     mVold->mountIncFsSuccess();
     mIncFs->makeFileSuccess();
     mIncFs->openMountSuccess();
+    mIncFs->waitForPendingReadsSuccess();
     mVold->bindMountSuccess();
     mDataLoader->initializeCreateOkNoStatus();
     mDataLoaderManager->bindToDataLoaderSuccess();
@@ -685,9 +700,9 @@
     EXPECT_CALL(*mLooper, addFd(MockIncFs::kPendingReadsFd, _, _, _, _)).Times(1);
     EXPECT_CALL(*mLooper, removeFd(MockIncFs::kPendingReadsFd)).Times(1);
     TemporaryDir tempDir;
-    int storageId =
-            mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel), {},
-                                               IncrementalService::CreateOptions::CreateNew);
+    int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
+                                                       IncrementalService::CreateOptions::CreateNew,
+                                                       {}, {}, {});
     ASSERT_GE(storageId, 0);
     mDataLoaderManager->setDataLoaderStatusUnavailable();
     ASSERT_NE(nullptr, mLooper->mCallback);
@@ -712,9 +727,9 @@
     // Not expecting callback removal.
     EXPECT_CALL(*mAppOpsManager, stopWatchingMode(_)).Times(0);
     TemporaryDir tempDir;
-    int storageId =
-            mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel), {},
-                                               IncrementalService::CreateOptions::CreateNew);
+    int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
+                                                       IncrementalService::CreateOptions::CreateNew,
+                                                       {}, {}, {});
     ASSERT_GE(storageId, 0);
     ASSERT_GE(mDataLoader->setStorageParams(true), 0);
 }
@@ -739,9 +754,9 @@
     // After callback is called, disable read logs and remove callback.
     EXPECT_CALL(*mAppOpsManager, stopWatchingMode(_)).Times(1);
     TemporaryDir tempDir;
-    int storageId =
-            mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel), {},
-                                               IncrementalService::CreateOptions::CreateNew);
+    int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
+                                                       IncrementalService::CreateOptions::CreateNew,
+                                                       {}, {}, {});
     ASSERT_GE(storageId, 0);
     ASSERT_GE(mDataLoader->setStorageParams(true), 0);
     ASSERT_NE(nullptr, mAppOpsManager->mStoredCallback.get());
@@ -762,9 +777,9 @@
     EXPECT_CALL(*mAppOpsManager, startWatchingMode(_, _, _)).Times(0);
     EXPECT_CALL(*mAppOpsManager, stopWatchingMode(_)).Times(0);
     TemporaryDir tempDir;
-    int storageId =
-            mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel), {},
-                                               IncrementalService::CreateOptions::CreateNew);
+    int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
+                                                       IncrementalService::CreateOptions::CreateNew,
+                                                       {}, {}, {});
     ASSERT_GE(storageId, 0);
     ASSERT_LT(mDataLoader->setStorageParams(true), 0);
 }
@@ -785,9 +800,9 @@
     EXPECT_CALL(*mAppOpsManager, startWatchingMode(_, _, _)).Times(0);
     EXPECT_CALL(*mAppOpsManager, stopWatchingMode(_)).Times(0);
     TemporaryDir tempDir;
-    int storageId =
-            mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel), {},
-                                               IncrementalService::CreateOptions::CreateNew);
+    int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
+                                                       IncrementalService::CreateOptions::CreateNew,
+                                                       {}, {}, {});
     ASSERT_GE(storageId, 0);
     ASSERT_LT(mDataLoader->setStorageParams(true), 0);
 }
@@ -799,9 +814,9 @@
     mDataLoaderManager->bindToDataLoaderSuccess();
     mDataLoaderManager->getDataLoaderSuccess();
     TemporaryDir tempDir;
-    int storageId =
-            mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel), {},
-                                               IncrementalService::CreateOptions::CreateNew);
+    int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
+                                                       IncrementalService::CreateOptions::CreateNew,
+                                                       {}, {}, {});
     std::string dir_path("test");
 
     // Expecting incfs to call makeDir on a path like:
@@ -823,9 +838,9 @@
     mDataLoaderManager->bindToDataLoaderSuccess();
     mDataLoaderManager->getDataLoaderSuccess();
     TemporaryDir tempDir;
-    int storageId =
-            mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel), {},
-                                               IncrementalService::CreateOptions::CreateNew);
+    int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
+                                                       IncrementalService::CreateOptions::CreateNew,
+                                                       {}, {}, {});
     auto first = "first"sv;
     auto second = "second"sv;
     auto third = "third"sv;
diff --git a/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java
index d338b58..ade01dc 100644
--- a/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java
@@ -19,7 +19,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
-import static com.android.server.blob.BlobStoreConfig.SESSION_EXPIRY_TIMEOUT_MILLIS;
+import static com.android.server.blob.BlobStoreConfig.DeviceConfigProperties.SESSION_EXPIRY_TIMEOUT_MS;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -93,6 +93,7 @@
         doReturn(true).when(mBlobsDir).exists();
         doReturn(new File[0]).when(mBlobsDir).listFiles();
         doReturn(true).when(() -> BlobStoreConfig.hasLeaseWaitTimeElapsed(anyLong()));
+        doCallRealMethod().when(() -> BlobStoreConfig.hasSessionExpired(anyLong()));
 
         mContext = InstrumentationRegistry.getTargetContext();
         mHandler = new TestHandler(Looper.getMainLooper());
@@ -236,7 +237,7 @@
     public void testHandleIdleMaintenance_deleteStaleSessions() throws Exception {
         // Setup sessions
         final File sessionFile1 = mock(File.class);
-        doReturn(System.currentTimeMillis() - SESSION_EXPIRY_TIMEOUT_MILLIS + 1000)
+        doReturn(System.currentTimeMillis() - SESSION_EXPIRY_TIMEOUT_MS + 1000)
                 .when(sessionFile1).lastModified();
         final long sessionId1 = 342;
         final BlobHandle blobHandle1 = BlobHandle.createWithSha256("digest1".getBytes(),
@@ -256,7 +257,7 @@
         mUserSessions.append(sessionId2, session2);
 
         final File sessionFile3 = mock(File.class);
-        doReturn(System.currentTimeMillis() - SESSION_EXPIRY_TIMEOUT_MILLIS - 2000)
+        doReturn(System.currentTimeMillis() - SESSION_EXPIRY_TIMEOUT_MS - 2000)
                 .when(sessionFile3).lastModified();
         final long sessionId3 = 9484;
         final BlobHandle blobHandle3 = BlobHandle.createWithSha256("digest3".getBytes(),
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 724048b..4a77489 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -1997,19 +1997,9 @@
 
     private static final Set<String> PROFILE_OWNER_ORGANIZATION_OWNED_GLOBAL_RESTRICTIONS =
             Sets.newSet(
-                    UserManager.DISALLOW_CONFIG_DATE_TIME,
-                    UserManager.DISALLOW_BLUETOOTH_SHARING,
-                    UserManager.DISALLOW_CONFIG_CELL_BROADCASTS,
-                    UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS,
-                    UserManager.DISALLOW_CONFIG_PRIVATE_DNS,
-                    UserManager.DISALLOW_CONFIG_TETHERING,
-                    UserManager.DISALLOW_DATA_ROAMING,
-                    UserManager.DISALLOW_SAFE_BOOT,
-                    UserManager.DISALLOW_SMS,
-                    UserManager.DISALLOW_USB_FILE_TRANSFER,
                     UserManager.DISALLOW_AIRPLANE_MODE,
-                    UserManager.DISALLOW_MOUNT_PHYSICAL_MEDIA,
-                    UserManager.DISALLOW_UNMUTE_MICROPHONE
+                    UserManager.DISALLOW_CONFIG_DATE_TIME,
+                    UserManager.DISALLOW_CONFIG_PRIVATE_DNS
             );
 
     private static final Set<String> PROFILE_OWNER_ORGANIZATION_OWNED_LOCAL_RESTRICTIONS =
@@ -2021,7 +2011,17 @@
                     UserManager.DISALLOW_CONTENT_SUGGESTIONS,
                     UserManager.DISALLOW_DEBUGGING_FEATURES,
                     UserManager.DISALLOW_SHARE_LOCATION,
-                    UserManager.DISALLOW_OUTGOING_CALLS
+                    UserManager.DISALLOW_OUTGOING_CALLS,
+                    UserManager.DISALLOW_BLUETOOTH_SHARING,
+                    UserManager.DISALLOW_CONFIG_CELL_BROADCASTS,
+                    UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS,
+                    UserManager.DISALLOW_CONFIG_TETHERING,
+                    UserManager.DISALLOW_DATA_ROAMING,
+                    UserManager.DISALLOW_SAFE_BOOT,
+                    UserManager.DISALLOW_SMS,
+                    UserManager.DISALLOW_USB_FILE_TRANSFER,
+                    UserManager.DISALLOW_MOUNT_PHYSICAL_MEDIA,
+                    UserManager.DISALLOW_UNMUTE_MICROPHONE
             );
 
     public void testSetUserRestriction_asPoOfOrgOwnedDevice() throws Exception {
@@ -2045,8 +2045,9 @@
         parentDpm.setCameraDisabled(admin1, true);
         verify(getServices().userManagerInternal).setDevicePolicyUserRestrictions(
                 eq(CALLER_USER_HANDLE),
-                MockUtils.checkUserRestrictions(UserManager.DISALLOW_CAMERA),
-                MockUtils.checkUserRestrictions(CALLER_USER_HANDLE),
+                MockUtils.checkUserRestrictions(),
+                MockUtils.checkUserRestrictions(UserHandle.USER_SYSTEM,
+                        UserManager.DISALLOW_CAMERA),
                 eq(false));
         DpmTestUtils.assertRestrictions(
                 DpmTestUtils.newRestrictions(UserManager.DISALLOW_CAMERA),
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/BubbleExtractorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/BubbleExtractorTest.java
index 38b71b7..13457f0 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/BubbleExtractorTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/BubbleExtractorTest.java
@@ -26,6 +26,8 @@
 import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
 
 import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
 import static junit.framework.Assert.assertTrue;
 
 import static org.mockito.ArgumentMatchers.any;
@@ -166,6 +168,8 @@
         setUpBubblesEnabled(true /* feature */,
                 BUBBLE_PREFERENCE_ALL /* app */,
                 ALLOW_BUBBLE_OFF /* channel */);
+        when(mActivityManager.isLowRamDevice()).thenReturn(false);
+        setUpShortcutBubble(true /* isValid */);
         NotificationRecord r = getNotificationRecord(true /* bubble */);
         mBubbleExtractor.process(r);
 
@@ -178,6 +182,8 @@
         setUpBubblesEnabled(true /* feature */,
                 BUBBLE_PREFERENCE_ALL /* app */,
                 DEFAULT_ALLOW_BUBBLE /* channel */);
+        when(mActivityManager.isLowRamDevice()).thenReturn(false);
+        setUpShortcutBubble(true /* isValid */);
         NotificationRecord r = getNotificationRecord(true /* bubble */);
 
         mBubbleExtractor.process(r);
@@ -190,6 +196,8 @@
         setUpBubblesEnabled(true /* feature */,
                 BUBBLE_PREFERENCE_ALL /* app */,
                 ALLOW_BUBBLE_ON /* channel */);
+        when(mActivityManager.isLowRamDevice()).thenReturn(false);
+        setUpShortcutBubble(true /* isValid */);
         NotificationRecord r = getNotificationRecord(true /* bubble */);
 
         mBubbleExtractor.process(r);
@@ -202,6 +210,8 @@
         setUpBubblesEnabled(false /* feature */,
                 BUBBLE_PREFERENCE_ALL /* app */,
                 ALLOW_BUBBLE_ON /* channel */);
+        when(mActivityManager.isLowRamDevice()).thenReturn(false);
+        setUpShortcutBubble(true /* isValid */);
         NotificationRecord r = getNotificationRecord(true /* bubble */);
 
         mBubbleExtractor.process(r);
@@ -215,6 +225,8 @@
         setUpBubblesEnabled(true /* feature */,
                 BUBBLE_PREFERENCE_NONE /* app */,
                 ALLOW_BUBBLE_ON /* channel */);
+        when(mActivityManager.isLowRamDevice()).thenReturn(false);
+        setUpShortcutBubble(true /* isValid */);
         NotificationRecord r = getNotificationRecord(true /* bubble */);
 
         mBubbleExtractor.process(r);
@@ -228,6 +240,8 @@
         setUpBubblesEnabled(true /* feature */,
                 BUBBLE_PREFERENCE_NONE /* app */,
                 DEFAULT_ALLOW_BUBBLE /* channel */);
+        when(mActivityManager.isLowRamDevice()).thenReturn(false);
+        setUpShortcutBubble(true /* isValid */);
         NotificationRecord r = getNotificationRecord(true /* bubble */);
 
         mBubbleExtractor.process(r);
@@ -241,6 +255,8 @@
         setUpBubblesEnabled(true /* feature */,
                 BUBBLE_PREFERENCE_SELECTED /* app */,
                 DEFAULT_ALLOW_BUBBLE /* channel */);
+        when(mActivityManager.isLowRamDevice()).thenReturn(false);
+        setUpShortcutBubble(true /* isValid */);
         NotificationRecord r = getNotificationRecord(true /* bubble */);
 
         mBubbleExtractor.process(r);
@@ -254,6 +270,8 @@
         setUpBubblesEnabled(true /* feature */,
                 BUBBLE_PREFERENCE_SELECTED /* app */,
                 ALLOW_BUBBLE_OFF /* channel */);
+        when(mActivityManager.isLowRamDevice()).thenReturn(false);
+        setUpShortcutBubble(true /* isValid */);
         NotificationRecord r = getNotificationRecord(true /* bubble */);
 
         mBubbleExtractor.process(r);
@@ -267,6 +285,9 @@
         setUpBubblesEnabled(true /* feature */,
                 BUBBLE_PREFERENCE_SELECTED /* app */,
                 ALLOW_BUBBLE_ON /* channel */);
+        when(mActivityManager.isLowRamDevice()).thenReturn(false);
+        setUpShortcutBubble(true /* isValid */);
+
         NotificationRecord r = getNotificationRecord(true /* bubble */);
 
         mBubbleExtractor.process(r);
@@ -279,6 +300,9 @@
         setUpBubblesEnabled(false /* feature */,
                 BUBBLE_PREFERENCE_SELECTED /* app */,
                 ALLOW_BUBBLE_ON /* channel */);
+        when(mActivityManager.isLowRamDevice()).thenReturn(false);
+        setUpShortcutBubble(true /* isValid */);
+
         NotificationRecord r = getNotificationRecord(true /* bubble */);
 
         mBubbleExtractor.process(r);
@@ -305,6 +329,7 @@
         mBubbleExtractor.process(r);
 
         assertTrue(r.canBubble());
+        assertNotNull(r.getNotification().getBubbleMetadata());
         assertFalse(r.getNotification().isBubbleNotification());
     }
 
@@ -320,6 +345,7 @@
         mBubbleExtractor.process(r);
 
         assertTrue(r.canBubble());
+        assertNotNull(r.getNotification().getBubbleMetadata());
         assertTrue(r.getNotification().isBubbleNotification());
     }
 
@@ -335,6 +361,7 @@
         mBubbleExtractor.process(r);
 
         assertTrue(r.canBubble());
+        assertNotNull(r.getNotification().getBubbleMetadata());
         assertTrue(r.getNotification().isBubbleNotification());
     }
 
@@ -350,7 +377,8 @@
         r.setShortcutInfo(null);
         mBubbleExtractor.process(r);
 
-        assertTrue(r.canBubble());
+        assertFalse(r.canBubble());
+        assertNull(r.getNotification().getBubbleMetadata());
         assertFalse(r.getNotification().isBubbleNotification());
     }
 
@@ -366,7 +394,8 @@
         r.setShortcutInfo(null);
         mBubbleExtractor.process(r);
 
-        assertTrue(r.canBubble());
+        assertFalse(r.canBubble());
+        assertNull(r.getNotification().getBubbleMetadata());
         assertFalse(r.getNotification().isBubbleNotification());
     }
 
@@ -381,7 +410,8 @@
         NotificationRecord r = getNotificationRecord(true /* bubble */);
         mBubbleExtractor.process(r);
 
-        assertTrue(r.canBubble());
+        assertFalse(r.canBubble());
+        assertNull(r.getNotification().getBubbleMetadata());
         assertFalse(r.getNotification().isBubbleNotification());
     }
 
@@ -395,7 +425,8 @@
         NotificationRecord r = getNotificationRecord(false /* bubble */);
         mBubbleExtractor.process(r);
 
-        assertTrue(r.canBubble());
+        assertFalse(r.canBubble());
+        assertNull(r.getNotification().getBubbleMetadata());
         assertFalse(r.getNotification().isBubbleNotification());
     }
 
@@ -414,7 +445,8 @@
 
         mBubbleExtractor.process(r);
 
-        assertTrue(r.canBubble());
+        assertFalse(r.canBubble());
+        assertNull(r.getNotification().getBubbleMetadata());
         assertFalse(r.getNotification().isBubbleNotification());
     }
 
@@ -429,7 +461,8 @@
         NotificationRecord r = getNotificationRecord(true /* bubble */);
         mBubbleExtractor.process(r);
 
-        assertTrue(r.canBubble());
+        assertFalse(r.canBubble());
+        assertNull(r.getNotification().getBubbleMetadata());
         assertFalse(r.getNotification().isBubbleNotification());
     }
 
@@ -445,7 +478,8 @@
         NotificationRecord r = getNotificationRecord(true /* bubble */);
         mBubbleExtractor.process(r);
 
-        assertTrue(r.canBubble());
+        assertFalse(r.canBubble());
+        assertNull(r.getNotification().getBubbleMetadata());
         assertFalse(r.getNotification().isBubbleNotification());
     }
 
@@ -462,7 +496,8 @@
         NotificationRecord r = getNotificationRecord(true /* bubble */);
         mBubbleExtractor.process(r);
 
-        assertTrue(r.canBubble());
+        assertFalse(r.canBubble());
+        assertNull(r.getNotification().getBubbleMetadata());
         assertFalse(r.getNotification().isBubbleNotification());
     }
 }
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 ced7804..2bea491 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -6135,8 +6135,6 @@
                 "tag", mUid, 0, nb.build(), new UserHandle(mUid), null, 0);
         NotificationRecord nr = new NotificationRecord(mContext, sbn, mTestNotificationChannel);
 
-
-
         // Test: Send the bubble notification
         mBinderService.enqueueNotificationWithTag(PKG, PKG, nr.getSbn().getTag(),
                 nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId());
@@ -6168,12 +6166,12 @@
         verify(mLauncherApps, times(1)).unregisterCallback(launcherAppsCallback.getValue());
 
         // We're no longer a bubble
-        Notification notif2 = mService.getNotificationRecord(
-                nr.getSbn().getKey()).getNotification();
-        assertFalse(notif2.isBubbleNotification());
+        NotificationRecord notif2 = mService.getNotificationRecord(
+                nr.getSbn().getKey());
+        assertNull(notif2.getShortcutInfo());
+        assertFalse(notif2.getNotification().isBubbleNotification());
     }
 
-
     @Test
     public void testNotificationBubbles_shortcut_stopListeningWhenNotifRemoved()
             throws RemoteException {
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 eb2d9fe..c700a09 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ShortcutHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ShortcutHelperTest.java
@@ -48,6 +48,7 @@
 import org.mockito.MockitoAnnotations;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 @SmallTest
@@ -73,6 +74,8 @@
     StatusBarNotification mSbn;
     @Mock
     Notification.BubbleMetadata mBubbleMetadata;
+    @Mock
+    ShortcutInfo mShortcutInfo;
 
     ShortcutHelper mShortcutHelper;
 
@@ -86,13 +89,13 @@
         when(mNr.getSbn()).thenReturn(mSbn);
         when(mSbn.getPackageName()).thenReturn(PKG);
         when(mNr.getNotification()).thenReturn(mNotif);
+        when(mNr.getShortcutInfo()).thenReturn(mShortcutInfo);
+        when(mShortcutInfo.getId()).thenReturn(SHORTCUT_ID);
         when(mNotif.getBubbleMetadata()).thenReturn(mBubbleMetadata);
         when(mBubbleMetadata.getShortcutId()).thenReturn(SHORTCUT_ID);
     }
 
     private LauncherApps.Callback addShortcutBubbleAndVerifyListener() {
-        when(mNotif.isBubbleNotification()).thenReturn(true);
-
         mShortcutHelper.maybeListenForShortcutChangesForBubbles(mNr,
                 false /* removed */,
                 null /* handler */);
@@ -124,12 +127,12 @@
     }
 
     @Test
-    public void testBubbleNoLongerBubble_listenerRemoved() {
+    public void testBubbleNoLongerHasBubbleMetadata_listenerRemoved() {
         // First set it up to listen
         addShortcutBubbleAndVerifyListener();
 
         // Then make it not a bubble
-        when(mNotif.isBubbleNotification()).thenReturn(false);
+        when(mNotif.getBubbleMetadata()).thenReturn(null);
         mShortcutHelper.maybeListenForShortcutChangesForBubbles(mNr,
                 false /* removed */,
                 null /* handler */);
@@ -138,6 +141,45 @@
     }
 
     @Test
+    public void testBubbleNoLongerHasShortcutId_listenerRemoved() {
+        // First set it up to listen
+        addShortcutBubbleAndVerifyListener();
+
+        // Clear out shortcutId
+        when(mBubbleMetadata.getShortcutId()).thenReturn(null);
+        mShortcutHelper.maybeListenForShortcutChangesForBubbles(mNr,
+                false /* removed */,
+                null /* handler */);
+
+        verify(mLauncherApps, times(1)).unregisterCallback(any());
+    }
+
+    @Test
+    public void testNotifNoLongerHasShortcut_listenerRemoved() {
+        // First set it up to listen
+        addShortcutBubbleAndVerifyListener();
+
+        // Clear out shortcutId
+        when(mNr.getShortcutInfo()).thenReturn(null);
+        mShortcutHelper.maybeListenForShortcutChangesForBubbles(mNr,
+                false /* removed */,
+                null /* handler */);
+
+        verify(mLauncherApps, times(1)).unregisterCallback(any());
+    }
+
+    @Test
+    public void testOnShortcutsChanged_listenerRemoved() {
+        // First set it up to listen
+        LauncherApps.Callback callback = addShortcutBubbleAndVerifyListener();
+
+        // App shortcuts are removed:
+        callback.onShortcutsChanged(PKG, Collections.emptyList(),  mock(UserHandle.class));
+
+        verify(mLauncherApps, times(1)).unregisterCallback(any());
+    }
+
+    @Test
     public void testListenerNotifiedOnShortcutRemoved() {
         LauncherApps.Callback callback = addShortcutBubbleAndVerifyListener();
 
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 4e82ceb..d063f10 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -57,6 +57,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_FIXED_TRANSFORM;
 import static com.android.server.wm.WindowContainer.POSITION_TOP;
 import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_NORMAL;
 
@@ -1060,6 +1061,11 @@
     @Test
     public void testApplyTopFixedRotationTransform() {
         mWm.mIsFixedRotationTransformEnabled = true;
+        final DisplayPolicy displayPolicy = mDisplayContent.getDisplayPolicy();
+        // Only non-movable (gesture) navigation bar will be animated by fixed rotation animation.
+        doReturn(false).when(displayPolicy).navigationBarCanMove();
+        displayPolicy.addWindowLw(mStatusBarWindow, mStatusBarWindow.mAttrs);
+        displayPolicy.addWindowLw(mNavBarWindow, mNavBarWindow.mAttrs);
         final Configuration config90 = new Configuration();
         mDisplayContent.computeScreenConfiguration(config90, ROTATION_90);
 
@@ -1080,6 +1086,12 @@
                 ROTATION_0 /* oldRotation */, ROTATION_90 /* newRotation */,
                 false /* forceUpdate */));
 
+        assertNotNull(mDisplayContent.getFixedRotationAnimationController());
+        assertTrue(mStatusBarWindow.getParent().isAnimating(WindowContainer.AnimationFlags.PARENTS,
+                ANIMATION_TYPE_FIXED_TRANSFORM));
+        assertTrue(mNavBarWindow.getParent().isAnimating(WindowContainer.AnimationFlags.PARENTS,
+                ANIMATION_TYPE_FIXED_TRANSFORM));
+
         final Rect outFrame = new Rect();
         final Rect outInsets = new Rect();
         final Rect outStableInsets = new Rect();
@@ -1132,6 +1144,7 @@
         assertFalse(app.hasFixedRotationTransform());
         assertFalse(app2.hasFixedRotationTransform());
         assertEquals(config90.orientation, mDisplayContent.getConfiguration().orientation);
+        assertNull(mDisplayContent.getFixedRotationAnimationController());
     }
 
     @Test
@@ -1310,7 +1323,7 @@
     }
 
     private static int getRotatedOrientation(DisplayContent dc) {
-        return dc.getLastOrientation() == SCREEN_ORIENTATION_LANDSCAPE
+        return dc.mBaseDisplayWidth > dc.mBaseDisplayHeight
                 ? SCREEN_ORIENTATION_PORTRAIT
                 : SCREEN_ORIENTATION_LANDSCAPE;
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
index 8ce5daa..e9ed20b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -603,6 +603,29 @@
     }
 
     @Test
+    public void testRequestResizeForBlastSync() {
+        final WindowState win = mChildAppWindowAbove;
+        makeWindowVisible(win, win.getParentWindow());
+        win.mLayoutSeq = win.getDisplayContent().mLayoutSeq;
+        win.reportResized();
+        win.updateResizingWindowIfNeeded();
+        assertThat(mWm.mResizingWindows).doesNotContain(win);
+
+        // Check that the window is in resizing if using blast sync.
+        win.reportResized();
+        win.prepareForSync(mock(BLASTSyncEngine.TransactionReadyListener.class), 1);
+        win.updateResizingWindowIfNeeded();
+        assertThat(mWm.mResizingWindows).contains(win);
+
+        // Don't re-add the window again if it's been reported to the client and still waiting on
+        // the client draw for blast sync.
+        win.reportResized();
+        mWm.mResizingWindows.remove(win);
+        win.updateResizingWindowIfNeeded();
+        assertThat(mWm.mResizingWindows).doesNotContain(win);
+    }
+
+    @Test
     public void testGetTransformationMatrix() {
         final int PARENT_WINDOW_OFFSET = 1;
         final int DISPLAY_IN_PARENT_WINDOW_OFFSET = 2;
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index f5ed64e..fadebaa 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -12716,7 +12716,6 @@
             @Nullable String mvnoMatchData) {
         try {
             if (!mccmnc.equals(getSimOperator())) {
-                Log.d(TAG, "The mccmnc does not match");
                 return false;
             }
             ITelephony service = getITelephony();
diff --git a/tests/AppLaunch/src/com/android/tests/applaunch/AppLaunch.java b/tests/AppLaunch/src/com/android/tests/applaunch/AppLaunch.java
index 7d750b7..1a58f17 100644
--- a/tests/AppLaunch/src/com/android/tests/applaunch/AppLaunch.java
+++ b/tests/AppLaunch/src/com/android/tests/applaunch/AppLaunch.java
@@ -115,7 +115,7 @@
     private static final int BETWEEN_LAUNCH_SLEEP_TIMEOUT = 3000; // 3s between launching apps
     private static final int PROFILE_SAVE_SLEEP_TIMEOUT = 1000; // Allow 1s for the profile to save
     private static final int IORAP_TRACE_DURATION_TIMEOUT = 7000; // Allow 7s for trace to complete.
-    private static final int IORAP_TRIAL_LAUNCH_ITERATIONS = 3;  // min 3 launches to merge traces.
+    private static final int IORAP_TRIAL_LAUNCH_ITERATIONS = 5;  // min 5 launches to merge traces.
     private static final int IORAP_COMPILE_CMD_TIMEOUT = 60;  // in seconds: 1 minutes
     private static final int IORAP_COMPILE_MIN_TRACES = 1;  // configure iorapd to need 1 trace.
     private static final int IORAP_COMPILE_RETRIES = 3;  // retry compiler 3 times if it fails.
diff --git a/tests/BlobStoreTestUtils/Android.bp b/tests/BlobStoreTestUtils/Android.bp
index 829c883..5c7c68b 100644
--- a/tests/BlobStoreTestUtils/Android.bp
+++ b/tests/BlobStoreTestUtils/Android.bp
@@ -15,6 +15,9 @@
 java_library {
   name: "BlobStoreTestUtils",
   srcs: ["src/**/*.java"],
-  static_libs: ["truth-prebuilt"],
+  static_libs: [
+    "truth-prebuilt",
+    "androidx.test.uiautomator_uiautomator",
+  ],
   sdk_version: "test_current",
 }
\ No newline at end of file
diff --git a/tests/BlobStoreTestUtils/src/com/android/utils/blob/Utils.java b/tests/BlobStoreTestUtils/src/com/android/utils/blob/Utils.java
index 6927e86..7cf58e1 100644
--- a/tests/BlobStoreTestUtils/src/com/android/utils/blob/Utils.java
+++ b/tests/BlobStoreTestUtils/src/com/android/utils/blob/Utils.java
@@ -18,12 +18,16 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import android.app.Instrumentation;
 import android.app.blob.BlobHandle;
 import android.app.blob.BlobStoreManager;
 import android.app.blob.LeaseInfo;
 import android.content.Context;
 import android.content.res.Resources;
 import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import androidx.test.uiautomator.UiDevice;
 
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
@@ -32,6 +36,8 @@
 import java.io.OutputStream;
 
 public class Utils {
+    public static final String TAG = "BlobStoreTest";
+
     public static final int BUFFER_SIZE_BYTES = 16 * 1024;
 
     public static final long KB_IN_BYTES = 1000;
@@ -68,7 +74,8 @@
 
     public static void assertLeasedBlobs(BlobStoreManager blobStoreManager,
             BlobHandle... expectedBlobHandles) throws IOException {
-        assertThat(blobStoreManager.getLeasedBlobs()).containsExactly(expectedBlobHandles);
+        assertThat(blobStoreManager.getLeasedBlobs()).containsExactly(
+                (Object[]) expectedBlobHandles);
     }
 
     public static void assertNoLeasedBlobs(BlobStoreManager blobStoreManager)
@@ -141,4 +148,16 @@
         assertThat(leaseInfo.getDescriptionResId()).isEqualTo(descriptionResId);
         assertThat(leaseInfo.getDescription()).isEqualTo(description);
     }
+
+    public static void triggerIdleMaintenance(Instrumentation instrumentation) throws IOException {
+        runShellCmd(instrumentation, "cmd blob_store idle-maintenance");
+    }
+
+    private static String runShellCmd(Instrumentation instrumentation,
+            String cmd) throws IOException {
+        final UiDevice uiDevice = UiDevice.getInstance(instrumentation);
+        final String result = uiDevice.executeShellCommand(cmd);
+        Log.i(TAG, "Output of '" + cmd + "': '" + result + "'");
+        return result;
+    }
 }